8. JRuby から Java へのアクセス

ここでは jruby 1.1.6 に固有の特徴を説明する。 その目的は,君が jruby を Java のクラスを利用するための簡易な言語として使えるようにすることである。

8.1 文字エンコーディング

jruby 1.1.6 は,ruby 1.8.6 と同じく文字を UTF-8/EUC-JP/SJIS 扱いするように指定できる。 無指定時は 1 バイトずつの列として扱う。

p /^あ*$/ === "ああ"

を内容とするファイルを作成して実験してみよう。 これはマルチバイト文字「あ」を1文字の「あ」として認識しないと true にならない。

$ cat a.rb
p /^あ*$/ === "ああ"
$ nkf -w a.rb | jruby
false
$ nkf -w a.rb | jruby -Ku
true
$ nkf -e a.rb | jruby -Ke
true
$ nkf -s a.rb | jruby -Ks
true
$ nkf -s a.rb | jruby -Ke
false
[…以下略…]

文字エンコーディングが一致するとき,正しく認識されていることが分かる。

p "あ"

を内容とするファイルを作成して,マルチバイト文字「あ」が 出力時に1文字の「あ」として認識されるか実験してみよう。

$ cat b.rb
p "あ"
$ nkf -w b.rb | jruby | nkf
"\343\201\202"
$ nkf -w b.rb | jruby -Ku | nkf
"あ"
$ nkf -e b.rb | jruby -Ke | nkf
"あ"
$ nkf -s b.rb | jruby -Ks | nkf
"あ"
$ nkf -s b.rb | jruby -Ke | nkf
"\202\240"
[…以下略…]

文字エンコーディングが一致するとき,正しく認識されていることが分かる。

また,無指定時を含めいずれの場合でも,単純な文字列入出力 (putsgets など) では,文字列をバイト透過に扱うから,バイナリ・データとして漢字や かな を扱うことができる。

$ cat c.rb
puts "あ"
$ jruby c.rb
あ
$ jruby -Ku c.rb
あ
$ jruby -Ke c.rb
あ
$ jruby -Ks c.rb

8.1.1 制限事項

現在の制限事項として,1 章 注釈で述べたように, 文字エンコーディングの指定にかかわらず,第2バイトが 「\」 になるいわゆる SJIS ダメ文字は,ruby と異なり,正しく字句解析できない。

$ nkf -S sososo.rb
puts "ソソソ"
$ ruby sososo.rb | nkf
sososo.rb:1: unterminated string meets end of file
$ ruby -Ks sososo.rb | nkf
ソソソ
$ jruby -Ks sososo.rb | nkf
:1: sososo.rb:2: unterminated string meets end of file (SyntaxError)

マルチバイト文字のバイト表現と ASCII 文字のバイト表現が重ならない EUC-JP や UTF-8 には,この問題はない。

また,文字エンコーディングの指定にかかわらず, Java との相互作用では,jruby の文字列はつねに UTF-8 として解釈される。 下記に示すように,Java にバイト列として例えば 0xE3 0x81 0x82 を渡すと,それが「あ」の文字コードとして解釈される。

$ cat d.rb
include Java
java.lang.System.out.println("あ")
$ nkf -w d.rb | jruby | nkf
あ
$ nkf -w d.rb | jruby -Ku | nkf
あ
$ nkf -w d.rb | jruby -Ke | nkf
あ
$ nkf -e d.rb | jruby -Ke | nkf
??
UTF-8 決め打ちなのは,jruby の文字列と Java の Unicode 文字列の相互変換です。 それ以外の,Java が jruby を経由せずに文字列を入出力する方法は,普通の Java プログラムと同じく Java 自身が決めます。 上記の例で Java の java.lang.System.out.println メソッドは,Unicode の「あ」をプラットフォームのデフォルト文字エンコーディング (例えば SJIS) で印字します。

以上のような制限事項から,今のところ jruby のプログラムは UTF-8 で記述するのが最も妥当である。 そうすることは現在の技術的トレンドとも合致している。 -Ku オプションを指定すれば,正規表現に日本語文字を使っても問題ない。

