土曜日 4 19, 2008
土曜日 4 19, 2008
前回、前々回に引き続いて、JRubyからJavaクラスAPIへアクセスする場合のTIPSの紹介です。
3回目の今回は、JRubyにおけるJavaインタフェースの実装に関して、もう少し深く堀下げてみたいと思います。
JRubyでは、Javaのクラスを親クラスとするサブクラスをRubyクラスとして定義することができますが、Javaのインタフェース実装をRuby側で定義することもできます。
前回のエントリで述べたように、JavaのインタフェースはJRuby上ではModule型にマッピングされますので、通常のRubyにおけるmixed-inクラスと同様にinclude文によってRubyクラスに取り込みます。ただし、一般的なmixed-inクラスと異なりJavaインタフェースはメソッドの実装がありませんので、includeしたJavaインタフェースのメソッド実装をクラスに記述します。以下の例は、Swing JButtonクラスのaddActionListener(java.awt.event.ActionListener)メソッドにRubyクラスとして定義したMyActionListenerクラスオブジェクトを与えてみた場合の例です。
>> class MyActionListener
>> include java.awt.event.ActionListener
>> def actionPerformed(ev)
>> p ev
>> end
>> end
=> nil
>> button = javax.swing.JButton.new
=> ...
>> button.add_action_listener(MyActionListener.new)
上記のinclude宣言はJRuby 1.0.xでは必須でしたが、JRuby 1.1では実装するJavaインタフェースをincludeする必要がなくなりました。
# JRuby 1.1の場合 >> class MyActionListener >> def actionPerformed(ev) >> p ev >> end >> end => nil >> button = javax.swing.JButton.new => ... >> button.add_action_listener(MyActionListener.new)
これは強い型づけの言語に慣れてたJava開発者から見ると、とても不思議な現象に思えるかも知れません。しかし、JRubyはRubyの動的な性質を備えているため、こんな芸当があっさりできてしまいます。具体的に何が起こっているかというと、Javaのメソッドが実行された時、JRubyランタイムがメソッド引数の型と与えられた引数の型をチェックし、不足しているインタフェースを実装したプロキシーで引数のオブジェクトをラップし、その後でJavaのメソッドを実行しているのです。
この現象を理解するために、もう一度上記スクリプトの動作をチェックしてみましょう。MyActionListenerクラスのオブジェクトを生成した時点では、そのオブジェクトは純粋なRubyクラスオブジェクトであり、java_class()メソッドにも応答できません。すなわち、Javaのプロキシーオブジェクトも関連付けられていません。
>> class MyActionListener
>> def actionPerformed(ev)
>> p ev
>> end
>> end
=> nil
>> listener = MyActionListener.new
=> #<MyActionListener:0x65394b>
>> listener.respond_to? :java_class
=> false # java_class()メソッドがない
そして、このオブジェクトをSwing JButtonのaddActionListener(ActionListener)メソッドに与えた後、もう一度このオブジェクトを調べてみると、今度はjava_class()メソッドに反応してプロキシーオブジェクトが設定されており、必要なActionListenerインタフェースも勝手に組み込まれていることが分かります。
>> button = javax.swing.JButton.new 'Button' => ... >> button.add_action_listener listener # リスナを追加 => nil >> listener.respond_to? :java_class => true # java_class()メソッドが存在する >> listener.java_class => $Proxy7 # プロキシーが作られている >> listener.java_class.interfaces => [java.awt.event.ActionListener] # インタフェースが組み込まれている
なお、この動的プロキシーの生成はクラスに対して行なわれるのではなく、オブジェクト毎に行なわれます。そのため、もう一つ別のMyActionListenerオブジェクトを生成してみると、それは純粋なRubyクラスオブジェクトとして生成されることが分かります。
>> another_listener = MyActionListener.new
=> #<MyActionListener:0x1289e48>
>> another_listener.respond_to? :java_class
=> false
なお、Rubyでは、クラスが変更される前に生成されたインスタンスでも、クラスの変更後の変更内容が即座に反映されます。上記の例ではactionPerfomed()メソッドの実装を変えてMyActionListenerクラスを再定義すると、JButtonオブジェクトに追加済みのリスナオブジェクトを入れ換えることなく、直ちにactionPerformed()メソッドの変更が反映できることが確認できます。これは、もう一つのスクリプト言語Groovyでは得られない動的な性質です。
JRuby 1.0.xでは利用できませんが、JRuby 1.1からは、Javaインタフェースの実装にクロージャを使うこともできるようになりました。先ほどのActionListenerの例をクロージャを利用して書き換えると以下のように非常にシンプルな記述になります。
>> button.add_action_listener do |ev| ?> p ev >> end => nil
クロージャなのでインタフェースどころか、実装すべきメソッド名さえ明示する必要がない点に注意して下さい。追加したクロージャベースのActionListenerを取り出して調べてみると、きちんとActionListenerインタフェースを実装したプロキシーでラップされていることが分かります。
>> button.action_listeners[0] => #<#<Class:01x39471b>:0x6b3fc7 @java_object=$Proxy7> >> button.action_listeners[0].java_class.interfaces => [java.awt.event.ActionListener]
上記の場合はActionListenerのインタフェースメソッドがactionPerformed(ActionEvent)メソッドの1つだけでしたが、インタフェースに複数のメソッドがある場合でもクロージャによるインタフェース実装を行なうことができます。例えば、java.awt.event.MouseListenerインタフェースは以下のように5つのイベントメソッドが定義されています。
public interface MouseListener extends EventListener {
public void mouseClicked(MouseEvent e);
public void mouseEntered(MouseEvent e);
public void mouseExited(MouseEvent e);
public void mousePressed(MouseEvent e);
public void mouseReleased(MouseEvent e);
}
このような場合でも先ほどと同じようにクロージャでリスナを代用することができます。
>> button.add_mouse_listener do |ev| ?> p ev >> end => nil >> button.mouse_listeners.to_ary => [#<Java::JavaxSwingPlafBasic::BasicButtonListener:0x3d5149 @java_ object=javax.swing.plaf.basic.BasicButtonListener@bf9a12>, #<#<Class :01x1a3bff5>:0x18f73cb @java_object=$Proxy8>] # 2つ目が追加したもの >> button.mouse_listeners[1].java_class.interfaces => [java.awt.event.MouseListener]
この場合、インタフェースに定義されている全てのメソッドが同じクロージャの定義で実装されたことになります。このようなイベントをダンプする簡単なアダプタを追加して動作を確認したい場合、JRubyはとても簡単な方法を提供してくれます。
ただし、クロージャでリスナを代用した場合、実行途中でリスナの動作を変えたい場合は、Javaの場合と同様に一旦コンポーネントからリスナを外し、リスナの実装を変えてから再度リスナの登録が必要になります。クロージャは定義が手軽ですがクロージャ定義はProcクラスのインスタンスそのものですので、この点は仕方がありません。JavaインタフェースをRubyで実装する場合に、Rubyクラスを使うか、クロージャを使うかは目的に応じて選択して下さい。
木曜日 4 17, 2008
前回のエントリに続いて、JRubyからJavaクラスAPIへアクセスする際のTIPSを紹介します。
今回のテーマは、JRubyランタイムにおけるJavaクラスとJavaインタフェースクラスの扱いの基本的な概念についてです。
JRubyランタイム上でJavaのオブジェクトを生成した場合、そのJavaオブジェクトは必ずそのJavaクラスに対応するプロキシーオブジェクトでラップされます。例えば、java.lang.Integerのオブジェクトを生成するとそのプロキシークラスとなるJava::JavaLang::Integerクラスのオブジェクトが生成されます。通常、Rubyオブジェクトのクラスを取得する場合は、class()メソッドを使用しますが、JRuby上でJavaクラスオブジェクトの参照に対して、class()メソッドを実行すると、Javaオブジェクトをラップしたプロキシーオブジェクトのクラスが取得されます。
>> i = java.lang.Integer.new 0 => #<Java::JavaLang::Integer:0xfd8f9c @java_object=0> >> i.class => Java::JavaLang::Integer # java.lang.IntegerのRubyクラス(プロキシークラス) >> i.class.class => Class # Rubyクラスのクラス
class()メソッドは元々Ruby Objectに備わったメソッドです。java.lang.Object#getClass()メソッドに対応するRubyスタイルのプロパティメソッドではありません。前のエントリで、Javaクラスのビーンプロパティの全てのgetterメソッドに対して、"get"プリフィックスを取り除いたRubyスタイルのプロパティメソッドが追加されることを述べましたが、純粋なRubyとしての互換性を考慮した結果、java.lang.Object#getClass()のようにRubyスタイルのプロパティメソッドが追加されない例外が存在することに注意してください。
Ruby上のJavaオブジェクトから、それがラップしている本来のJavaクラス参照を取得するためには、java_class()メソッドを使用します。
>> i.java_class => java.lang.Integer # java.lang.IntegerのJavaクラス >> i.java_class.java_class => java.lang.Class # Javaクラスのクラス
また、java_class()メソッドはJavaクラスのプロキシークラスにも用意されていますので、特定のJavaクラスの参照は以下のようにしても取得できます。
>> java.lang.Integer
=> Java::JavaLang::Integer # java.lang.IntegerのRubyクラス(プロキシークラス)
>> java.lang.Integer.java_class
=> java.lang.Integer # java.lang.IntegerのJavaクラス
このようにJavaのオブジェクトを扱う場合、RubyのクラスとJavaのクラスを区別して考えなければいけない場合があります。しかし、Javaクラスの参照をJavaのメソッドの引数に与える場合、これら2つのクラスの違いを意識する必要はありません。JavaのプロキシークラスはJavaクラスに自動的に変換されます。例えば以下のようなJavaのクラスがあるとします。
public class Foo {
public static void print(Class c) {
System.out.println("The class is: " + c);
}
}
この場合、Foo.print(Class)メソッドに与える引数が本来のJavaクラスの参照であっても、プロキシークラスの参照であっても正しく動作します。
>> include_class 'Foo' => ["Foo"] >> Foo.print java.lang.Integer The class is: class java.lang.Integer # プロキシークラスはJavaクラスに自動変換される => nil >> Foo.print java.lang.Integer.java_class The class is: class java.lang.Integer => nil
一見複雑そうに見えるJRubyにおけるJavaクラスの二重生活("secret double-life")ですが、この二重生活があるおかげで静的なJavaクラスに対してRubyの柔軟性のメリットもたらしており、Javaのクラスであっても継承を使うことなくメソッドの追加や再定義を行うことができるようになっています。例えば、Javaのクラスにはデフォルトで"get"プリフィックスなしのgetterメソッドや、アンダースコア(_)スタイルのメソッドが追加されていますが、これらの追加メソッドはこのプロキシークラスに対して追加されているわけです。
先ほどのJavaメソッドFoo.print(Class)に対して、Javaのインタフェースクラスを適用して見たらどうなるでしょうか。
>> Foo.print java.lang.Runnable.java_class
The class is: interface java.lang.Runnable
=> nil
>> Foo.print java.lang.Runnable
TypeError: expected [java.lang.Class]; got: [org.jruby.RubyModule]; error: argument type mismatch
from ...
java_class()メソッドでJavaのRunnableインタフェースクラスを抽出して引数に渡した場合は正しく動作しましたが、java_class()メソッドを省略した場合は失敗してしまいました。これはなぜでしょうか。
実はRubyの世界では、メソッド実装を持たないインタフェースという概念がありません。Javaの世界では実装を持つことができるクラスと実装を持たないインタフェースが概念として分離されていますが、クラスもインタフェースもJava上ではjava.lang.Class型として表現されています。JRubyでは、JavaのクラスはRubyのClass型にマッピングされますが、JavaのインタフェースはModule型にマッピングされるという違いがあります。
>> java.lang.Integer.class => Class >> java.lang.Runnable.class => Module
ここで少しModule型について説明しておきます。RubyにおいてClass型とModule型の機能的な違いは、それ自身からインスタンスを生成できるかどうかだけです。RubyにおけるModule型の代表的な使われ方は、クラスや変数をグループ化してネームスペースを区別するための箱として使われる場合と、mixed-in(あるいは、mix-in)クラスとして使われる場合の2通りがあります。
1つめの使われ方ですが、Module型はネームスペース空間として用いられるため、JRubyではJavaのパッケージをRubyのModule型にマッピングしています。
>> java.lang.class => Module
2つ目の使われ方のmixed-inクラスというのは、オブジェクト指向における多重継承の問題を回避しつつメソッドの実装を複数のクラスで共有するための代表的な手法で、クラス継承ツリーの制約を受けずにメソッドの実装を任意のクラスに付与することができる特別なクラスのことを言います。Javaではmixed-inの概念がないため、似たような実装のメソッドを複数のクラスに重複して実装せざるを得ない場合があります。例えば、SwingコンポーネントのJComboBoxクラスとAbstractButtonクラスは共にjava.awt.ItemSelectableインタフェースを実装していますが、クラス継承のパスが同一ではないため、ItemSelectableインタフェースのメソッドはJComboBoxクラスとAbstractButtonクラスに重複して実装せざるを得なくなっています。このようにJavaのインタフェースは実装を持つことができないという欠点がありますが、それ自身ではインスタンス化することができず、多重継承が可能であるという性質であることから、比較的性質の近いModule型にマッピングされる仕様になっています。
>> java.lang.Runnable.class => Module
JavaインタフェースがなぜClass型ではなくModule型にマッピングされたかの説明が長くなりましたが、JRubyからJavaオブジェクトのメソッド引数にJavaインタフェースクラスの参照を渡す必要がある場合は、class()メソッドではなくjava_class()メソッドを使ってJavaのインタフェースクラスを取り出す必要があります。例えばjavax.swing.JComponent#getListeners(Class)メソッドを呼び出す場合は以下のようなスクリプトになります。
>> label = javax.swing.JLabel.new 'label' => ... >> label.get_listeners java.awt.event.MouseListener.java_class => #<#<Class:01xc3362f>:0x1a5770 @java_object=[Ljava.awt.event.MouseListener;@13f348b> # さらにto_ary()メソッドを適用するとEventListener配列の中身を表示できる >> label.get_listeners(java.awt.event.MouseListener.java_class).to_ary => []
金曜日 4 11, 2008
先日、JRubyの最新バージョン1.1の正式版がリリースされました。JRuby 1.1の目玉はjrubycコマンドによるAOT(Ahead Of Time)コンパイルが利用できるようになったことですが、それ以外にも本家MRI(Matz Ruby Implementation)に対する互換性の改善やRubyランタイムからJavaオブジェクトへアクセス方法の改善が非常に細かいレベルで行われています。
このエントリでは、純粋なRubyとしてJRubyを使う場合ではなく、RubyからJavaクラスAPIを使用する場合のTIPSをいくつか紹介します。
純粋にJavaだけを使用した開発プロジェクトに参加しているプログラマや設計者にとっても、JRubyは簡単な動作検証のための道具としてとても役に立ちます。プログラマは時々既存のJavaクラスライブラリのAPI仕様書からは読み取れない動作仕様を確認するため、プロダクション用のソースコードを書く前に(あるいは書いている途中で)、これらのAPIを呼び出す小さなテストプログラムを書いて、実際の振る舞いを確認することがあります。
このような場合、JRubyのjirbコマンドを使って、目的のJavaクラスオブジェクトをJRubyランタイム上に生成し、インタラクティブに目的のメソッドを呼び出して実行結果を確認したり、オブジェクトの状態を簡単に確認することができます。
もちろん、このような目的であればJRuby以外のスクリプト言語でも同様の目的を果たすことができます。例えば、GroovyははじめからJavaランタイムを前提としたスクリプト言語であるため、Javaのシンタックスに近い形で、スクリプトからJavaのクラスオブジェクトにアクセスできます。どのスクリプト言語を選択するかは好みによるところが大きいと思いますが、JRubyにはGroovyにはない柔軟性があるため、個人的にはJRubyを好んで使用しています。その一方で、JRubyからJavaクラスAPIへのアクセスにはちょっとしたコツが必要になります。以下では、JRuby初心者が分かりにくいと思われるJavaクラスAPIアクセスに関するTIPSをいくつか紹介します。
Javaクラスのメソッド名はtoOctetString()のようにinterCap形式で定義するのが一般的ですが、Rubyの文化では単語の区切りにアンダースコア(_)を使う方が好まれているようです。JRubyでは全てのinterCap形式のメソッド名に対応してアンダースコアで区切られたメソッド名をランタイムに追加しています。例えば、java.lang.Integerクラスに定義されているtoXXXString()はto_xxx_string()としてもアクセスできるようになっています。
>> integer = java.lang.Integer.new 0
>> integer.methods.each {|m| p m if /to.*[Ss]tring/ =~ m};''
"toBinaryString"
"toString"
"toOctalString"
"to_octal_string"
"to_string"
"toHexString"
"to_hex_string"
"to_binary_string"
>> java.lang.Integer.to_hex_string 255
=> "ff"
また、ビーンプロパティのアクセッサメソッドも、Rubyの文化に合わせgetterにはプリフィックス"get"をつけないものが追加され、setterの代わりに演算子"="を使用した代入文のためのメソッドも追加されています。
>> button = javax.swing.JButton.new
>> button.methods.each {|m| p m if /.*[Ll]abel/ =~ m};''
"getLabel"
"setLabel"
"label"
"label="
"get_label"
"set_label"
>> button.label = 'Press Me!'
=> "Press Me!"
さらにbooleanタイプのプロパティの場合は、"is<プロパティ名>"に対して"<プロパティ名>?"という形式のメソッドも追加されます。
>> button = javax.swing.JButton.new
>> button.methods.each {|m| p m if /.*[Oo]paque.*/ =~ m};''
"set_opaque"
"opaque="
"opaque?"
"is_opaque"
"isOpaque"
"opaque"
"setOpaque"
>> button.opaque?
=> true
interCap形式のメソッドを使うか、Rubyらしいメソッドを使うかは好みの問題だと思いますので、どちらかお好きな方を使用してください。このように同じことをやるのに、たくさんの表現形式を用意してしまうところなどをみると、JRuby実装は本当にRubyらしさを追求しているなと思います。
これはJavaクラスのアクセスに特化した話ではないのですが、Rubyの言語仕様に合わせて、クラスのstaticなメンバ変数(定数)にアクセスする場合には、"::"を使用します。
>> javax.swing.JSlider::VERTICAL => 1 # インタフェースクラスから直接参照することも可能 >> javax.swing.SwingConstants::HORIZONTAL => 0
staticメソッドにアクセスする場合は、"::"または"."のどちらかを使用します。staticなメンバ変数のアクセスに"."は使用できません。
>> java.util.Calendar::getInstance() => #<Java::JavaUtil::GregorianCalendar:0x39826 @java_object=... # または >> java.util.Calendar.getInstance() => #<Java::JavaUtil::GregorianCalendar:0xc7ecd5 @java_object=...
Ruby Stringや数値(Fixnum、Float)、HashなどのオブジェクトがJava側のメソッドの引数に与えられた場合は適切なJavaのオブジェクトに自動変換されますが、Ruby配列はJavaの配列に自動的には変換されません。例えば、Ruby配列を引数にjava.util.Arrays.asList(Object[])メソッドを実行すると以下のようにタイプエラーとなります。
>> array = ["One", "Two", "Three", "Four"] => ["One", "Two", "Three", "Four"] >> java.util.Arrays.as_list array TypeError: expected [[Ljava.lang.Object;]; got: [$Proxy8]; error: argument type mismatch
Ruby配列をJavaの配列に変換するには、以下のようにto_java()メソッドを用います。
>> java.util.Arrays.as_list array.to_java
=> #<#<Class:01x12e7234>:0x4bd767 @java_object=[One, Two, Three, Four]>
to_java()メソッドには変換先Java配列のデータタイプを明示することもできます。引数を省略した場合は、Object[]型に変換されます。
>> [1,2,3].to_java => #<#<Class:01xc5575>:0x1be8bf1 @java_object=[Ljava.lang.Object;@d591a6> >> [1,2,3].to_java :byte => #<#<Class:01x13244cd>:0x14300c8 @java_object=[B@1e867d6> >> [1,2,3].to_java :int => #<#<Class:01x186dd93>:0x13d0fea @java_object=[I@1dffb78> >> [1,2,3].to_java :string => #<#<Class:01xa6faa9>:0x928739 @java_object=[Ljava.lang.String;@1ebe8ec>
引数に指定できるデータタイプは、プリミティブ型とそのラッパークラス、String、BigDecimalが指定できるようです。指定可能な引数のリストはドキュメントに明示されていないようなので、JRubyに含まれる以下のソースを確認してみてください。
なお、to_java()メソッドは多次元配列に対しても有効です。例えば、2次元配列をモデルの初期データとしてSwingのJTableオブジェクトを生成するために、コンストラクタjavax.swing.JTable#JTable(Object[][], Object[])を呼び出す場合は以下のようなスクリプトになります。
>> table = javax.swing.JTable.new( ?> [['Thomas', 'Enebo'], ['Charles', 'Nutter']].to_java, ?> ['First name', 'Last name'].to_java) => #<Java::JavaxSwing::JTable:0x53b2c @java_object=...
Javaの配列は配列要素を分かりやすく表示する専用のtoString()メソッドが用意されていないため、Javaのメソッドが返したJava配列の中身がそのままではよく分かりません。
>> javalist = java.util.Arrays.as_list array.to_java
=> #<#<Class:01x12e7234>:0x13577ca @java_object=[One, Two, Three, Four]>
>> javalist.to_array
=> #<#<Class:01xc5575>:0x1f854bd @java_object=[Ljava.lang.Object;@1f80c0e>
Javaの配列をRuby配列に変換するには、to_ary()メソッドを使用します。
>> javalist.to_array.to_ary
=> ["One", "Two", "Three", "Four"]
まだ、他にもありますが今日のところはここまでにしておきます。あまり、JRuby 1.1での拡張部分にふれることが出来なかったため、続きは次回のエントリで書きます。