在JavaScript中调用Java类和接口的方法
前言
本文中所有的代码使用JavaScript编写,但你也可以用其他兼容JSR223的脚本语言。这些例子可作为脚本文件也可以在交互式Shell中一次运行一个语句的方式来运行。在JavaScript中访问对象的属性和方法的语法与Java语言相同。
本文包含如下几部分:
1、访问Java类
为了在JavaScript中访问原生类型或者引用Java类型,可以调用Java.type()函数,该函数根据传入的完整类名返回对应对象的类型。下面代码显示如何获取不同的对象类型:
varArrayList=Java.type("java.util.ArrayList"); varintType=Java.type("int"); varStringArrayType=Java.type("java.lang.String[]"); varint2DArrayType=Java.type("int[][]");
在JavaScript中使用Java.type()函数返回的类型对象的方法跟在Java的类似。
例如你可以使用如下方法来实例化一个类:
varanArrayList=newJava.type("java.util.ArrayList");
Java类型对象可用来实例化Java对象。下面的代码显示如何使用默认的构造函数实例化一个新对象以及调用包含参数的构造函数:
varArrayList=Java.type("java.util.ArrayList"); vardefaultSizeArrayList=newArrayList; varcustomSizeArrayList=newArrayList(16);
你可以使用Java.type()方法来获取对象类型,可以使用如下方法来访问静态属性以及方法:
varFile=Java.type("java.io.File"); File.createTempFile("nashorn",".tmp");
如果要访问内部静态类,可以传递美元符号$给Java.type()方法。
下面代码显示如何返回java.awt.geom.Arc2D的Float内部类:
varFloat=Java.type("java.awt.geom.Arc2D$Float");
如果你已经有一个外部类类型对象,那么你可以像访问属性一样访问其内部类,如下所示:
varArc2D=Java.type("java.awt.geom.Arc2D") varFloat=Arc2D.Float
由于是非静态内部类,必须传递的是外部类实例作为参数给构造函数。
虽然在JavaScript中使用类型对象跟在Java中类似,但其与java.lang.Class对象还是有些区别的,这个区别就是getClass()方法的返回值。你可以使用class和static属性来获取这个信息。
下面代码显示二者的区别:
varArrayList=Java.type("java.util.ArrayList"); vara=newArrayList; //Allofthefollowingaretrue: print("Typeactsastargetofinstanceof:"+(ainstanceofArrayList)); print("Classdoesn'tactastargetofinstanceof:"+!(ainstanceofa.getClass())); print("Typeisnotthesameasinstance'sgetClass():"+(a.getClass()!==ArrayList)); print("Type's`class`propertyisthesameasinstance'sgetClass():"+(a.getClass()===ArrayList.class)); print("Typeisthesameasthe`static`propertyoftheinstance'sgetClass():"+(a.getClass().static===ArrayList));
在语法和语义上,JavaScript在编译时类表达式和运行时对象都和Java语义类似。不过在Java中Class对象是没有名为static这样的属性,因为编译时的类表达式不作为对象。
2、导入Java包和类
为了根据其简单的名称来访问Java类,我们可以使用importPackage()和importClass()函数来导入Java的包和类。这些函数存在于兼容性脚本文件(mozilla_compat.js)中。
下面例子展示如何使用importPackage()和importClass()函数:
//Loadcompatibilityscript load("nashorn:mozilla_compat.js"); //Importthejava.awtpackage importPackage(java.awt); //Importthejava.awt.Frameclass importClass(java.awt.Frame); //CreateanewFrameobject varframe=newjava.awt.Frame("hello"); //CallthesetVisible()method frame.setVisible(true); //AccessaJavaBeanproperty print(frame.title);
可以通过Packages全局变量来访问Java包,例如Packages.java.util.Vector或者Packages.javax.swing.JFrame。但标准的JavaSE包有更简单的访问方式,如:java对应Packages.java,javax对应Packages.javax,以及org对应Packages.org。
java.lang包默认不需要导入,因为这会和Object、Boolean、Math等其他JavaScript内建的对象在命名上冲突。此外,导入任何Java包和类也可能导致JavaScript全局作用域下的变量名冲突。为了避免冲突,我们定义了一个JavaImporter对象,并通过with语句来限制导入的Java包和类的作用域,如下列代码所示:
//CreateaJavaImporterobjectwithspecifiedpackagesandclassestoimport varGui=newJavaImporter(java.awt,javax.swing); //PasstheJavaImporterobjecttothe"with"statementandaccesstheclasses //fromtheimportedpackagesbytheirsimplenameswithinthestatement'sbody with(Gui){ varawtframe=newFrame("AWTFrame"); varjframe=newJFrame("SwingJFrame"); };
3、使用Java数组
为了创建Java数组对象,首先需要获取Java数组类型对象并进行初始化。JavaScript访问数组元素的语法以及length属性都跟Java一样,如下列代码所示:
varStringArray=Java.type("java.lang.String[]"); vara=newStringArray(5); //Setthevalueofthefirstelement a[0]="Scriptingisgreat!"; //Printthelengthofthearray print(a.length); //Printthevalueofthefirstelement print(a[0]);
给定一个JavaScript数组我们还可以用Java.to()方法将它转成Java数组。我们需要将JavaScript数组作为参数传给该方法,并指定要返回的数组类型,可以是一个字符串,或者是类型对象。我们也可以忽略类型对象参数来返回Object[]数组。转换操作是根据ECMAScript转换规则进行的。下面代码展示如何通过不同的Java.to()的参数将JavaScript数组变成Java数组:
//创建一个JavaScript数组 varanArray=[1,"13",false]; //将数组转换成java的int[]数组 varjavaIntArray=Java.to(anArray,"int[]"); print(javaIntArray[0]);//printsthenumber1 print(javaIntArray[1]);//printsthenumber13 print(javaIntArray[2]);//printsthenumber0 //将JavaScript数组转换成Java的String[]数组 varjavaStringArray=Java.to(anArray,Java.type("java.lang.String[]")); print(javaStringArray[0]);//printsthestring"1" print(javaStringArray[1]);//printsthestring"13" print(javaStringArray[2]);//printsthestring"false" //将JavaScript数组转换成Java的Object[]数组 varjavaObjectArray=Java.to(anArray); print(javaObjectArray[0]);//printsthenumber1 print(javaObjectArray[1]);//printsthestring"13" print(javaObjectArray[2]);//printsthebooleanvalue"false"
你可以使用Java.from()方法来将一个Java数组转成JavaScript数组。
下面代码演示如何将一个包含当前目录下文件列表的数组转成JavaScript数组:
//GettheJavaFiletypeobject varFile=Java.type("java.io.File"); //CreateaJavaarrayofFileobjects varlistCurDir=newFile(".").listFiles(); //ConverttheJavaarraytoaJavaScriptarray varjsList=Java.from(listCurDir); //PrinttheJavaScriptarray print(jsList);
注意:
大多数情况下,你可以在脚本中使用Java对象而无需转换成JavaScript对象。
4、实现Java接口
在JavaScript实现Java接口的语法与在Java总定义匿名类的方法类似。我们只需要实例化接口并用JavaScript函数实现其方法即可。
下面代码演示如何实现Runnable接口:
//CreateanobjectthatimplementstheRunnableinterfacebyimplementing //therun()methodasaJavaScriptfunction varr=newjava.lang.Runnable(){ run:function(){ print("running...\n"); } }; //ThervariablecanbepassedtoJavamethodsthatexpectanobjectimplementing //thejava.lang.Runnableinterface varth=newjava.lang.Thread(r); th.start(); th.join();
如果一个方法希望一个对象,这个对象实现了只有一个方法的接口,你可以传递一个脚本函数给这个方法,而不是传递对象。例如,在上面的例子中Thread()构造函数要求一个实现了Runnable接口的对象作为参数。我们可以利用自动转换的优势传递一个脚本函数给Thread()构造器。
下面的例子展示如何创建一个Thread对象而无需实现Runnable接口:
//DefineaJavaScriptfunction functionfunc(){ print("Iamfunc!"); }; //PasstheJavaScriptfunctioninsteadofanobjectthatimplements //thejava.lang.Runnableinterface varth=newjava.lang.Thread(func); th.start(); th.join();
你可以通过传递相关类型对象给Java.extend()函数来实现多个接口。
5、扩展抽象Java类
你可以实例化一个匿名的抽象类的子类,只需要给构造函数传递一个JavaScript对象,对象中包含了一些属性对应了抽象类方法实现的值。如果一个方法是重载的,JavaScript函数将会提供所有方法变种的实现。下面例子显示如何初始化抽象类TimerTask的子类:
varTimerTask=Java.type("java.util.TimerTask"); vartask=newTimerTask({run:function(){print("HelloWorld!")}});
除了调用构造函数并传递参数,我们还可以在new表达式后面直接提供参数。
下面的例子显示该语法的使用方法(类似Java匿名内部类的定义),这比上面的例子要简单一些:
vartask=newTimerTask{ run:function(){ print("HelloWorld!") } };
如果抽象类包含单个抽象方法(SAM类型),那么我们就无需传递JavaScript对象给构造函数,我们可以传递一个实现了该方法的函数接口。下面的例子显示如何使用SAM类型来简化代码:
vartask=newTimerTask(function(){print("HelloWorld!")});
不管你选择哪种语法,如果你需要调用一个包含参数的构造函数,你可以在实现对象和函数中指定参数。
如果你想要调用一个要求SAM类型参数的Java方法,你可以传递一个JavaScript函数给该方法。Nashorn将根据方法需要来实例化一个子类并使用这个函数去实现唯一的抽象方法。
下面的代码显示如何调用Timer.schedule()方法,该方法要求一个TimerTask对象作为参数:
varTimer=Java.type("java.util.Timer"); Timer.schedule(function(){print("HelloWorld!")});
注意:
前面的语法假设所要求的SAM类型是一个接口或者包含一个默认构造函数,Nashorn用它来初始化一个子类。这里是无法使用不包含默认构造函数的类的。
6、扩展具体Java类
为了避免混淆,扩展抽象类的语法不能用于扩展具体类。因为一个具体类是可以被实例化的,这样的语法会被解析成试图创建一个新的类实例并传递构造函数所需类的对象(如果预期的对象类型是一个接口)。为了演示这个问题,请看看下面的示例代码:
vart=newjava.lang.Thread({run:function(){print("Threadrunning!")}});
这行代码被解析为扩展了Thread类并实现了run()方法,而Thread类的实例化是通过传递给其构造函数一个实现了Runnable接口的对象。
为了扩展一个具体类,传递其类型对象给Java.extend()函数,然后返回其子类的类型对象。紧接着就可以使用这个子类的类型对象来创建实例并提供额外的方法实现。
下面的代码将向你展示如何扩展Thread类并实现run()方法:
varThread=Java.type("java.lang.Thread"); varthreadExtender=Java.extend(Thread); vart=newthreadExtender(){ run:function(){print("Threadrunning!")}};
Java.extend()函数可以获取多个类型对象的列表。你可以指定不超过一个Java的类型对象,也可以指定跟Java接口一样多的类型对象数量。返回的类型对象扩展了指定的类(或者是java.lang.Object,如果没有指定类型对象的话),这个类实现了所有的接口。类的类型对象无需在列表中排在首位。
7、访问超类(父类)的方法
想要访问父类的方法可以使用Java.super()函数。
下面的例子中显示如何扩展java.lang.Exception类,并访问父类的方法。
Example3-1访问父类的方法(super.js) varException=Java.type("java.lang.Exception"); varExceptionAdapter=Java.extend(Exception); varexception=newExceptionAdapter("MyExceptionMessage"){ getMessage:function(){ var_super_=Java.super(exception); return_super_.getMessage().toUpperCase(); } } try{ throwexception; }catch(ex){ print(exception); }
如果你运行上面代码将会打印如下内容:
jdk.nashorn.javaadapters.java.lang.Exception:MYEXCEPTIONMESSAGE
8、绑定实现到类
前面的部分我们描述了如何扩展Java类以及使用一个额外的JavaScript对象参数来实现接口。实现是绑定的具体某个实例上的,这个实例是通过new来创建的,而不是整个类。这样做有一些好处,例如运行时的内存占用,因为Nashorn可以为每个实现的类型组合创建一个单一的通用适配器。
下面的例子展示不同的实例可以是同一个Java类,而其JavaScript实现对象却是不同的:
varRunnable=java.lang.Runnable; varr1=newRunnable(function(){print("I'mrunnable1!")}); varr2=newRunnable(function(){print("I'mrunnable2!")}); r1.run(); r2.run(); print("Wesharethesameclass:"+(r1.class===r2.class));
上述代码将打印如下结果:
I'mrunnable1! I'mrunnable2! Wesharethesameclass:true
如果你想传递类的实例给外部API(如JavaFX框架,传递Application实例给JavaFXAPI),你必须扩展一个Java类或者实现了与该类绑定的接口,而不是它的实例。你可以通过传递一个JavaScript对象绑定实现类并传递给Java.extend()函数的最后一个参数。这个会创建一个跟原有类包含一样构造函数的新类,因为它们不需要额外实现对象参数。
下面的例子展示如何绑定实现到类中,并演示在这种情况下对于不同调用的实现类是不同的:
varRunnableImpl1=Java.extend(java.lang.Runnable,function(){print("I'mrunnable1!")}); varRunnableImpl2=Java.extend(java.lang.Runnable,function(){print("I'mrunnable2!")}); varr1=newRunnableImpl1();varr2=newRunnableImpl2(); r1.run(); r2.run(); print("Wesharethesameclass:"+(r1.class===r2.class));
上面例子执行结果如下:
I'mrunnable1! I'mrunnable2! Wesharethesameclass:false
将实现对象从构造函数调用移到Java.extend()函数调用可以避免在构造函数调用中所需的额外参数。每一个Java.extend()函数的调用都需要一个指定类的实现对象生成一个新的Java适配器类。带类边界实现的适配器类仍可以使用一个额外的构造参数用来进一步重写特定实例的行为。因此你可以合并这两种方法:你可以在一个基础类中提供部分JavaScript实现,然后传递给Java.extend()函数,以及在对象中提供实例实现并传递给构造函数。对象定义的函数并传递给构造函数时将覆盖对象的一些函数定义。
下面的代码演示如何通过给构造函数传递一个函数来覆盖类边界对象的函数:
varRunnableImpl=Java.extend(java.lang.Runnable,function(){print("I'mrunnable1!")}); varr1=newRunnableImpl(); varr2=newRunnableImpl(function(){print("I'mrunnable2!")}); r1.run(); r2.run(); print("Wesharethesameclass:"+(r1.class===r2.class));
上面例子执行后打印结果如下:
I'mrunnable1! I'mrunnable2! Wesharethesameclass:true
9、选择方法重载变体
Java的方法可以通过使用不同的参数类型进行重载。Java编译器(javac)会在编译时选择正确的方法来执行。在Nashorn中对Java重载方法的解析实在方法被调用的时候执行的。也是根据参数类型来确定正确的方法。但如果实际的参数类型会导致模棱两可的情况下,我们可以显式的指定具体某个重载变体。这会提升程序执行的性能,因为Nashorn引擎无需在调用过程中去辨别该调用哪个方法。
重载的变种作为特别的属性暴露出来。我们可以用字符串的形式来引用它们,字符串包含方法名称、参数类型,两者使用圆括号包围起来。
下面的例子显示如何调用 System.out.println()方法带Object参数的变种,我们传递一个“hello”字符串给它:
varout=java.lang.System.out; out["println(Object)"]("hello");
上述的例子中,光使用Object类名就足够了,因为它是唯一标识正确的签名。你必须使用完整的类名的情况是两个重载变种函数使用不同的参数类型,但是类型的名称相同(这是可能的,例如不同包中包含相同的类名)。
10、映射数据类型
绝大多数Java和JavaScript之前的转换如你所期待的运行良好。前面的章节中我们提到过一些简单的Java和JavaScript之间的数据类型映射。例如可以显式地转换数组类型数据,JavaScript函数可以在当成参数传递给Java方法时自动转换成SAM类型。每个JavaScript对象实现了java.util.Map接口来让API可以直接接受映射。当传递数值给JavaAPI时,会被转成所期待的目标数值类型,可以是封装类型或者是原始数据类型。如果目标类型不太确定(如Number),你只能要求它必须是Number类型,然后专门针对该类型是封装了Double、Integer或者是Long等等。内部的优化使得数值可以是任何封装类型。同事你可以传递任意JavaScript值给JavaAPI,不管是封装类型还是原始类型,因为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字符串对象。
总结
以上就是这篇文章的全部内容,希望对大家的学习和工作能有一定的帮助,如果有疑问大家可以留言交流。