正規表現等での文字エンコーディングのサポートは,前回の版のチュートリアルでとりあげた jruby 1.0.3 と比べ,jruby 1.1.6 で大きく改良された点です。 ただ,SJIS についていえば,筆者の環境 (OS X 10.4.11) のせいかどうか,jruby 1.1.6 に -Ks を与えて d.rb を実行すると異常終了します。 jirb-Ks を与えたときも異常終了します。

8.2 標準 Java ライブラリへのアクセス

下記を行うことによって標準 Java ライブラリへのアクセスが可能になる (参照: "JRuby Cookbook", November 2008, §1.5)。

include Java

あるいは include するかわりに,こうしてもよい (参照: Calling Java from JRuby - JRubyWiki)。

require 'java'
後者の書き方を採った場合,もしも誰かが jruby と互換仕様の ruby-java ブリッジを作れば,オリジナルの ruby で jruby と同じスクリプトを動かせるようになるはずです。

UTF-8 で下記のスクリプトを作成し,jruby で実行すると,

include Java

frame = javax.swing.JFrame.new("窓の例")
label = javax.swing.JLabel.new("こんにちは")
frame.getContentPane.add(label)
frame.setDefaultCloseOperation(javax.swing.JFrame::EXIT_ON_CLOSE)
frame.pack
frame.setVisible(true)

このようなウィンドウが表示される:

ここで javax.swing.JFrame という表現がなにか Ruby 言語の範囲外の魔法に見えるかもしれないが, 実は完全に正当な Ruby 式である。上記をメソッド呼出しの丸括弧を省略しないで書くとこうなる。

include(Java)

frame = javax().swing().JFrame().new("窓の例")
label = javax().swing().JLabel().new("こんにちは")
frame.getContentPane().add(label)
frame.setDefaultCloseOperation(javax().swing().JFrame()::EXIT_ON_CLOSE)
frame.pack()
frame.setVisible(true)

つまり,定数 Java, EXIT_ON_CLOSE とローカル変数 frame, label を除き,このプログラムにある識別子は実はすべてメソッド名である。 このように "魔法を解いた" かたちにすれば自明なことだが, 長々しく javax.swing.JFrame などと繰り返し書くのが面倒ならば,

include Java

swing = javax.swing
JFrame = swing.JFrame

frame = JFrame.new("窓の例")
label = swing.JLabel.new("こんにちは")
frame.getContentPane.add(label)
frame.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE)
frame.pack
frame.setVisible(true)

のように Java のパッケージ名やクラス名 (のように見えるメソッド呼出しの戻り値) を 変数や定数に代入して,それを使い回せばよい。 メソッド呼出しの回数を減らすことになるから,若干の効率改善も期待される。

完全に正当な Ruby 式ですから,逆にいえば,オリジナルの ruby で jruby 互換仕様の ruby-java ブリッジを実現することは技術的には可能なはずです。 誰か作られた方,または作ろうとしている方はいらっしゃらないでしょうか。
include Java をせずに二重コロン記法を使って Java::javax.swing.JFrame のようにアクセスすることもできます。 8.8 節も見てください。

8.3 Ruby らしさのための同義語

Java の命名規則は Ruby とは異なる。 jruby は,Java のメソッド名を Ruby の命名規則にしたがって変換した同義語メソッドによって Ruby 話者の便宜を図っている。

setXyz(a)xyz=(a)
例: frame.setVisible(true) ⇒ frame.visible = true
getXyz()xyz()
例: frame.getContentPane ⇒ frame.contentPane
abcDefGhi(…)abc_def_ghi(…)
例: frame.contentPane ⇒ frame.content_pane

下記はそれを利用した例である。

include Java

swing = javax.swing
JFrame = swing.JFrame

frame = JFrame.new("窓の例")
label = swing.JLabel.new("こんにちは")
frame.content_pane.add(label)
frame.default_close_operation = JFrame::EXIT_ON_CLOSE
frame.pack
frame.visible = true
上記の framelabel が具体的にどんなメソッドを用意しているのか, メソッドの一覧をとって調べてみましょう。

