土曜日 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クラスを使うか、クロージャを使うかは目的に応じて選択して下さい。