木曜日 4 17, 2008
木曜日 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 => []