Java のスクリプティング

この記事では Rhino を使用して JavaScript を超えて Java に到達する方法を説明します。Java によるスクリプティングには多くの用途があります。これは、利用可能な多くの Java ライブラリを利用して、強力なスクリプトを素早く作成することを可能にします。スクリプトを書くことで Java クラスをテストできます。 私たちは、探索的プログラミングのためのスクリプトを使用して、Java 開発を支援することもできます。探索的プログラミングとは、ライブラリや API がそれを使用するクイックプログラムを書くことによって何ができるのかを学習するプロセスです。ここからわかるように、スクリプトによってこのプロセスが簡単になります。

ECMA 標準では Java (またはそのような外部オブジェクトシステムとの通信) は扱いません。したがって、この章で扱うすべての機能を拡張機能と見なす必要があります。

Java パッケージとクラスへのアクセス

すべての Java コードはクラスの一部です。すべての Java クラスはパッケージの一部です。ただし、JavaScript では、スクリプトはパッケージ階層の外に存在します。どうしたら Java パッケージのクラスにアクセスできるでしょうか?

Rhino は Packages という名前の最上位変数を定義します。Packages 変数のプロパティはすべて javacom などのトップレベルの Java パッケージです。たとえば、java パッケージの値にアクセスできます。

js> Packages.java
[JavaPackage java]

便利なショートカットとして、Rhino は Packages.java と同等のトップレベルの変数 java を定義しています。したがって、前の例はさらに短くなる可能性があります。

js> java
[JavaPackage java]

パッケージの階層を下げるだけで Java クラスにアクセスできます。

js> java.io.File
[JavaClass java.io.File]

スクリプトが多数の異なる Java クラスにアクセスすると、毎回そのクラスの完全なパッケージ名を使用するのが面倒になることがあります。Rhino は Java の import 宣言と同じ目的を果たすトップレベル関数 importPackage を提供します。たとえば、java.io パッケージ内のすべてのクラスをインポートし、File という名前だけを使用して java.io.File クラスにアクセスできます。

js> importPackage(java.io)
js> File
[JavaClass java.io.File]

ここで importPackage(java.io) は、java.io パッケージ内のすべてのクラス (File など) を最上位レベルで使用可能にします。これは import java.io.*; Java 宣言と実質的に同じです。

Java では java.lang.* が暗黙的にインポートされるのに対し、Rhino はそうでないことに注意することが重要です。その理由は、JavaScript には java.lang パッケージで定義された名前とは異なる独自のトップレベルオブジェクト BooleanMathNumberObject、および String があるからです。この競合のため、java.lang パッケージで importPackage を使用しないことをお勧めします。

注意すべき点の1つは、Rhino が Java パッケージまたはクラス名を指定する際にエラーを処理することです。java.MyClass にアクセスすると、Rhino は java.MyClass という名前のクラスのロードを試みます。そのロードに失敗すると、java.MyClass はパッケージ名であるとみなされ、エラーは報告されません。

js> java.MyClass
[JavaPackage java.MyClass]

このオブジェクトをクラスとして使用しようとした場合にのみ、エラーが報告されます。

外部パッケージとクラス

Rhino で外部パッケージやクラスを使用することもできます。.jar または .class ファイルがクラスパス上にあることを確認してから、JavaScript アプリケーションにインポートすることができます。これらのパッケージは java パッケージにはない可能性が高いので、パッケージ名の前に "Packages." を付ける必要があります。たとえば、org.mozilla.javascript パッケージをインポートするには、次のように importPackage() を使用できます。

$ java org.mozilla.javascript.tools.shell.Main
js> importPackage(Packages.org.mozilla.javascript);
js> Context.currentContext;
org.mozilla.javascript.Context@bb6ab6

場合によっては、importClass() メソッドを使用してインポートするのではなく、例にあるようにパッケージの完全修飾名を使用します。これも可能ですが、入力が増えます。完全修飾名を使用すると、上記の例は次のようになります。

$ java org.mozilla.javascript.tools.shell.Main
js> jsPackage = Packages.org.mozilla.javascript;
[JavaPackage org.mozilla.javascript]
js> jsPackage.Context.currentContext;
org.mozilla.javascript.Context@bb6ab6

また、パッケージから1つのクラスだけをインポートする場合は、importClass() メソッドを使用してクラスをインポートできます。上記の例は、次のように表すことができます。

$ java org.mozilla.javascript.tools.shell.Main
js> importClass(Packages.org.mozilla.javascript.Context);
js>  Context.currentContext;
org.mozilla.javascript.Context@bb6ab6

Java をあわせて使用する

