JRubyいろいろ - 「Ruby逆引きレシピ」のJRubyレシピ

Ruby逆引きレシピ」にはJRubyのレシピが3つ、161, 162, 163あるんですね!うれしいなぁ、こんなところにもJRubyが、、、こうしてあちらこちらでJRubyのあれこれが増えると、きっと使ってくれる人も増えるよね。

でも!あ、ちょっと違う‥、なところが。もうちょっと説明がほしいかも‥なところがいくつか。ま、これまではあまりまとまった情報はないし、kenaiのWikiは古いの新しいのがごちゃ混ぜで、どれが正しいのかよくわからなかったのかもしれないので、やむを得ないでしょう。いまなら、core developersの執筆本(Using JRuby: Bringing Ruby to Java by Charles O Nutter, Thomas Enebo, Nick Sieger, Ola Bini, and Ian Dees | The Pragmatic Bookshelf)が出ているので、これが一番いいのでは。が、まだ邦訳は出ていないので、今回のJRubyいろいろは勝手にレシピ修正&追加編を果敢に実行と行きましょう。


その1 レシピ161を勝手に修正

hello_jruby.rb(レシピ161から引用)

require 'java'
import 'java.lang.System'

System.out.println("Hello, JRuby")

うん、間違ってはいない。でも、なんか、このいかにもRubyシンタックスJavaを使ってみました風がいまひとつ。。。かつて、私がJavaを使い始めた十数年前に、Javaシンタックスを使ったC言語なプログラムを書いていたのを思い出すような。。。で、こんなんどう?Rubyっぽくなったでしょ?

require 'java'
Java::JavaLang::System.out.println "こんにちは, JRuby"

Rubyな人なら一目でわかるmodule定義を使った方法ですね!JRubyjavaライブラリはこの例のようなモジュールでのクラス呼び出しができるようにしてくれているんです。ついでに日本語も大丈夫。これはRuby 1.8.7モード。

で、もうひとつ。import文が何をしているのかの説明が欲しかったかも。JRubyのimport文はJavaのimport文とは若干違う使い方をするので。ええ、そうです。モジュール定義の中でつかえるんですよ。レシピ163では include_packageを使っていますが、importも使えるんです。JRubyJavaライブラリがやっているみたいに。ちなみに、こんな MyJavaモジュールを定義すると、java.lang.Systemもjava.util.concurrent.ConcurrentHashMapもMyJavaなクラスみたいにして使えちゃう。

require 'java'

module MyJava
  import "java.lang"
  import "java.util.concurrent"
end

MyJava::System.out.println "こんにちは, JRuby"
h = MyJava::ConcurrentHashMap.new
h.put("日本語", "はろ〜")
h.put("英語", "Hello")
puts h

これを実行すると、こんな風にプリントされる。

こんにちは, JRuby
{英語=Hello, 日本語=はろ〜}

Hashの方の順番がputした順じゃないのはオブジェクトがJavaのhashだからなんですよ。Javaの場合、hashの順番は保証されていませんからね。

では、もしもimportしたいパッケージがたくさんあったらどうする?ひとつひとつ書いていく、、、?いやいや、Rubyなプログラムでそんなことはしませんよ、ね。

module MyJava
  ['lang', 'util', 'util.concurrent'].each do |name|
    import "java.#{name}"
  end
end

な感じで、配列に並べていけばよし、と。Rubyな人なら、もっといろいろアイディアがありそう。


ここで、ちょっとレシピ163に飛んで、、、サンプルにjava.utilを使っていますが、これはすでにJRubyがモジュール定義しているので、

h = Java::JavaUtil::HashMap.new

で行ける。自分が書いたクラスとか、jp.xxx.xxxなクラスとかはモジュール定義のありがたみがあるじゃないかと。


次ぎは jirbね。起動はjirbでもいいんだけれど、jruby -S irbの方がいろいろなオプションを指定できて便利。たとえば、Ruby 1.9モードで使いたい場合は

jruby --1.9 -vS irb
jruby 1.5.0 (ruby 1.9.2dev trunk 24787) (2010-05-12 6769999) (Java HotSpot(TM) Client VM 1.5.0_24) [i386-java]

なんかできる。