8.4 Ruby による Java インタフェースの実装

Ruby のクラス定義で Java のインタフェース (の名前のように見えるメソッド呼出しの戻り値)include し,対応するメソッドを定義することによって,Java のインタフェースを実装できる。

include Java

swing = javax.swing
JFrame = swing.JFrame

frame = JFrame.new("§8.4")
button = swing.JButton.new("カウンタ")

class ButtonAction
  include java.awt.event.ActionListener

  def initialize(button)
    @button = button
    @count = 0
  end

  def actionPerformed(evt)
    @count += 1
    @button.text = "%03d" % @count
  end
end

button.add_action_listener(ButtonAction.new(button))
frame.content_pane.add(button)
frame.default_close_operation = JFrame::EXIT_ON_CLOSE
frame.pack
frame.visible = true

このようなウィンドウが表示される:

ボタンを1回押すとこのようになる:

ボタンをさらに1回押すとこうなる:

8.5 Ruby による Java クラスの拡張

Java のクラス (の名前のように見えるメソッド呼出しの戻り値) を基底クラスとして Ruby のクラスを定義すれば, Ruby で Java のクラスを拡張できる。

include Java

Swing = javax.swing
JFrame = Swing.JFrame

class Button < Swing.JButton
  include java.awt.event.ActionListener

  def initialize
    super("000")
    @count = 0
    add_action_listener(self)
  end

  def actionPerformed(evt)
    @count += 1
    self.text = "%03d" % @count
  end
end