Java クラスにアクセスできるようになったので、次の論理的なステップはオブジェクトを作成することです。これは、Java の場合と同様に new 演算子を使用して動作します。

js> new java.util.Date()
Thu Jan 24 16:18:17 EST 2002

新しいオブジェクトを JavaScript 変数に格納すると、そのオブジェクトに対してメソッドを呼び出すことができます。

js> f = new java.io.File("test.txt")
test.txt
js> f.exists()
true
js> f.getName()
test.txt

静的メソッドおよびフィールドは、クラスオブジェクト自体からアクセスできます。

js> java.lang.Math.PI
3.141592653589793
js> java.lang.Math.cos(0)
1

JavaScript では、Java と異なり、メソッド自体はオブジェクトであり、呼び出されるだけでなく評価されます。メソッドオブジェクトを単独で表示すると、メソッドのさまざまなオーバーロードされた形式を見ることができます。

js> f.listFiles
function listFiles() {/*
java.io.File[] listFiles()
java.io.File[] listFiles(java.io.FilenameFilter)
java.io.File[] listFiles(java.io.FileFilter)
*/}

この出力は、File クラスが3つのオーバーロードされたメソッド listFiles を定義していることを示しています。1つは引数を取らず、他は FilenameFilter 引数を持つもの、FileFilter 引数を持つものです。すべてのメソッドは File オブジェクトの配列を返します。Java メソッドのパラメータと戻り値の型を見ることができるのは、メソッドを調べている可能性があり、パラメータや戻り値の型が不明な探索プログラミングで特に役立ちます。

探索的プログラミングのもう1つの有用な機能は、オブジェクトに対して定義されたすべてのメソッドとフィールドを表示する機能です。JavaScript の for..in 構文を使用して、これらの値をすべて出力することができます:

js> for (i in f) { print(i) }
exists
parentFile
mkdir
toString
wait
[44 others]

File クラスのメソッドだけでなく、(wait のような) 基本クラス java.lang.Object から継承されたメソッドもリストされていることに注意してください。これにより、深くネストされた継承階層のオブジェクトを扱うことが容易になります。これは、そのオブジェクトで使用可能なすべてのメソッドを見ることができるからです。

Rhino は、JavaBeans のプロパティにプロパティ名で直接アクセスできるようにすることで、別の便利さを提供します。JavaBean のプロパティ foo は、getFoosetFoo のメソッドで定義されています。さらに、同じ名前のブール値プロパティは、isFoo メソッドで定義することができます。たとえば、次のコードは実際に File オブジェクトの getName メソッドと isDirectory メソッドを呼び出します。

js> f.name
test.txt
js> f.directory
false

オーバーロードされたメソッドの呼び出し

引数の型に基づいて呼び出すメソッドを選択するプロセスはオーバーロード解決と呼ばれます。Java では、オーバーロードの解決はコンパイル時に実行され、Rhino では実行時に行われます。第2章で議論したように、JavaScript の動的型指定を使用すると、この違いは避けられません。変数の型は実行時まで認識されないため、過負荷解決が発生するだけです。

例として、いくつかのオーバーロードされたメソッドを定義し、それらを呼び出す以下の Java クラスを考えてみましょう。

public class Overload {

    public String f(Object o) { return "f(Object)"; }
    public String f(String s) { return "f(String)"; }
    public String f(int i)    { return "f(int)"; }

    public String g(String s, int i) { return "g(String,int)"; }
    public String g(int i, String s) { return "g(int,String)"; }

    public static void main(String[] args) {
        Overload o = new Overload();
        Object[] a = new Object[] { new Integer(3), "hi", Overload.class };
        for (int i = 0; i != a.length; ++i)
            System.out.println(o.f(a[i]));
    }
}

プログラムをコンパイルして実行すると、出力が生成されます

f(Object)
f(Object)
f(Object)

しかし、同様のスクリプトを書くと

var o = new Packages.Overload();
var a = [ 3, "hi", Packages.Overload ];
for (var i = 0; i != a.length; ++i)
    print(o.f(a[i]));

それを実行すると、出力が得られます

f(int)
f(String)
f(Object)

Rhino は実行時にオーバーロードされたメソッドを選択するため、引数に一致するより具体的な型を呼び出します。その間、Java はコンパイル時に引数の型だけでオーバーロードされたメソッドを選択します。

これは、各呼び出しでより良い一致が可能なメソッドを選択する利点がありますが、より多くの作業が行われるためパフォーマンスに影響します。実際には、この性能コストは実際のアプリケーションでは顕著ではありません。

