前回の記事 http://d.hatena.ne.jp/n_shuyo/20070627/jruby では、既存の Java アプリケーションを JRuby(jirb) の中から起動すれば、ちょうど Ruby コンソール付きで起動した状態となり、稼働中の Java アプリケーションの内部を対話的に操作できることを紹介させてもらった。
ただし、JRuby の方に Java アプリケーション内の各種インスタンスが自動的に渡ってくるわけではないので、無改造で操作できる範囲は限られる可能性が高い。まあでもほとんどのケースでは、比較的少量の改修で Ruby から存分に本体をいじくることができるんじゃないかと見込んでいる。
というわけでそれを実践するべく、先の Blokus アプリケーションにて Ruby で思考ルーチンを書くというのに挑戦してみたところ、確かにこの場合については少量の改修で成功した。
が、Blokus についてはひとまず稿を改めるとして、ここでは「 JRuby において、Ruby のオブジェクトを Java に渡す」際、かなりよくわからん挙動をするので、そこをわかっている範囲でまとめてみる。っていうか、正直まだ結構あれこれよくわかってない。へるぷみー。
早速だが、次のプログラムを動かしてみる。
本当は、たかが動作確認サンプルのくせにこんなに長いコードを blog に貼り付けたくはないのだが、かなり直感的でない結果になので、証拠がないときっと信じてもらえまい……
/* JRubyTrial.java */ import java.util.List; public class JRubyTrial { public String testToString(List a) { return a.toString(); } public int testSize(List a) { return a.size(); } private List _list = null; public void setMember(List list) { _list = list; } public List getMember() { return _list; } public int getMemberSize() { return _list.size(); } }
require 'java' include_class "JRubyTrial" t = JRubyTrial.new include_class 'java.util.ArrayList' class MyList < ArrayList def toString "MYLIST" end def size 111 end end include_class 'java.util.List' class MyList2 include List def toString "RUBYLIST" end def size 111 end end puts "== (A) ArrayList.new" p list0 = ArrayList.new puts "toString: #{t.testToString(list0)}" puts "size[default]: #{t.testSize(list0)}" def list0.size; 222; end puts "size[override]: #{t.testSize(list0)}" puts "\n== (B) MyList.new (< ArrayList)" p list1 = MyList.new t.member = list1 puts "toString: #{t.testToString(list1)}" puts "size: #{t.testSize(list1)}" puts "size[member]: #{t.memberSize}" puts "size[return member]: #{t.member.size}" def list1.size; 222; end puts "size[override]: #{t.testSize(list1)}" puts "size[member]: #{t.memberSize}" puts "size[return member]: #{t.member.size}" puts "\n== (C) MyList2.new (include List)" p list2 = MyList2.new t.member = list2 puts "toString: #{t.testToString(list2)}" puts "size: #{t.testSize(list2)}" puts "size[member]: #{t.memberSize}" puts "size[return member]: #{t.member.size}" def list2.size; 222; end puts "size[override]: #{t.testSize(list2)}" puts "size[member]: #{t.memberSize}" puts "size[return member]: #{t.member.size}" puts "\n== (D) List.new" p list3 = List.new t.member = list3 puts "size: #{t.testSize(list3)}" rescue puts "size[undefined]: #{$!}" def list3.toString; "List.new"; end def list3.size; 222; end puts "toString[ruby]: #{list3.toString}" puts "toString[java]: #{t.testToString(list3)}" puts "size[defined]: #{t.testSize(list3)}" puts "size[member]: #{t.memberSize}" puts "size[return member]: #{t.member.size}"
やっていることはシンプル。
java.util.List インターフェースを持つ Ruby オブジェクトを4通りの方法で生成、Java に渡して期待される動作をするかを確認している。
生成する4つの方法とは以下の通り。
- (A) java.util.ArrayList インスタンスを生成し、メソッドを追加定義する
- (B) java.util.ArrayList を継承したクラスを定義、そのインスタンスを生成する
- (C) java.util.List を実装したクラスを定義、そのインスタンスを生成する
- (D) java.util.List をおもむろに new して、メソッドを追加定義する
また、Java 側では toString() と size() について動作を確認できるようになっている。また、渡されたオブジェクトを一度インスタンス変数に格納し、Java 側で size() を呼んだ場合と、オブジェクトを Ruby に返してから size() を呼んだ場合も検証できるようにしてある。
これを実行した結果が以下の通り。
== (A) ArrayList.new #<Java::JavaUtil::ArrayList:0x183e7de @java_object=[]> toString: [] size[default]: 0 size[override]: 0 == (B) MyList.new (< ArrayList) #<MyList:0xa522a6 @java_object=MYLIST> toString: MYLIST size: 111 size[member]: 111 size[return member]: 0 size[override]: 222 size[member]: 222 size[return member]: 0 == (C) MyList2.new (include List) #<MyList2:0x1c9e67a @java_object=$Proxy0> toString: $Proxy0 size: 111 size[member]: 111 size[return member]: 111 size[override]: 222 size[member]: 222 size[return member]: 222 == (D) List.new #<#<Class:01x7cd37a>:0xa89ce3 @java_object=$Proxy0> size[undefined]: undefined method `size' for #<#<Class:01x7cd37a>:0xa89ce3 @java_object=$Proxy0>:#<Class:01x7cd37a> toString[ruby]: List.new toString[java]: $Proxy0 size[defined]: 222 size[member]: 222 size[return member]: 222
結果をまとめると、こうだ。
- (1) ArrayList をインスタンス化したものにメソッドを追加定義しても無効
- (2) ArrayList を継承したクラスのインスタンスを、一度 Java インスタンスのメンバに格納、Ruby に返した場合、Ruby 側で定義した内容は保証されない
- (3) List を直接実装したインスタンスの toString メソッドは override できない
(1) は納得できるし、実際のプログラミングにおいて障害になることもないだろう。問題は (2) と (3) だ。
(2) は Java のクラスを継承した Ruby クラスを Java とやりとりするのは結構危なっかしいという事を示している。
実は、JRuby の Limitations ( http://jruby.codehaus.org/Limitations ) に "Extended Java Classes in Ruby will not be visible to Java consumers" と書かれている。で、そこに例が記されているのだが、でもそれはちゃんと動いてしまうのだ……。
でもわざわざ limitations にそう書かれるということは何か不具合があるに違いないということで色々試して見つけたのがこの (2) の現象である。
このサンプルではこれ以上触れていないが、Java から返ってきたオブジェクトは Ruby 側で定義した内容が全て無くなっているかと思えば必ずしもそうではない。いっそ Java のベースクラスのインスタンスになってしまってくれていた方が挙動としてはわかりやすいくらいなのだが。
上記 Limitations には一応 "This will be fixed once we have a compiler." とも書いてある。が、そこに書いてある例は動くわけで、fix されるのが (2) の現象なのかどうかはわからない……
(3) も「 implements できないメソッドは toString だけなのか?」というあたりに確証が無くて困っている。
ともあれ、問題の範囲が予測できるという点で、Ruby から Java に渡すオブジェクトは (C) か (D) の方法で作った方が問題が起こりにくそうだということがわかるだろう。
これがわかってしまえば(そのために盛大な試行錯誤をしたのだが……)、Blokus アプリを JRuby に対応させるのは簡単だった。というわけで、それらは次回。
【追記】
はっ。もしや「Ruby で定義できるのはインターフェースに明記されているメソッドのみ」なのか!? とひらめく。
早速確認。
/* ToString.java */ public interface ToString { public String toString(); }
あとは jirb で……
irb(main):001:0> require 'java' => true irb(main):002:0> include_class 'ToString' => ["ToString"] irb(main):003:0> t = ToString.new => #<#<Class:01x15983b7>:0x1d0d124 @java_object=$Proxy0> ^^^^^^^ <= ここに Java で toString した値が出る irb(main):004:0> def t.toString; "TEST" ; end => nil irb(main):005:0> t => #<#<Class:01x15983b7>:0x1d0d124 @java_object=$Proxy0> ^^^^^^^
だめか orz