frame = JFrame.new("§8.5")
button = Button.new
frame.content_pane.add(button)
frame.default_close_operation = JFrame::EXIT_ON_CLOSE
frame.pack
frame.visible = true
このリストの 3 行目で swing = javax.swing ではなく Swing = java.swing としていることに注意してください。 (小文字ではじまる) ローカル変数に代入しているのではなく (大文字ではじまる) 定数として定義しています。
jruby 1.1.6 のバグなのかどうか不明ですが,jruby 1.0.3 と異なり,ローカル変数 swing に代入して 6 行目のクラス定義で基底クラス swing.JButton を参照すると,undefined method `JButton' for nil:NilClass (NoMethodError) というエラーが発生します。

8.6 import

Java のクラスを表す定数を定義するとき,

JFrame = javax.swing.JFrame

と書くかわりに,Java プログラムと同じように

import javax.swing.JFrame

と書いてよい。これは,括弧を省略せずに書けば,すべてメソッド呼出しの組み合わせ

import(javax().swing().JFrame())

である。 ただし,その結果として得られる JFrame は Ruby の他のクラス名と同じく 定数 である。 前節の例をこの書き方で表すと,次のようになる。

include Java

import javax.swing.JFrame
import javax.swing.JButton
import java.awt.event.ActionListener

class Button < JButton
  include ActionListener

  def initialize
    super("000")
    @count = 0
    add_action_listener(self)
  end

  def actionPerformed(ev)
    @count += 1
    self.text = "%03d" % @count
  end
end

frame = JFrame.new("§8.5.1")
button = Button.new
frame.content_pane.add(button)
frame.default_close_operation = JFrame::EXIT_ON_CLOSE
frame.pack
frame.visible = true

8.7 配列の相互変換

Ruby の配列は to_java メソッドで Java の配列に変換できる。 メソッドを無引数で呼び出した場合,配列は java.lang.Object[] に変換される。 次の引数を与えて Java の配列の要素型を指定することもできる。

現在の実装では,Ruby の配列に不適切な型の要素があっても, かなり強引に型を自動変換する。 この振舞は実用的だが,注意が必要である。 逆変換には to_a メソッドを使う。

下記の jirb セッションでは,Ruby の文字列から配列 a を得て, それを Java の byte 配列 bbに変換してから, to_a で逆変換したり, java.lang.String のコンストラクタに与えて Java の文字列 ss にしている。

irb(main):001:0> a = "the great ruby".unpack("C*")
=> [116, 104, 101, 32, 103, 114, 101, 97, 116, 32, 114, 117, 98, 121]
irb(main):002:0> bb = a.to_java(:byte)
=> [B@ff6313
irb(main):003:0> bb.to_a
=> [116, 104, 101, 32, 103, 114, 101, 97, 116, 32, 114, 117, 98, 121]
irb(main):004:0> ss = java.lang.String.new(bb)
=> #<Java::JavaLang::String:0x61116f @java_object=the great ruby>
irb(main):005:0> ss.to_s
=> "the great ruby"
jirb の場合,include Java は不要です。 なお,jruby 1.1.6 では a.to_java(:decimal) はできないようです。

8.8 君の Java ライブラリへのアクセス

非標準的な Java ライブラリへ jruby からアクセスするには,require メソッドに jar ファイルの名前を文字列として与える。 モジュール Java の関数であるかのように,Java パッケージを参照できる。 ただし,java, javax, com, org 以外で始まるパッケージ名は Java:: を接頭した二重コロン記法でアクセスする必要がある。 include Java ではアクセスできない。

下記の例は,Java による Lisp インタープリタ L2Lisp 5.0 の jar ファイルを require "../tmp/L2Lisp.jar" で取り込み, LL = Java::jp.co.okisoft.ngo.suzuki611.llsp.LL によって main 関数を含む LL クラスを得て,そしてその main 関数を実行している。

ここで main 関数の引数 [tf.path].to_java(:string)tf は, 文字列 script の内容 (簡単な Lisp スクリプトからなる) を書き込んだ一時ファイルである。 そのパス名 tf.path を唯一の要素とする Ruby リストを,to_java(:string) を使って Java の String[] 値に変換している。

script = '
(princ "1 + 2 = ") (print (+ 1 2))
(princ "100! = ")  (print (reduce * (range 1 101)))
'

require "tempfile"
tf = Tempfile.new("lisp_script")
tf.puts(script)
tf.close

require "../tmp/L2Lisp.jar"
LL = Java::jp.co.okisoft.ngo.suzuki611.llsp.LL
LL.main([tf.path].to_java(:string))

下記がその実行例である。確かに Lisp スクリプトが実行されている。

$ jruby ll.rb
1 + 2 = 3
100! = 9332621544394415268169923885626670049071596826438162146859296389521759999
32299156089414639761565182862536979208272237582511852109168640000000000000000000
00000
$ 

おわりに

ここでは jruby から Java のクラスを利用する観点での jruby の特徴と機能を説明した。

本章で述べたほかにも, jruby は java.util.Collectioneach イテレータを定義し, Enumerable モジュールを mix-in させているなど, Java のクラスを Ruby らしく簡潔に使う便宜を用意している。 そういった便宜は Ruby の人々が抱く直感に沿って用意されており, 君が Ruby に慣れるにつれ,jruby 経由で Java のクラスもどんどん便利に使えるようになるはずだ。

irb(main):001:0> a = java.util.ArrayList.new
=> #<Java::JavaUtil::ArrayList:0xd1edcc @java_object=[]>
irb(main):002:0> a.add_all [3, 1, 4, 1, 5]
=> true
irb(main):003:0> print a
[3, 1, 4, 1, 5]=> nil
irb(main):004:0> a << 9
=> #<Java::JavaUtil::ArrayList:0xd1edcc @java_object=[3, 1, 4, 1, 5, 9]>
irb(main):005:0> a.to_a
=> [3, 1, 4, 1, 5, 9]
irb(main):006:0> a.inject(1) {|x, y| x * y}
=> 540

jruby の実装はまだ決して完璧ではないが, いわゆる Java から Ruby へ,というあれかこれかの二者択一ではなく, 豊富なライブラリと,飾り気のない簡潔明快な記述という両者の長所を享受する道が, jruby によって開けているといえる。


戻る


Copyright (c) 2007, 2008 Oki Software Co., Ltd.