過負荷の解決は実行時に発生するため、実行時に失敗する可能性があります。たとえば、Overload のメソッド g を2つの整数で呼び出すと、どちらのメソッドの形式も他よりも引数の型が近くないため、エラーが発生します。

js> o.g(3,4)
js:"<stdin>", line 2: The choice of Java method Overload.g
matching JavaScript argument types (number,number) is ambiguous;
candidate methods are: 
class java.lang.String g(java.lang.String,int)
class java.lang.String g(int,java.lang.String)

オーバーロードセマンティクスのより正確な定義については、Java メソッドのオーバーロードと LiveConnect 3 を参照してください。

Java インターフェイスの実装

Java クラスにアクセスし、Java オブジェクトを作成し、それらのオブジェクトのフィールド、メソッド、プロパティにアクセスできるようになったので、私たちはすぐに大きな力を持っています。しかし、それだけでは不十分な例がいくつかあります。Java の多くの API は、クライアントが実装しなければならないインターフェースを提供することで機能します。その1つの例は Thread クラスです。そのコンストラクタは、新しいスレッドが開始されたときに呼び出される単一メソッドの run を含む Runnable を取ります。

このニーズに対応するため、Rhino はインターフェイスを実装する新しい Java オブジェクトを作成する機能を提供します。まず、Java インターフェイスで必要とされるメソッドと名前が一致する関数プロパティーを持つ JavaScript オブジェクトを定義する必要があります。Runnable を実装するには、パラメータを指定せずに実行するメソッドを1つだけ定義する必要があります。第3章から覚えていれば、{propertyName:value} 表記で JavaScript オブジェクトを定義することは可能です。この構文を関数式と組み合わせて使用すると、run メソッドで JavaScript オブジェクトを定義できます。

js> obj = { run: function () { print("\nrunning"); } }
[object Object]
js> obj.run()

running

Runnable を構築することによって、Runnable インターフェイスを実装するオブジェクトを作成できます。

js> r = new java.lang.Runnable(obj);
[object JavaObject]

Java ではインプリメンテーションが利用できないため、インターフェイス上で new 演算子を使用することはできません。ここで Rhino は JavaScript オブジェクト obj から実装を取得します。Runnable を実装するオブジェクトができたので、Thread を作成して実行することができます。run に対して定義した関数は、新しいスレッドで呼び出されます。

js> t = new java.lang.Thread(r)
Thread[Thread-2,5,main]
js> t.start()
js>

running

最後の js プロンプトと新しいスレッドからの出力は、スレッドのスケジューリングに応じてどちらの順序でも表示されます。

Rhino は Runnable を実装する新しい Java クラスのバイトコードを生成し、run メソッドへのすべての呼び出しを関連する JavaScript オブジェクトに転送します。このクラスを実装するオブジェクトは Java アダプタと呼ばれます。JavaScript への転送は実行時に行われるため、呼び出されるまでインターフェイスを実装するメソッドの定義を遅らせることができます。必要なメソッドを省略することは大規模なプログラミングにおいては悪い習慣ですが、小さなスクリプトや探索的プログラミングには便利です。

JavaAdapter コンストラクタ

前のセクションでは、Java インターフェイスで new 演算子を使用して Java アダプタを作成しました。このアプローチには限界があります。複数のインターフェイスを実装することは不可能であり、非抽象クラスを拡張することもできません。これらの理由から、JavaAdapter コンストラクタがあります。

JavaAdapter コンストラクタの構文は次のとおりです。

new JavaAdapter(javaIntfOrClass, [javaIntf, ..., javaIntf,] javascriptObject)

ここで javaIntfOrClass は実装するインターフェイスまたは拡張するクラスであり、javaIntf は実装するための追加のインターフェイスです。javascriptObject は、Java アダプタから呼び出されるメソッドを含む JavaScript オブジェクトです。

実際には、JavaAdapter コンストラクタを直接呼び出す必要はほとんどありません。ほとんどの場合、new 演算子を使用する前の構文で十分です。

Java インターフェイスとしての JavaScript 関数

多くの場合、前述の Runnable の例のように、またはさまざまなイベントリスナの実装を提供する場合のように、1つのメソッドだけを持つインターフェイスを実装する必要があります。 これを容易にするために、Rhino はそのようなインターフェイスが期待されるときに JavaScript 関数を渡すことができます。この関数はインターフェイスメソッドの実装として呼び出されます。

以下は単純化された Runnable の例です:

js> t = java.lang.Thread(function () { print("\nrunning"); });
Thread[Thread-0,5,main]
js> t.start()
js> 
running

