在JavaScript中调用Java类和接口的方法

Ninjajs 2019-05-08

前言

本文中所有的代码使用 JavaScript 编写,但你也可以用其他兼容 JSR 223 的脚本语言。这些例子可作为脚本文件也可以在交互式 Shell 中一次运行一个语句的方式来运行。在 JavaScript 中访问对象的属性和方法的语法与 Java 语言相同。

本文包含如下几部分:

1、访问 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[][]");

在 JavaScript 中使用 Java.type() 函数返回的类型对象的方法跟在 Java 的类似。

例如你可以使用如下方法来实例化一个类:

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() 方法来获取对象类型,可以使用如下方法来访问静态属性以及方法:

var File = Java.type("java.io.File");
File.createTempFile("nashorn", ".tmp");

如果要访问内部静态类,可以传递美元符号 $ 给 Java.type() 方法。

下面代码显示如何返回 java.awt.geom.Arc2DFloat 内部类:

var Float = Java.type("java.awt.geom.Arc2D$Float");

如果你已经有一个外部类类型对象,那么你可以像访问属性一样访问其内部类,如下所示:

var Arc2D = Java.type("java.awt.geom.Arc2D")
var Float = Arc2D.Float

由于是非静态内部类,必须传递的是外部类实例作为参数给构造函数。

虽然在 JavaScript 中使用类型对象跟在 Java 中类似,但其与 java.lang.Class 对象还是有些区别的,这个区别就是 getClass() 方法的返回值。你可以使用 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 这样的属性,因为编译时的类表达式不作为对象。

2、导入 Java 包和类

为了根据其简单的名称来访问 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);

可以通过 Packages 全局变量来访问 Java 包,例如Packages.java.util.Vector 或者 Packages.javax.swing.JFrame。但标准的 Java SE 包有更简单的访问方式,如: java 对应 Packages.java, javax 对应 Packages.javax, 以及 org 对应 Packages.org。

java.lang 包默认不需要导入,因为这会和 ObjectBooleanMath 等其他 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");
};

3、使用 Java 数组

为了创建 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 数组:

// 创建一个 JavaScript 数组
var anArray = [1, "13", false];

// 将数组转换成 java 的 int[] 数组
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

// 将 JavaScript 数组转换成 Java 的 String[] 数组
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"

// 将 JavaScript 数组转换成 Java 的 Object[] 数组
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.from() 方法来将一个 Java 数组转成 JavaScript 数组。

下面代码演示如何将一个包含当前目录下文件列表的数组转成 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 对象。

4、实现 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();

如果一个方法希望一个对象,这个对象实现了只有一个方法的接口,你可以传递一个脚本函数给这个方法,而不是传递对象。例如,在上面的例子中 Thread() 构造函数要求一个实现了 Runnable 接口的对象作为参数。我们可以利用自动转换的优势传递一个脚本函数给 Thread() 构造器。

下面的例子展示如何创建一个 Thread 对象而无需实现 Runnable 接口:

// 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() 函数来实现多个接口。

5、扩展抽象 Java 类

你可以实例化一个匿名的抽象类的子类,只需要给构造函数传递一个 JavaScript 对象,对象中包含了一些属性对应了抽象类方法实现的值。如果一个方法是重载的,JavaScript 函数将会提供所有方法变种的实现。下面例子显示如何初始化抽象类 TimerTask 的子类:

var TimerTask = Java.type("java.util.TimerTask");
var task = new TimerTask({ run: function() { print("Hello World!") } });

除了调用构造函数并传递参数,我们还可以在 new 表达式后面直接提供参数。

下面的例子显示该语法的使用方法(类似 Java 匿名内部类的定义),这比上面的例子要简单一些:

var task = new TimerTask {
 run: function() {
  print("Hello World!")
 }
};

如果抽象类包含单个抽象方法(SAM 类型),那么我们就无需传递 JavaScript 对象给构造函数,我们可以传递一个实现了该方法的函数接口。下面的例子显示如何使用 SAM 类型来简化代码:

var task = new TimerTask(function() { print("Hello World!") });

不管你选择哪种语法,如果你需要调用一个包含参数的构造函数,你可以在实现对象和函数中指定参数。

如果你想要调用一个要求 SAM 类型参数的 Java 方法,你可以传递一个 JavaScript 函数给该方法。Nashorn 将根据方法需要来实例化一个子类并使用这个函数去实现唯一的抽象方法。

下面的代码显示如何调用 Timer.schedule() 方法,该方法要求一个 TimerTask 对象作为参数:

var Timer = Java.type("java.util.Timer");
Timer.schedule(function() { print("Hello World!") });

注意:

前面的语法假设所要求的 SAM 类型是一个接口或者包含一个默认构造函数,Nashorn 用它来初始化一个子类。这里是无法使用不包含默认构造函数的类的。

6、扩展具体 Java 类

为了避免混淆,扩展抽象类的语法不能用于扩展具体类。因为一个具体类是可以被实例化的,这样的语法会被解析成试图创建一个新的类实例并传递构造函数所需类的对象(如果预期的对象类型是一个接口)。为了演示这个问题,请看看下面的示例代码:

var t = new java.lang.Thread({ run: function() { print("Thread running!") } });

