この章では、スクリプトからJavaクラスおよびインタフェースにアクセスする方法について説明します。
コード例はJavaScriptで作成されていますが、JSR 223に準拠する任意のスクリプト言語を使用できます。例をスクリプト・ファイルとして使用したり、例の式を対話型シェルで1つずつ実行したりできます。オブジェクトのフィールドやメソッドにアクセスする構文は、JavaScriptでもJavaでも同じです。
この章には、次のセクションがあります。
JavaScriptからJavaのプリミティブ型および参照型にアクセスするには、Java.type()
関数を呼び出します。この関数は、文字列として渡されたクラスの完全な名前に対応する型オブジェクトを返します。次の例は、様々な型オブジェクトを取得する方法を示しています。
var ArrayList = Java.type("java.util.ArrayList"); var intType = Java.type("int"); var StringArrayType = Java.type("java.lang.String[]"); var int2DArrayType = Java.type("int[][]");
Java.type()
関数から返された型オブジェクトは、Javaでクラス名を使用する場合と同じようにJavaScriptで使用できます。たとえば、次のように新しいオブジェクトをインスタンス化するために使用できます。
var anArrayList = new Java.type("java.util.ArrayList");
Java型オブジェクトを使用して、新しいJavaオブジェクトをインスタンス化できます。次の例は、新しいオブジェクトをインスタンス化するときに、デフォルト・コンストラクタを使用する方法と、別のコンストラクタに引数を渡す方法を示しています。
var ArrayList = Java.type("java.util.ArrayList"); var defaultSizeArrayList = new ArrayList; var customSizeArrayList = new ArrayList(16);
Java.type()
関数から返された型オブジェクトを使用して、次のようにstaticフィールドおよびメソッドにアクセスできます。
var File = Java.type("java.io.File"); File.createTempFile("nashorn", ".tmp");
static内部クラスにアクセスするには、Java.type()
メソッドに渡す引数でドル記号($)を使用します。次の例は、java.awt.geom.Arc2D
のFloat
内部クラスの型オブジェクトを返す方法を示しています。
var Float = Java.type("java.awt.geom.Arc2D$Float");
外部クラスの型オブジェクトがすでにある場合は、次のように外部クラスのプロパティとして内部クラスにアクセスできます。
var Arc2D = Java.type("java.awt.geom.Arc2D") var Float = Arc2D.Float
staticでない内部クラスの場合は、内部クラスのインスタンスをコンストラクタの最初の引数として渡す必要があります。
JavaScriptの型オブジェクトは、Javaクラスと同じように使用できますが、getClass()
メソッドから返されるjava.lang.Class
オブジェクトとは区別されます。class
およびstatic
プロパティを使用して一方からもう一方を取得できます。次の例は、この区別を示しています。
var ArrayList = Java.type("java.util.ArrayList"); var a = new ArrayList; // All of the following are true: print("Type acts as target of instanceof: " + (a instanceof ArrayList)); print("Class doesn't act as target of instanceof: " + !(a instanceof a.getClass())); print("Type is not the same as instance's getClass(): " + (a.getClass() !== ArrayList)); print("Type's `class` property is the same as instance's getClass(): " + (a.getClass() === ArrayList.class)); print("Type is the same as the `static` property of the instance's getClass(): " + (a.getClass().static === ArrayList));
このコンパイル時のクラス式と実行時のクラス・オブジェクトを区別する点で、JavaScriptとJavaコードは構文的にも意味的にも似ています。ただし、Javaではコンパイル時のクラス式がオブジェクトとして表現されることはないため、Class
オブジェクトのstatic
プロパティに相当するものはありません。
Javaクラスに単純名でアクセスするには、importPackage()
およびimportClass()
関数を使用してJavaパッケージおよびクラスをインポートします。これらの関数は、互換性スクリプト(mozilla_compat.js
)に組み込まれています。次の例は、importPackage()
およびimportClass()
関数の使用方法を示しています。
// Load compatibility script load("nashorn:mozilla_compat.js"); // Import the java.awt package importPackage(java.awt); // Import the java.awt.Frame class importClass(java.awt.Frame); // Create a new Frame object var frame = new java.awt.Frame("hello"); // Call the setVisible() method frame.setVisible(true); // Access a JavaBean property print(frame.title);
JavaパッケージにアクセスするにはPackages
グローバル変数(たとえば、Packages.java.util.Vector
やPackages.javax.swing.JFrame
)を使用しますが、標準Java SEパッケージにはショートカット(Packages.java
を表すjava
、Packages.javax
を表すjavax
、Packages.org
を表すorg
)があります。
java.lang
パッケージは、そのクラスがObject
、Boolean
、Math
およびその他の組込みJavaScriptオブジェクトと競合するため、デフォルトではインポートされません。さらに、Javaパッケージまたはクラスをインポートすると、JavaScriptのグローバル変数スコープと競合する可能性があります。これを避けるには、次の例に示すように、JavaImporter
オブジェクトを定義し、with
文を使用してインポートされたJavaパッケージおよびクラスのスコープを制限します。
// Create a JavaImporter object with specified packages and classes to import var Gui = new JavaImporter(java.awt, javax.swing); // Pass the JavaImporter object to the "with" statement and access the classes // from the imported packages by their simple names within the statement's body with (Gui) { var awtframe = new Frame("AWT Frame"); var jframe = new JFrame("Swing JFrame"); };
Java配列オブジェクトを作成するには、最初にJava配列型オブジェクトを取得し、次にそれをインスタンス化する必要があります。JavaScriptで配列要素とlength
プロパティにアクセスする構文は、次の例に示すように、Javaと同じです。
var StringArray = Java.type("java.lang.String[]"); var a = new StringArray(5); // Set the value of the first element a[0] = "Scripting is great!"; // Print the length of the array print(a.length); // Print the value of the first element print(a[0]);
JavaScript配列を取得したら、Java.to()
メソッドを使用してそれをJava配列に変換できます。このメソッドには、JavaScript配列変数を渡し、さらに返される配列の型を文字列または型オブジェクトとして渡す必要があります。配列型引数を省略して、Object[]
配列を返すこともできます。変換は、ECMAScriptの変換規則に従って行われます。次の例は、様々なJava.to()
メソッドの引数を使用してJavaScript配列をJava配列に変換する方法を示しています。
// Create a JavaScript array var anArray = [1, "13", false]; // Convert the JavaScript array to a Java int[] array var javaIntArray = Java.to(anArray, "int[]"); print(javaIntArray[0]); // prints the number 1 print(javaIntArray[1]); // prints the number 13 print(javaIntArray[2]); // prints the number 0 // Convert the JavaScript array to a Java String[] array var javaStringArray = Java.to(anArray, Java.type("java.lang.String[]")); print(javaStringArray[0]); // prints the string "1" print(javaStringArray[1]); // prints the string "13" print(javaStringArray[2]); // prints the string "false" // Convert the JavaScript array to a Java Object[] array var javaObjectArray = Java.to(anArray); print(javaObjectArray[0]); // prints the number 1 print(javaObjectArray[1]); // prints the string "13" print(javaObjectArray[2]); // prints the boolean value "false"
Java配列を取得したら、Java.from()
メソッドを使用してそれをJavaScript配列に変換できます。次の例は、現在のディレクトリ内にあるファイルのリストを含むJava配列を、同じ内容を持つJavaScript配列に変換する方法を示しています。
// Get the Java File type object var File = Java.type("java.io.File"); // Create a Java array of File objects var listCurDir = new File(".").listFiles(); // Convert the Java array to a JavaScript array var jsList = Java.from(listCurDir); // Print the JavaScript array print(jsList);
注: ほとんどの場合、Java配列をJavaScript配列に変換しなくても、スクリプトでJava配列を使用できます。 |
JavaScriptでJavaインタフェースを実装する構文は、Javaで匿名クラスを宣言する場合とほぼ同じです。インタフェースをインスタンス化し、同じ式でそのメソッドを(JavaScript関数として)実装します。次の例は、Runnable
インタフェースの実装方法を示しています。
// Create an object that implements the Runnable interface by implementing // the run() method as a JavaScript function var r = new java.lang.Runnable() { run: function() { print("running...\n"); } }; // The r variable can be passed to Java methods that expect an object implementing // the java.lang.Runnable interface var th = new java.lang.Thread(r); th.start(); th.join();
あるメソッドがメソッドを1つだけ持つインタフェースを実装するオブジェクトを受け取る場合は、このメソッドにオブジェクトのかわりにスクリプト関数を渡すことができます。たとえば、前の例のThread()
コンストラクタは、メソッドが1つだけ定義されたRunnable
インタフェースを実装するオブジェクトを受け取ります。自動変換を利用して、オブジェクトのかわりにスクリプト関数をこのThread()
コンストラクタに渡すことができます。次の例は、Runnable
インタフェースを実装せずにThread
オブジェクトを作成する方法を示しています。
// Define a JavaScript function function func() { print("I am func!"); }; // Pass the JavaScript function instead of an object that implements // the java.lang.Runnable interface var th = new java.lang.Thread(func); th.start(); th.join();
サブクラスに複数のインタフェースを実装するには、関連する型オブジェクトをJava.extend()
関数に渡します。詳細は、「具象Javaクラスの拡張」を参照してください。
抽象Javaクラスの匿名サブクラスをインスタンス化するには、そのコンストラクタに、抽象メソッドを実装する関数を値とするプロパティを持つJavaScriptオブジェクトを渡します。メソッドがオーバーロードされると、JavaScript関数はメソッドのすべてのバリアントに実装を提供します。次の例は、TimerTask
抽象クラスのサブクラスをインスタンス化する方法を示しています。
var TimerTask = Java.type("java.util.TimerTask"); var task = new TimerTask({ run: function() { print("Hello World!") } });
コンストラクタを呼び出して引数を渡すかわりに、new
式の後に引数を直接指定できます。次の例は、(Javaの匿名内部クラスの定義と同じように)この構文を使って前の例の2行目を簡単にする方法を示しています。
var task = new TimerTask { run: function() { print("Hello World!") } };
抽象クラスに抽象メソッドが1つだけある場合(SAMタイプ)は、コンストラクタにJavaScriptオブジェクトを渡すかわりに、そのメソッドを実装する関数を渡すことができます。次の例は、SAMタイプの使用時に構文を簡単にする方法を示しています。
var task = new TimerTask(function() { print("Hello World!") });
どちらの構文を選んでも、引数を指定してコンストラクタを呼び出す必要がある場合は、実装オブジェクトまたは関数の後に引数を指定できます。
SAMタイプを引数として取るJavaメソッドを呼び出す場合は、そのメソッドにJavaScript関数を渡すことができます。Nashornは、期待されるクラスのサブクラスをインスタンス化し、その関数を使用して唯一の抽象メソッドを実装します。次の例は、TimerTask
オブジェクトを引数として受け取るTimer.schedule()
メソッドを呼び出す方法を示しています。
var Timer = Java.type("java.util.Timer"); Timer.schedule(function() { print("Hello World!") });
注: 前の構文は、期待されるSAMタイプがインタフェースであるか、またはNashornが期待されるクラスのサブクラスをインスタンス化するために使用するデフォルト・コンストラクタを持っていることを前提としています。デフォルト以外のコンストラクタは使用できません。 |
あいまいさを回避するため、抽象クラスを拡張する構文では具象クラスを使用できません。具象クラスはインスタンス化できるため、このような構文は、クラスの新しいインスタンスを作成し、そのインスタンスにコンストラクタが受け取るオブジェクトを渡そうとしていると解釈される可能性があります(期待されるオブジェクト型がインタフェースである場合)。この具体例として、次の例を考えてみます。
var t = new java.lang.Thread({ run: function() { print("Thread running!") } });
このコードは、run()
メソッドの指定された実装を使ってThread
クラスを拡張していると解釈することも、Runnable
インタフェースを実装するオブジェクトをコンストラクタに渡すことでThread
クラスをインスタンス化している(詳細は、「Javaインタフェースの実装」を参照)と解釈することもできます。
具象Javaクラスを拡張するには、その型オブジェクトを、サブクラスの型オブジェクトを返すJava.extend()
関数に渡します。次に、指定されたメソッド実装を使ってサブクラスのインスタンスを作成するために、サブクラスの型オブジェクトをJavaScriptからJavaへのアダプタとして使用します。次の例は、run()
メソッドの指定された実装を使ってThread
クラスを拡張する方法を示しています。
var Thread = Java.type("java.lang.Thread"); var threadExtender = Java.extend(Thread); var t = new threadExtender() { run: function() { print("Thread running!") }};
Java.extend()
関数は、複数の型オブジェクトのリストを取ることができます。Javaクラスの型オブジェクトは1つしか指定できませんが、Javaインタフェースの型オブジェクトは必要な数だけ指定できます。返された型オブジェクトは、指定されたクラス(クラスを指定しなかった場合はjava.lang.Object
)を拡張し、すべてのインタフェースを実装します。クラスの型オブジェクトがリストの先頭である必要はありません。
スーパークラスのメソッドにアクセスするには、Java.super()
関数を使用します。例3-1は、java.lang.Exception
クラスを拡張して、スーパークラスのメソッドにアクセスする方法を示しています。
例3-1 スーパークラスのメソッドへのアクセス(super.js)
var Exception = Java.type("java.lang.Exception"); var ExceptionAdapter = Java.extend(Exception); var exception = new ExceptionAdapter("My Exception Message") { getMessage: function() { var _super_ = Java.super(exception); return _super_.getMessage().toUpperCase(); } } try { throw exception; } catch (ex) { print(exception); }
例3-1のコードを実行すると、標準出力に次が出力されます。
jdk.nashorn.javaadapters.java.lang.Exception: MY EXCEPTION MESSAGE
前の項では、Javaクラスを拡張し、実装を指定するコンストラクタの追加のJavaScriptオブジェクト・パラメータを使用してインタフェースを実装する方法について説明しました。したがって、実装はクラス全体ではなく、new
で作成された実際のインスタンスにバインドされます。これにはいくつかの利点があります。たとえば、Nashornは実装されるあらゆる型の組合せに対応する単一の汎用アダプタを作成できるため、実行時のメモリー・サイズの点で有利です。しかし、次の例は、異なるインスタンスが、異なるJavaScript実装オブジェクトを持つにもかかわらず、同じJavaクラスを持つことを示しています。
var Runnable = java.lang.Runnable; var r1 = new Runnable(function() { print("I'm runnable 1!") }); var r2 = new Runnable(function() { print("I'm runnable 2!") }); r1.run(); r2.run(); print("We share the same class: " + (r1.class === r2.class));
前の例は次を出力します。
I'm runnable 1! I'm runnable 2! We share the same class: true
インスタンス化するクラスを外部APIに渡す場合は(たとえば、JavaFXフレームワークを使用するときは、Application
クラスをJavaFX APIに渡してインスタンス化します)、クラスのインスタンスではなくクラスにバインドされた実装を使ってJavaクラスを拡張するか、またはインタフェースを実装する必要があります。実装をクラスにバインドするには、実装を含むJavaScriptオブジェクトをJava.extend()
関数の最後の引数として渡します。これにより、コンストラクタで追加の実装オブジェクト・パラメータが不要になるため、元のクラスと同じコンストラクタを持つクラスが作成されます。次の例は、実装をクラスにバインドする方法と、その場合は呼出しが異なれば実装クラスも異なることを示しています。
var RunnableImpl1 = Java.extend(java.lang.Runnable, function() { print("I'm runnable 1!") }); var RunnableImpl2 = Java.extend(java.lang.Runnable, function() { print("I'm runnable 2!") }); var r1 = new RunnableImpl1();var r2 = new RunnableImpl2(); r1.run(); r2.run(); print("We share the same class: " + (r1.class === r2.class));
前の例は次を出力します。
I'm runnable 1! I'm runnable 2! We share the same class: false
実装オブジェクトをコンストラクタの呼出しからJava.extend()
関数の呼出しに移動すると、コンストラクタの呼出しで追加の引数が不要になります。クラス固有の実装オブジェクトを使ってJava.extend()
関数を呼び出すたびに、新しいJavaアダプタ・クラスが生成されます。クラスにバインドされた実装を持つアダプタ・クラスも、特定のインスタンスの動作をさらにオーバーライドするために、追加のコンストラクタ引数を取ることができます。このように、Java.extend()
関数に渡されたクラスベースのJavaScript実装オブジェクトで実装の一部を提供する方法と、コンストラクタに渡されたオブジェクトでインスタンス用の実装を提供する方法の2つを組み合わせることができます。コンストラクタに渡されたオブジェクトで定義される関数は、クラスにバインドされたオブジェクトで定義される関数をオーバーライドします。次の例は、クラスにバインドされたオブジェクトで定義される関数をコンストラクタに渡された関数でオーバーライドする方法を示しています。
var RunnableImpl = Java.extend(java.lang.Runnable, function() { print("I'm runnable 1!") }); var r1 = new RunnableImpl(); var r2 = new RunnableImpl(function() { print("I'm runnable 2!") }); r1.run(); r2.run(); print("We share the same class: " + (r1.class === r2.class));
前の例は次を出力します。
I'm runnable 1! I'm runnable 2! We share the same class: true
Javaメソッドは、引数の型でオーバーロードできます。Javaコンパイラ(javac
)は、コンパイル時に正しいメソッド・バリアントを選択します。Nashornから呼び出されたJavaメソッドのオーバーロードの解決は、メソッドが呼び出された時点で行われます。正しいメソッド・バリアントが引数の型に基づいて自動的に選択されます。ただし、実際の引数の型に本当のあいまいさがある場合は、特定のオーバーロード・バリアントを明示的に指定できます。これにより、Nashornエンジンは呼出し時にオーバーロードの解決を行う必要がなくなるため、パフォーマンスが向上する可能性があります。
オーバーロード・バリアントは、特別なプロパティとして公開されます。これらは、メソッドの名前とそれに続くカッコで囲まれた引数の型からなる文字列の形式で参照できます。次の例は、引数としてObject
クラスを受け取るSystem.out.println()
メソッドのバリアントを呼び出し、それに"hello"
を渡す方法を示しています。
var out = java.lang.System.out; out["println(Object)"]("hello");
前の例では、非修飾クラス名(Object
)で正しいシグネチャが一意に識別されるため、これで十分です。シグネチャに完全修飾のクラス名を使用する必要があるのは、同一の非修飾名を持つ2つのオーバーロード・バリアントが異なるパラメータ型を使用する場合のみです(これは、異なるパッケージに同じ名前を持つパラメータ型が含まれていた場合に起こる可能性があります)。
JavaとJavaScript間のほとんどの変換は、期待どおりに機能します。これまでの項では、JavaとJavaScript間のあまり目立たないデータ型マッピングについていくつか説明しました。たとえば、配列は明示的に変換する必要があり、JavaScript関数はJavaメソッドへのパラメータとして渡されたときにSAMタイプに自動的に変換されます。すべてのJavaScriptオブジェクトは、APIでマップを直接受け取ることができるようにjava.util.Map
インタフェースを実装しています。Java APIに数値が渡されると、その数値は期待されるターゲット数値型(boxedまたはプリミティブ)に変換されます。ただし、ターゲット型が限定的でない場合(Number
など)は、Number
型になることしか期待できず、その型がボクシングされたDouble
、Integer
、Long
およびその他であるかどうかを特に検査する必要があります。数値は内部の最適化によって任意のboxed型になります。また、JavaScript仕様で定義されたToNumber
の変換アルゴリズムが値に適用されるため、任意のJavaScript値を、boxed型またはプリミティブ型の数値を受け取るJava APIに渡すことができます。JavaメソッドがString
またはBoolean
オブジェクトを受け取る場合は、JavaScript仕様で定義されたToString
およびToBoolean
の変換で許可されるすべての変換を使用して値が変換されます。
注意: 文字列操作の内部パフォーマンスの最適化により、JavaScript文字列がjava.lang.String 型にならず、java.lang.CharSequence 型になることがあります。java.lang.String 引数を受け取るJavaメソッドにJavaScript文字列を渡すと、Java String が返されますが、メソッドのシグネチャがより汎用的である場合(たとえば、java.lang.Object パラメータを受け取る場合)は、Java String オブジェクトではないがCharSequence を実装するprivateエンジン実装クラスのオブジェクトを取得できます。 |