Rhino では、すべてのメソッドが同じシグネチャを持つ場合、複数のメソッドを持つ Java インターフェイスの実装として JavaScript 関数を使用することもできます。関数を呼び出すと、Rhino はメソッドの名前を追加の引数として渡します。関数は、呼び出されたメソッドに代わって関数を使用して区別できます。

js> var frame = new Packages.javax.swing.JFrame();
js> frame.addWindowListener(function(event, methodName) {
	if (methodName == "windowClosing") {     
            print("Calling System.exit()..."); java.lang.System.exit(0);
	}
    });
js> frame.setSize(100, 100);
js> frame.visible = true;
true
js> Calling System.exit()...

Java 配列の作成

Rhino には Java 配列を作成するための特別な構文はありません。このためには java.lang.reflect.Array クラスを使用する必要があります。5つの Java 文字列の配列を作成するには次の呼び出しを行います。

js> a = java.lang.reflect.Array.newInstance(java.lang.String, 5);
[Ljava.lang.String;@7ffe01

プリミティブ型の配列を作成するには、java.lang パッケージの関連オブジェクトクラスで定義されている特殊な TYPE フィールドを使用する必要があります。たとえば、バイトの配列を作成するには特別なフィールド java.lang.Byte.TYPE を使用する必要があります。

js> a = java.lang.reflect.Array.newInstance(java.lang.Character.TYPE, 2);
[C@7a84e4

結果の値は、その型の Java 配列が許可されている任意の場所で使用できます。

js> a[0] = 104
104
js> a[1] = 105
105
js> new java.lang.String(a)
hi

Java 文字列と JavaScript 文字列

Java 文字列と JavaScript 文字列は同じではないことに注意してください。Java 文字列は java.lang.String 型のインスタンスであり、そのクラスによって定義されたすべてのメソッドを持ちます。JavaScript 文字列には String.prototype で定義されたメソッドがあります。最も一般的な障害は length です。これは Java 文字列のメソッドであり、JavaScript 文字列の動的プロパティです。

js> javaString = new java.lang.String("Java")
Java
js> jsString = "JavaScript"
JavaScript
js> javaString.length()
4
js> jsString.length
10

Rhino は2つの型の違いを減らすための助けとなります。まず Java 文字列を Java メソッドに渡し、Rhino が変換を実行します。 前の例の java.lang.String コンストラクタの呼び出しで実際にこの機能が動作していました。

Rhino は、java.lang.String クラスがまだそれらを定義していない場合、JavaScript メソッドを Java 文字列で使用できるようにします。例えば:

js> javaString.match(/a.*/)
ava

JavaImporter コンストラクタ

JavaImporter は、Java のスクリプト作成時に明示的なパッケージ名を省略できる新しいグローバルコンストラクタです。

var SwingGui = JavaImporter(Packages.javax.swing,
                            Packages.javax.swing.event,
                            Packages.javax.swing.border,
                            java.awt.event,
                            java.awt.Point,
                            java.awt.Rectangle,
                            java.awt.Dimension);
...

with (SwingGui) {
    var mybutton = new JButton(test);
    var mypoint = new Point(10, 10);
    var myframe = new JFrame();
...
}

これまでこのような機能は org.mozilla.javascript.ImporterTopLevel クラスをトップレベルのスコープとして使用した埋め込みにのみ使用できました。このクラスでは、スクリプト用の importPackage() および importClass() グローバル関数が追加されていますが、広範囲に使用すると Java クラスの名前でグローバル名前空間が汚染され、ロードされたクラスがガベージコレクションから保護されます。

詳細については Bugzilla 245882 を参照してください。

Java 例外

Java メソッドによってスローされた例外は、try ... catch 文を使用して JavaScript コードで捕捉できます。Rhino は Java 例外を次のプロパティを持つエラーオブジェクトにラップします。

  • javaException: Java メソッドによってスローされた元の例外
  • rhinoException: Rhino ランタイムでラップされた例外

instanceof 演算子を使用すると、例外の型を問い合せることができます。

try { 
    java.lang.Class.forName("NonExistingClass"); 
} catch (e) {
    if (e.javaException instanceof java.lang.ClassNotFoundException) {
       print("Class not found");
    }
}

Rhino は、例外の条件付きキャッチを定義する try ... catch ステートメントの拡張もサポートしています。

function classForName(name) {
    try {
        return java.lang.Class.forName(name);
    } catch (e if e.javaException instanceof java.lang.ClassNotFoundException) {
        print("Class " + name + " not found");
    } catch (e if e.javaException instanceof java.lang.NullPointerException) {
        print("Class name is null");
    }
}

classForName("NonExistingClass");
classForName(null);

ドキュメントのタグと貢献者

タグ: 
最終更新者: mdnwebdocs-bot,