这行代码被解析为扩展了 Thread 类并实现了 run() 方法,而 Thread 类的实例化是通过传递给其构造函数一个实现了 Runnable 接口的对象。

为了扩展一个具体类,传递其类型对象给 Java.extend() 函数,然后返回其子类的类型对象。紧接着就可以使用这个子类的类型对象来创建实例并提供额外的方法实现。

下面的代码将向你展示如何扩展 Thread 类并实现 run() 方法:

var Thread = Java.type("java.lang.Thread");
var threadExtender = Java.extend(Thread);
var t = new threadExtender() {
 run: function() { print("Thread running!") }};

Java.extend() 函数可以获取多个类型对象的列表。你可以指定不超过一个 Java 的类型对象,也可以指定跟 Java接口一样多的类型对象数量。返回的类型对象扩展了指定的类(或者是 java.lang.Object ,如果没有指定类型对象的话),这个类实现了所有的接口。类的类型对象无需在列表中排在首位。

7、访问超类(父类)的方法

想要访问父类的方法可以使用 Java .super() 函数。

下面的例子中显示如何扩展 java.lang.Exception 类,并访问父类的方法。

Example 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);
}

如果你运行上面代码将会打印如下内容:

jdk.nashorn.javaadapters.java.lang.Exception: MY EXCEPTION MESSAGE

8、绑定实现到类

前面的部分我们描述了如何扩展 Java 类以及使用一个额外的 JavaScript 对象参数来实现接口。实现是绑定的具体某个实例上的,这个实例是通过 new 来创建的,而不是整个类。这样做有一些好处,例如运行时的内存占用,因为 Nashorn 可以为每个实现的类型组合创建一个单一的通用适配器。

下面的例子展示不同的实例可以是同一个 Java 类,而其 JavaScript 实现对象却是不同的:

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 适配器类。带类边界实现的适配器类仍可以使用一个额外的构造参数用来进一步重写特定实例的行为。因此你可以合并这两种方法:你可以在一个基础类中提供部分 JavaScript 实现,然后传递给 Java.extend() 函数,以及在对象中提供实例实现并传递给构造函数。对象定义的函数并传递给构造函数时将覆盖对象的一些函数定义。

下面的代码演示如何通过给构造函数传递一个函数来覆盖类边界对象的函数:

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

9、选择方法重载变体

Java 的方法可以通过使用不同的参数类型进行重载。Java 编译器 (javac) 会在编译时选择正确的方法来执行。在 Nashorn 中对Java 重载方法的解析实在方法被调用的时候执行的。也是根据参数类型来确定正确的方法。但如果实际的参数类型会导致模棱两可的情况下,我们可以显式的指定具体某个重载变体。这会提升程序执行的性能,因为 Nashorn 引擎无需在调用过程中去辨别该调用哪个方法。

重载的变种作为特别的属性暴露出来。我们可以用字符串的形式来引用它们,字符串包含方法名称、参数类型,两者使用圆括号包围起来。

下面的例子显示如何调用  System.out.println() 方法带 Object 参数的变种,我们传递一个 “hello” 字符串给它:

var out = java.lang.System.out;
out["println(Object)"]("hello");

上述的例子中,光使用 Object 类名就足够了,因为它是唯一标识正确的签名。你必须使用完整的类名的情况是两个重载变种函数使用不同的参数类型,但是类型的名称相同(这是可能的,例如不同包中包含相同的类名)。

10、映射数据类型

绝大多数 Java 和 JavaScript 之前的转换如你所期待的运行良好。前面的章节中我们提到过一些简单的 Java 和 JavaScript 之间的数据类型映射。例如可以显式地转换数组类型数据,JavaScript 函数可以在当成参数传递给 Java 方法时自动转换成 SAM 类型。每个 JavaScript 对象实现了 java.util.Map 接口来让 API 可以直接接受映射。当传递数值给 Java API 时,会被转成所期待的目标数值类型,可以是封装类型或者是原始数据类型。如果目标类型不太确定(如 Number),你只能要求它必须是 Number 类型,然后专门针对该类型是封装了 Double、Integer 或者是 Long 等等。内部的优化使得数值可以是任何封装类型。同事你可以传递任意 JavaScript 值给 Java API,不管是封装类型还是原始类型,因为 JavaScript 的 ToNumber 转换算法将会自动处理其值。如果 Java 方法要求一个 String 或者 Boolean 对象参数,JavaScript 将会使用 ToString ToBoolean 转换来获得其值。

注意:

因为对字符串操作的内部性能优化考虑,JavaScript 字符串并不总是对应 java.lang.String 类型,也可能是 java.lang.CharSequence 类型。如果你传递一个 JavaScript 字符串给要求 java.lang.String 参数的 Java 方法,那么这个 JavaScript 字符串就是 java.lang.String 类型,但如果你的方法签名想要更加泛型化(例如接受的参数类型是 java.lang.Object),那么你得到的参数对象就会使一个实现了 CharSequence 类的对象,而不是一个 Java 字符串对象。

总结
以上就是这篇文章的全部内容,希望对大家的学习和工作能有一定的帮助,如果有疑问大家可以留言交流。

相关推荐