ま、これはいいとして、、、「jirbはrequire 'java'が実行された状態で起動するため」はうそだよぉ〜 (^^;; このサンプルはJRubyが内部で自動的にJavaライブラリをロードするコードだったから、そう勘違いしただけだと思う。その証拠に、


ー追記ー
!!!おぉっと、失礼。JRuby 1.5.0 は起動時のパフォーマンスを良くするために、Javaライブラリをロードしなくなったから、jirbもrequire 'java'された状態では起動しなくなったのでした。レシピは明らかにJRuby 1.5.0よりも前のバージョンで書かれているので、ここは正しいはず。
ということで、JRuby 1.5.0を使う場合はrequire 'java'をお忘れなく。Nokogiriも1.4.2から、内部でrequire 'java'をするようになったのですが、それ以前ははこれをやっていないので、JRuby 1.5.0でうごかん!ということがあるかも。

jirb
irb(main):001:0> require 'java'
=> true
irb(main):002:0> require 'java'
=> false

ね。最初のrequire 'java'はJavaライブラリをロードしました、の、true。二回目のrequire 'java'はもうロードしてあるから、二度はロードしないよ、の、falseになっているでしょ?駄目押しに、Javaライブラリがロードされていないと使えない、to_javaメソッドを試すと、

jirb
irb(main):001:0> "Heeey!".to_java
NoMethodError: undefined method `to_java' for "Heeey!":String
	from (irb):1
irb(main):002:0> require 'java'
=> true
irb(main):003:0> "Heeey!".to_java
=> #

というように、jirbは決してJavaライブラリがロードされた状態では起動しません。Javaのクラスを使うときはrequire 'java'を習慣にしておくといいかも。付け足すと、レシピのコードはJRubyJavaプロクシが使われたので、自動的にJavaのライブラリがロードされたものなのですよ。


その2 レシピ162を勝手に修正
(レシピ162から引用)

require 'java'
require 'example.recipe.sample.jar'

sample = SampleClass.new

って、require 'example.recipe.sample.jar'がビミョー。このrequireは必要なのかもしれないけれど、無駄かもしれない。もしも、requireしないと動かないと仮定すると、そこからわかるのは、"example.recipe.sample.jar"っていう名前のjarアーカイブjrubyコマンドを実行したディレクトリ(カレントディレクトリ)にあって、かつ、カレントディレクトリにクラスパスが通っていないということ。もしも、これが、

require '/Users/yoko/example/recipe/sample.jar'

だったら、あぁ、そこにあるjarアーカイブをロードしたのね、と素直になっとくできるんだけど。。。

このあたりは、JRubyJavaアプリでしかもRubyを動かしちゃうという複雑なところで、わかりにくいところでしょうねぇ。で、どうしたらjarアーカイブの中身をRubyで使えるようにできるかというと、、、

require '/Users/yoko/example/recipe/sample.jar'

この場合、Javaのクラスパスの設定は無用

  • ケース2 Jarアーカイブのあるところをクラスパスに設定する
export CLASSPATH=$CLASSPATH:/Users/yoko/example/recipe/sample.jar
(Windowsは : の代わりに、; でしたね)
あるいは
jruby -J-cp /Users/yoko/example/recipe/sample.jar -S irb

JRubyJavaのアプリですからね、クラスパスが設定されているところ、例えばWebアプリのWEB-INF/libとかWEB-INF/classesとかにあるものは自動的に読み込みますよ。Servletコンテナがクラスパスを設定しているから。さらに、JRubyJavaのアプリですからね、-J-cpでjarアーカイブを見にいきまよ。

(.rbの中、あるいはirbで)
$CLASSPATH << '/Users/yoko/example/recipe/sample.jar'


では、実際に試してみましょうか。こんなクラスを作って、Oak.jarというアーカイブを作ってみました。

package acorn;

public class SimpleMethods {

    public double getSqrt(int v) {
        return Math.sqrt*1;
    }
}

いろいろあるパス設定、今回はJavaっぽく、-J-cpを使ってみました。

jruby -J-cp ~/NetBeansProjects/Oak/dist/Oak.jar -S irb
irb(main):001:0> require 'java'
=> true
irb(main):002:0> m = Java::Acorn::SimpleMethods.new
=> #
irb(main):003:0> m.getSqrt 144
=> 12.0

おぉ、何やら動きましたねぇ。
パスの設定についてはこの辺が参考になるかも。


と、動いたところで、レシピ162にでているように、もっとRubyっぽい感じのメソッド名を試そうじゃないですか。で、どんなメソッド名が使えるの?と思ったら、methodsメソッドを叩いてみよー!

irb(main):004:0> m.methods
=> ["__jsend!", "get_sqrt", "__jcreate!", "sqrt", "getSqrt", "hashCode", "finalize", "getClass", "equals?", "toString", "get_class", "hash_code", "clone", "notifyAll", "to_string", "notify_all", "equals", "wait", "notify", "initialize", "java_send", "marshal_load", "java_method", "marshal_dump", "equal?", "java_class", "inspect", "eql?", "to_java_object", "synchronized", "java_object", "java_object=", "==", "to_s", "hash", "__jtrap", "java_kind_of?", "handle_different_imports", "include_class", "object_id", "frozen?", "java_package", "methods", "=~", "to_a", "tainted?", "javax", "tap", "taint", "public_methods", "dup", "method", "java_require", "singleton_methods", "is_a?", "instance_eval", "instance_variable_get", "com", "java_signature", "instance_of?", "java", "java_name", "org", "__id__", "__send__", "id", "instance_variable_set", "send", "untaint", "class", "protected_methods", "kind_of?", "enum_for", "instance_variable_defined?", "instance_variables", "display", "extend", "to_enum", "freeze", "java_implements", "private_methods", "nil?", "java_annotation", "type", "===", "instance_exec", "to_java", "respond_to?"]

ごそごそとたくさん出てきたねー。コレ全部、使えるんだけれど、目的はgetSqrt()のRubyっぽいバーションだから、最初の方を見てみると、、、get_sqrtとsqrtなんかも使えそうだ。試してみると、、、

irb(main):005:0> m.get_sqrt 121
=> 11.0
irb(main):006:0> m.sqrt 111
=> 10.5356537528527

おっけー!


さて、ここで、JavaアプリJRubyの悩みが‥。こんなJavaのクラスがあったら、どうやってRubyから使う、、、、?!

package acorn;

public class SimpleMethods {

    public double getSqrt(int v) {
        System.out.println("int version");
        return Math.sqrt*2;
    }

    public static void main(String[] args) {
        SimpleMethods m = new SimpleMethods();
        System.out.println(m.getSqrt(144));
    }
}

Javaには型があるから、3つのgetSqrtの区別はカンタン。でもRubyは型なんて気にしない、なるようになるって、の言語。String引き数はいいとして、intとlongの違いは、、、そんなクラスを定義するなっ、というのもありかもしれないけれど、あっちやらこっちやらでリリースされているAPIを使おうとしたら、自分ではどうしようもないよね。
ご安心ください。JRubyはちゃんとメソッドを区別する方法を用意しています。どのバージョンからだったかは失念。たぶん、1.4.0あたりじゃなかったかなー。じゃ、もう一度 irbで。

jruby -J-cp ~/NetBeansProjects/Oak/dist/Oak.jar -S irbirb(main):001:0> require 'java'
=> true
irb(main):002:0> m = Java::Acorn::SimpleMethods.new
=> #
irb(main):003:0> m.sqrt "144"       <--- Stringは大丈夫
String version
=> 12.0
irb(main):004:0> m.sqrt 144        <--- Fixnumはjava.lang.Longにマップされる
long version
=> 12.0
irb(main):010:0> m.java_send :getSqrt, [Java::int], 144 <--- java_sendを使うとintバージョンも呼べる
int version
=> 12.0
irb(main):011:0> m.java_send :getSqrt, [Java::long], 144 <--- java_sendでlongバーション
long version
=> 12.0
irb(main):013:0> m.java_send :getSqrt, [Java::JavaLang::String], "144" <--- java_sendでStringバーション
String version
=> 12.0

というこで、無事オーバーロードされたメソッドを区別して使えました。java_sendを使うと、Javaのメソッド名そのままで、エイリアスのget_sqrtやsqrtは使えないけれど、intバージョンを呼べないよりはいいでしょう。


以上、勝手にレシピ修正&追加編でしたー。

次は RailsConfの後ですね。ええ、行くんですよ、RaisConf, Baltimore。楽しみー!!!
誰か、RailsConf行く人がいたら、会場で私を見かけたら声をかけてくださいませ。

*1:double)v); } public static void main(String[] args) { SimpleMethods m = new SimpleMethods(); System.out.println(m.getSqrt(144

*2:double)v); } public double getSqrt(long v) { System.out.println("long version"); return Math.sqrt((double)v); } public double getSqrt(String v) { System.out.println("String version"); return Math.sqrt(Double.valueOf(v