Java-脚本编程教程-集成-Groovy-和-JS-二-

97 阅读52分钟

Java 脚本编程教程:集成 Groovy 和 JS(二)

协议:CC BY-NC-SA 4.0

五、过程和编译的脚本

在本章中,您将学习:

  • 如何从 Java 程序中调用脚本编写的过程
  • 如何在 Nashorn 脚本中实现 Java 接口
  • 如何编译 Nashorn 脚本并重复执行

在脚本中调用过程

在前面的章节中,您已经看到了如何使用ScriptEngineeval()方法调用脚本。如果ScriptEngine支持过程调用,也可以直接从 Java 程序中调用用脚本语言编写的过程。

脚本语言可以允许创建过程、函数和方法。Java 脚本 API 允许您从 Java 应用中调用这样的过程、函数和方法。在本节中,我将使用术语“过程”来表示过程、函数和方法。当讨论的上下文需要时,我将使用特定的术语。

并非所有脚本引擎都需要支持过程调用。Nashorn JavaScript 引擎支持过程调用。如果有脚本引擎支持,那么脚本引擎类的实现必须实现Invocable接口。Invocable接口包含以下四种方法:

  • <T> T getInterface(Class<T> cls)
  • <T> T getInterface(Object obj, Class<T> cls)
  • Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException
  • Object invokeMethod(Object obj, String name, Object... args) throws ScriptException, NoSuchMethodException

两个版本的getInterface()方法让你得到一个用脚本语言实现的 Java 接口的实例。我将在下一节详细讨论这些函数。invokeFunction()方法允许您调用用脚本语言编写的顶级函数。invokeMethod()方法让你调用用脚本语言编写的对象的方法。

在调用过程之前,检查脚本引擎是否实现了Invocable接口是开发人员的责任。调用过程包括四个步骤:

  • 检查脚本引擎是否支持过程调用
  • 将引擎引用转换为Invocable类型
  • 评估包含该过程源代码的脚本,以便引擎编译并缓存该脚本
  • 使用Invocable接口的invokeFunction()方法调用过程和函数;使用invokeMethod()方法来调用在脚本语言中创建的对象的方法

以下代码片段检查脚本引擎实现类是否实现了Invocable接口:

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Make sure the script engine implements the Invocable interface

if (engine instanceof Invocable) {

System.out.println("Invoking procedures is supported.");

}

else  {

System.out.println("Invoking procedures is not supported.");

}

第二步是将引擎引用转换为Invocable接口类型:

if (engine instanceof Invocable) {

Invocable inv = (Invocable)engine;

// More code goes here

}

第三步是评估脚本,因此脚本引擎编译并存储过程的编译形式,供以后调用。以下代码片段执行此步骤:

// Declare a function named add that adds two numbers

String script = "function add(n1, n2) { return n1 + n2; }";

// Evaluate the function. Call to eval() does not invoke the function. // It just compiles it.

engine.eval(script);

最后一步是调用该过程,如下所示:

// Invoke the add function with 30 and 40 as the function's arguments.

// It is as if you called add(30, 40) in the script.

Object result = inv.invokeFunction("add", 30, 40);

invokeFunction()的第一个参数是过程的名称。第二个参数是 varargs,用于指定过程的参数。invokeFunction()方法返回过程返回的值。

清单 5-1 显示了如何调用一个函数。它调用用 Nashorn JavaScript 编写的函数。它在名为factorial.jsavg.js的文件中加载脚本。这些文件包含名为factorial()avg()的函数的 Nashorn 代码。稍后,程序使用Invocable接口的invokeFunction()调用这些函数。

清单 5-1。调用用 Nashorn JavaScript 编写的函数

// InvokeFunction.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class InvokeFunction {

public static void main(String[] args) {

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Make sure the script engine implements the Invocable // interface

if (!(engine instanceof Invocable)) {

System.out.println(   "Invoking procedures is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable) engine;

try {

String scriptPath1 = "factorial.js";

String scriptPath2 = "avg.js";

// Evaluate the scripts first, so the                         // factorial() and avg() functions are

// compiled and are available to be invoked

engine.eval("load('" + scriptPath1 + "');");

engine.eval("load('" + scriptPath2 + "');");

// Invoke the add function twice

Object result1 = inv.invokeFunction("factorial", 10);

System.out.println("factorial(10) = " + result1);

Object result2 = inv.invokeFunction("avg", 10, 20, 30);

System.out.println("avg(10, 20, 30) = " + result2);

}

catch (ScriptException | NoSuchMethodException e) {

e.printStackTrace();

}

}

}

factorial(10) = 3628800.0

avg(10, 20, 30) = 20.0

面向对象或基于对象的脚本语言可以让您定义对象及其方法。您可以使用Invocable接口的invokeMethod(Object obj, String name, Object... args)方法调用这些对象的方法。第一个参数是对象的引用,第二个参数是要在对象上调用的方法的名称,第三个参数是 varargs 参数,用于将参数传递给被调用的方法。

清单 5-2 包含一个 Nashorn 脚本,它创建了一个名为calculator的对象,并添加了四个方法来对两个数进行加、减、乘、除。注意,我使用了 Nashorn 语法扩展来定义函数表达式,其中没有指定大括号和return语句。

清单 5-2。在 Nashorn 脚本中创建的计算器对象

// calculator.js

// Create an object

var calculator = new Object();

// Add four methods to the prototype to the calculator object

calculator.add = function (n1, n2) n1 + n2;

calculator.subtract = function (n1, n2) n1 - n2;

calculator.multiply = function (n1, n2) n1 * n2;

calculator.divide = function (n1, n2) n1 / n2;

清单 5-3 展示了在 Nashorn 中创建的calculator对象上的方法调用。注意,该对象是在 Nashorn 脚本中创建的。要从 Java 调用对象的方法,需要通过脚本引擎获取对象的引用。程序评估calculator.js文件中创建calculator对象的脚本,并将其引用存储在名为calculator的变量中。engine.get("calculator")方法返回对 Java 代码的calculator对象的引用。

清单 5-3。在 Nashorn 中创建的对象上调用方法

// InvokeMethod.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class InvokeMethod {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Make sure the script engine implements the Invocable                 // interface

if (!(engine instanceof Invocable)) {

System.out.println(                           "Invoking methods is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable) engine;

try {

// Declare a global object with an add() method

String scriptPath = "calculator.js";

// Evaluate the script first

engine.eval("load('" + scriptPath + "')");

// Get the calculator object reference that was // created in the script

Object calculator = engine.get("calculator");

// Invoke the methods on the calculator object

int x = 30;

int y = 40;

Object addResult = inv.invokeMethod(calculator, "add", x, y);

Object subResult = inv.invokeMethod(calculator, "subtract", x, y);

Object mulResult = inv.invokeMethod(calculator, "multiply", x, y);

Object divResult = inv.invokeMethod(calculator, "divide", x, y);

System.out.printf("calculator.add(%d, %d) = %s%n", x, y, addResult);

System.out.printf("calculator.subtract(%d, %d) = %s%n", x, y, subResult);

System.out.printf("calculator.multiply(%d, %d) = %s%n", x, y, mulResult);

System.out.printf("calculator.divide(%d, %d) = %s%n", x, y, divResult);

}

catch (ScriptException | NoSuchMethodException e) {

e.printStackTrace();

}

}

}

calculator.add(30, 40) = 70

calculator.subtract(30, 40) = -10.0

calculator.multiply(30, 40) = 1200.0

calculator.divide(30, 40) = 0.75

Tip

使用Invocable界面重复执行程序、函数和方法。具有过程、函数和方法的脚本评估将中间代码存储在引擎中,从而在重复执行时获得性能增益。

在脚本中实现 Java 接口

Java 脚本 API 允许您用脚本语言实现 Java 接口。用脚本语言实现 Java 接口的优点是,您可以用 Java 代码使用接口的实例,就好像接口是用 Java 实现的一样。您可以将接口的实例作为参数传递给 Java 方法。Java 接口的方法可以使用对象的顶级过程或方法在脚本中实现。

Invocable接口的getInterface()方法用于获取在脚本中实现的 Java 接口的实例。该方法有两个版本:

  • <T> T getInterface(Class<T> cls)
  • <T> T getInterface(Object obj, Class<T> cls)

第一个版本用于获取 Java 接口的实例,该接口的方法在脚本中作为顶级过程实现。接口类型作为参数传递给该方法。假设你有一个Calculator接口,如清单 5-4 所示,它包含四个方法,分别叫做add()subtract()multiply()divide()

清单 5-4。计算器界面

// Calculator.java

package com.jdojo.script;

public interface Calculator {

double add (double n1, double n2);

double subtract (double n1, double n2);

double multiply (double n1, double n2);

double divide (double n1, double n2);

}

考虑用 Nashorn 编写的顶级函数,如清单 5-5 所示。该脚本包含四个函数,对应于Calculator接口中的函数。

清单 5-5。calculatorasfunctions.js 文件的内容

// calculatorasfunctions.js

function add(n1, n2) {

n1 + n2;

}

function subtract(n1, n2) {

n1 - n2;

}

function multiply(n1, n2) {

n1 * n2;

}

function divide(n1, n2) {

n1 / n2;

}

这两个函数为Calculator接口的四个方法提供了实现。JavaScript 引擎编译完函数后,您可以获得一个Calculator接口的实例,如下所示:

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable)engine;

// Get the reference of the Calculator interface

Calculator calc = inv.getInterface(Calculator.class);

if (calc == null) {

System.err.println("Calculator interface implementation not found.");

}

else {

// Use calc to call the methods of the Calculator interface

}

您可以添加两个数字,如下所示:

int sum = calc.add(15, 10);

清单 5-6 展示了如何在 Nashorn 中使用顶级过程实现一个 Java 接口。请查阅脚本语言(除 Nashorn 之外)的文档,以了解它如何支持此功能。

清单 5-6。使用脚本中的顶级函数实现 Java 接口

// UsingInterfaces.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class UsingInterfaces {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Make sure the script engine implements Invocable // interface

if (!(engine instanceof Invocable)) {

System.out.println("Interface implementation in script" +

"is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable) engine;

// Create the script for add() and subtract() functions

String scriptPath  = "calculatorasfunctions.js";

try {

// Compile the script that will be stored in the // engine

engine.eval("load('" + scriptPath + "')");

// Get the interface implementation

Calculator calc =          inv.getInterface(Calculator.class);

if (calc == null) {

System.err.println("Calculator interface " +

"implementation not found.");

return;

}

double x = 15.0;

double y = 10.0;

double addResult = calc.add(x, y);

double subResult = calc.subtract(x, y);

double mulResult = calc.multiply(x, y);

double divResult = calc.divide(x, y);

System.out.printf(                           "calc.add(%.2f, %.2f) = %.2f%n", x, y, addResult);

System.out.printf(                           "calc.subtract(%.2f, %.2f) = %.2f%n", x, y, subResult);

System.out.printf(                           "calc.multiply(%.2f, %.2f) = %.2f%n", x, y, mulResult);

System.out.printf(                           "calc.divide(%.2f, %.2f) = %.2f%n", x, y, divResult);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

calc.add(15.00, 10.00) = 25.00

calcr.subtract(15.00, 10.00) = 5.00

calcr.multiply(15.00, 10.00) = 150.00

calc.divide(15.00, 10.00) = 1.50

Nashorn 引擎是如何找到Calculator接口的实现的?当您调用InvocablegetInterface(Class<T> cls)时,引擎会在指定的类中寻找具有匹配名称的编译函数作为抽象方法。在我们的例子中,引擎在引擎中寻找名为addsubtractmultiplydivide的编译函数。注意,需要调用引擎的eval()方法来编译calculatorasfunctions.js文件中的函数。Nashorn 引擎与引擎中 Java 接口方法和脚本函数中的形参数量不匹配。

第二个版本的getInterface()方法用于获得一个 Java 接口的实例,该接口的方法被实现为一个对象的实例方法。它的第一个参数是用脚本语言创建的对象的引用。对象的实例方法实现作为第二个参数传入的接口类型。清单 5-2 中的代码创建了一个名为calculator的对象,它的实例方法实现了Calculator接口。您将把calculator对象的方法作为 Java 中Calculator接口的实现。

当脚本对象的实例方法实现 Java 接口的方法时,您需要执行一个额外的步骤。在获取接口的实例之前,需要获取脚本对象的引用,如下所示:

// Load the calculator object in the engine

engine.load('calculator.js');

// Get the reference of the global script object calculator

Object calc = engine.get("calculator");

// Get the implementation of the Calculator interface

Calculator calculator = inv.getInterface(calc, Calculator.class);

清单 5-7 展示了如何使用 Nashorn 将 Java 接口的方法实现为对象的实例方法。

清单 5-7。将 Java 接口的方法实现为脚本中对象的实例方法

// ScriptObjectImplInterface.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class ScriptObjectImplInterface {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Make sure the engine implements the Invocable interface

if (!(engine instanceof Invocable)) {

System.out.println("Interface implementation in " +

"script is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable)engine;

String scriptPath  = "calculator.js";

try {

// Compile and store the script in the engine

engine.eval("load('" + scriptPath + "')");

// Get the reference of the global script object calc

Object scriptCalc = engine.get("calculator");

// Get the implementation of the Calculator interface

Calculator calc = inv.getInterface(scriptCalc, Calculator.class);

if (calc == null) {

System.err.println("Calculator interface " +

"implementation not found.");

return;

}

double x = 15.0;

double y = 10.0;

double addResult = calc.add(x, y);

double subResult = calc.subtract(x, y);

double mulResult = calc.multiply(x, y);

double divResult = calc.divide(x, y);

System.out.printf("calc.add(%.2f, %.2f) = %.2f%n", x, y, addResult);

System.out.printf("calc.subtract(%.2f, %.2f) = %.2f%n", x, y, subResult);

System.out.printf("calc.multiply(%.2f, %.2f) = %.2f%n", x, y, mulResult);

System.out.printf("calc.divide(%.2f, %.2f) = %.2f%n", x, y, divResult);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

calc.add(15.00, 10.00) = 25.00

calcr.subtract(15.00, 10.00) = 5.00

calcr.multiply(15.00, 10.00) = 150.00

calc.divide(15.00, 10.00) = 1.50

使用编译的脚本

脚本引擎可以允许编译脚本并重复执行它。执行编译后的脚本可以提高应用的性能。脚本引擎可以以 Java 类、Java 类文件的形式或特定于语言的形式编译和存储脚本。

并非所有脚本引擎都需要支持脚本编译。支持脚本编译的脚本引擎必须实现Compilable接口。Nashorn 引擎支持脚本编译。以下代码片段检查脚本引擎是否实现了Compilable接口:

// Get the script engine reference

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("YOUR_ENGINE_NAME");

if (engine instanceof Compilable) {

System.out.println("Script compilation is supported.");

}

else {

System.out.println("Script compilation is not supported.");

}

一旦知道脚本引擎实现了Compilable接口,就可以将其引用转换为Compilable类型,如下所示:

// Cast the engine reference to the Compilable type

Compilable comp = (Compilable)engine;

Compilable接口包含两个方法:

  • CompiledScript compile(String script) throws ScriptException
  • CompiledScript compile(Reader script) throws ScriptException

该方法的两个版本仅在脚本源的类型上有所不同。第一个版本接受脚本作为String,第二个版本接受脚本作为Reader

compile()方法返回一个CompiledScript类的对象。CompiledScript是一个抽象类。脚本引擎的提供者提供了这个类的具体实现。一个CompiledScript与创建它的ScriptEngine相关联。CompiledScript类的getEngine()方法返回与其关联的ScriptEngine的引用。

要执行编译后的脚本,您需要调用CompiledScript类的以下eval()方法之一:

  • Object eval() throws ScriptException
  • Object eval(Bindings bindings) throws ScriptException
  • Object eval(ScriptContext context) throws ScriptException

没有任何参数的eval()方法使用脚本引擎的默认脚本上下文来执行编译后的脚本。当你向另外两个版本传递一个Bindings或一个ScriptContext时,它们的工作方式与ScriptEngine接口的eval()方法相同。

Tip

当您使用CompiledScript类的eval()方法评估脚本时,在已编译脚本的执行过程中对引擎状态所做的更改可能在引擎随后执行脚本时可见。

清单 5-8 显示了如何编译并执行一个脚本。它使用不同的参数将相同的编译脚本执行两次。

清单 5-8。使用编译的脚本

// CompilableTest .java

package com.jdojo.script;

import javax.script.Bindings;

import javax.script.Compilable;

import javax.script.CompiledScript;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class CompilableTest {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

if (!(engine instanceof Compilable)) {

System.out.println("Script compilation not supported.");

return;

}

// Cast the engine reference to the Compilable type

Compilable comp = (Compilable)engine;

try {

// Compile a script

String script = "print(n1 + n2)";

CompiledScript cScript = comp.compile(script);

// Store n1 and n2 script variables in a Bindings

Bindings scriptParams = engine.createBindings();

scriptParams.put("n1", 2);

scriptParams.put("n2", 3);

cScript.eval(scriptParams);

// Execute the script again with different values // for n1 and n2

scriptParams.put("n1", 9);

scriptParams.put("n2", 7);

cScript.eval(scriptParams);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

5

16

摘要

Java 脚本 API 支持直接从 Java 调用用脚本语言编写的过程、函数和方法。这可以通过Invocable界面实现。如果脚本引擎支持过程调用,它会实现Invocable接口。Nashorn 引擎支持过程调用。被调用的过程可以被实现为对象的顶层函数或方法。Invocable接口的invokeFunction()方法用于调用脚本中的顶层函数。Invocable接口的invokeMethod()方法用于调用一个对象的方法。在调用顶级函数和对象之前,必须由引擎对其方法进行评估。

Java Script API 还允许您用脚本语言实现 Java 接口。Java 接口的方法可以被实现为对象的顶级函数或方法。Invocable接口的getInterface()方法用于获取 Java 接口的实现。

Java 脚本 API 还允许您编译一次脚本,将其存储在脚本引擎中,并多次执行脚本。通过Compilable接口支持。支持脚本编译的脚本引擎需要实现Compilable接口。你需要调用Compilable接口的compile()方法来编译脚本。compile()方法返回CompiledScript的实例,调用其eval()方法来执行脚本。

注意,通过脚本引擎实现InvocableCompilable接口是可选的。在调用过程和编译脚本之前,您需要检查脚本引擎是否是这些接口的实例,将引擎转换为这些类型,然后执行这些接口的方法。

六、在脚本语言中使用 Java

在本章中,您将学习:

  • 如何将 Java 类导入脚本
  • 如何创建 Java 对象并在脚本中使用它们
  • 如何调用 Java 对象的重载方法
  • 如何创建 Java 数组
  • 如何在脚本中扩展 Java 类和实现 Java 接口
  • 如何从脚本中调用对象的超类方法

脚本语言允许在脚本中使用 Java 类库。每种脚本语言都有自己的使用 Java 类的语法。讨论所有脚本语言的语法是不可能的,也超出了本书的范围。在这一章中,我将讨论在 Nashorn 中使用 Java 构造的语法。

导入 Java 类型

有四种方法可以将 Java 类型导入 Nashorn 脚本:

  • 使用Packages全局对象
  • 使用Java全局对象的type()方法
  • 使用importPackage()importClass()功能
  • with子句中使用JavaImporter

在导入 Java 类型的四种类型中,使用全局 Java 对象的type()方法的第二种类型是首选的。下面几节将详细描述在脚本中导入 Java 类型的四种方式。

使用包全局对象

Nashorn 将a ll Java 包定义为名为Packages的全局变量的属性。例如,java.langjavax.swing包可以分别称为Packages.java.langPackages.javax.swing。以下代码片段使用了 Nashorn 中的java.util.Listjavax.swing.JFrame:

// Create a List

var list1 = new Packages.java.util.ArrayList();

// Create a JFrame

var frame1 = new Packages.javax.swing.JFrame("Test");

Nashorn 将javajavaxorgcomedunet声明为全局变量,分别是Packages.javaPackages.javaxPackages.orgPackages.comPackages.eduPackages.net的别名。本书示例中的类名以前缀com开头,例如com.jdojo.script.Test。要在 JavaScript 代码中使用这个类名,可以使用Packages.com.jdojo.script.Testcom.jdojo.script.Test。但是,如果一个类名不是以这些预定义的前缀之一开头,您必须使用Packages全局变量来访问它;例如,如果您的类名是p1.Test,您需要在 JavaScript 代码中使用Packages.p1.Test来访问它。以下代码片段使用Packages.javaPackages.javaxjavajavax别名:

// Create a List

var list = new java.util.ArrayList();

// Create a JFrame

var frame = new javax.swing.JFrame("Test");

使用 Java 全局对象

Java 7 中的 Rhino JavaScript 也支持将包作为Packages对象的属性来访问。使用Packages对象速度较慢并且容易出错。Nashorn 定义了一个名为Java的新的全局对象,它包含许多有用的函数来处理 Java 包和类。如果您使用的是 Java 8 或更高版本,您应该更喜欢使用Java对象。对象的type()函数将一个 Java 类型导入到脚本中。您需要传递要导入的 Java 类型的完全限定名。在 Nashorn 中,以下代码片段导入了java.util.ArrayList类并创建了它的对象:

// Import java.util.ArrayList type and call it ArrayList

var ArrayList = Java.type("java.util.ArrayList");

// Create an object of the ArrayList type

var list = new ArrayList();

您也可以将两个语句合并为一个。确保将对Java.type()方法的调用添加到一对括号中;否则,该语句将生成一个错误,认为 Java.type 是一个构造函数,而您正试图使用构造函数创建一个 Nashorn 对象:

// Create an object of the java.util.ArrayList type

var list = new``(``Java.type("java.util.ArrayList")``)

在代码中,您将从Java.type()函数返回的导入类型称为ArrayList,这也是导入的类的名称。这样做是为了让下一条语句看起来像是用 Java 编写的。第二条语句的读者会知道你正在创建一个ArrayList类的对象。但是,您可以为导入的类型指定任何想要的名称。下面的代码片段导入java.util.ArrayList并将其命名为MyList:

// Import java.util.ArrayList type and call it MyList

var MyList = Java.type("java.util.ArrayList");

// Create an object of the MyList type

var list2 = new MyList();

使用 importPackage()和 importClass()函数

Rhino JavaScript 允许在脚本中使用 Java 类型的简单名称。Rhino JavaScript 有两个名为importPackage()importClass()的内置函数,分别用于从一个包中导入所有类和从一个包中导入一个类。出于兼容性的考虑,Nashorn 保留了这些功能。要在 Nashorn 中使用这些功能,您需要使用load()功能从mozilla_compat.js文件中加载兼容模块。以下代码片段改写了上一节“这些函数”中的上述逻辑:

// Load the compatibility module. It is needed in Nashorn, not in Rhino.

load("nashorn:mozilla_compat.js");

// Import ArrayList class from the java.util package

importClass(java.util.ArrayList);

// Import all classes from the javax.swing package

importPackage(javax.swing);

// Use simple names of classes

var list = new ArrayList();

var frame = new JFrame("Test");

JavaScript 不会自动从java.lang包中导入所有的类,因为同名的 JavaScript 类,例如StringObjectNumbe r 等等,会与java.lang包中的类名冲突。要使用来自java.lang包的类,您可以导入它或者使用PackagesJava全局对象来使用它的完全限定名。您不能从java.lang包中导入所有的类。下面的代码片段生成了一个错误,因为 JavaScript 中已经定义了String类名:

// Load the compatibility module. It is needed in Nashorn, not in Rhino.

load("nashorn:mozilla_compat.js");

// Will cause a conflict with String object in Nashorn

importClass(java.lang.String);

如果你想使用java.lang.String类,你需要使用它的完全限定名。以下代码片段使用内置的 JavaScript String类和java.lang.String类:

var javaStr = new java.lang.String("Hello"); // Java String class

var jsStr = new String("Hello");             // JavaScript String class

如果java.lang包中的类名与 JavaScript 顶级类名不冲突,可以使用importClass()函数导入 Java 类。例如,您可以使用下面的代码片段来使用java.lang.System类:

// Load the compatibility module. It is needed in Nashorn, not in Rhino.

load("nashorn:mozilla_compat.js");

importClass(java.lang.System);

var jsStr = new String("Hello");

System.out.println(jsStr);

在这段代码中,jsStr是一个 JavaScript String,它被传递给接受java.lang.String类型的System.out.println() Java 方法。在这种情况下,JavaScript 自动处理从 JavaScript 类型到 Java 类型的转换。

使用 JavaImporter 对象

在 JavaScript 中,您可以通过在with语句中使用JavaImporter对象来使用简单的类名。请参考with声明中的Chapter 4了解更多详情。JavaImporter是一个 Nashorn 函数对象,可用作函数或构造函数。它接受 Java 包和类的列表。您可以创建一个JavaImporter对象,如图所示:

// Import all classes from the java.lang package

var langPkg = new JavaImporter(Packages.java.lang);

// Import all classes from the java.lang and java.util packages and the

// JFrame class from the javax.swing package

var pkg2 = JavaImporter(java.lang, java.util, javax.swing.JFrame);

注意第一条语句中使用了new操作符。第二条语句没有使用new操作符;它使用 JavaImporter 作为函数。这两种说法做了同样的事情。

以下代码片段创建了一个JavaImporter对象,并在with语句中使用它:

// Create a Java importer for java.lang and java.util packages

var javaLangAndUtilPkg = JavaImporter(java.lang, java.util);

// Use the imported types in the with clause

with (javaLangAndUtilPkg) {

var list = new ArrayList();

list.add("one");

list.add("two");

System.out.println("Hello");

System.out.println("List is " + list);

}

Hello

List is [one, two]

创建和使用 Java 对象

使用带有构造函数的new操作符在脚本中创建新的 Java 对象。以下代码片段在 Nashorn 中创建了一个String对象:

// Create a Java String object

var JavaString = Java.type("java.lang.String");

var greeting = new JavaString("Hello");

在大多数脚本语言中,访问 Java 对象的方法和属性是相似的。一些脚本语言允许您使用属性名调用对象的 getter 和 setter 方法。Nashorn 中的以下代码创建了一个java.util.Date对象,并使用属性名和方法名来访问该对象的方法。您可能会得到不同的输出,因为代码在当前日期运行:

var LocalDate = Java.type("java.time.LocalDate");

var dt = LocalDate.now();

var year = dt.year;             // Use as a property

var month = dt.month;           // Use as a property

var date = dt.getDayOfMonth();  // Use as a method

print("Date:" + dt);

print("Year:" + year + ", Month:" + month + ", Day:" + date);

Date:2014-10-12

Year:2014, Month:OCTOBER, Day:12

在 JavaScript 中,您可以像使用属性一样使用 Java 对象的方法。当你在读取名为xxx的属性时,JavaScript 会自动调用getXxx()方法。当你设置名为xxx的属性时,会调用setXxx()方法。JavaBeans 方法约定用于查找相应的方法。例如,如果你读取一个LocalDate对象的leapYear属性,那么该对象的isLeapYear()方法将被调用,因为该属性属于boolean类型。

使用 JavaScript 时,理解不同类型的String对象很重要。一个String对象可能是一个 JavaScript String对象或者一个 Java java.lang.String对象。JavaScript 为其String对象定义了一个length属性,而 Java 为其java.lang.String类定义了一个length()方法。以下代码片段显示了创建和访问 JavaScript String和 Java java.lang.String对象的长度的不同:

// JavaScript String

var jsStr = new String("Hello JavaScript String");

print("JavaScript String: " + jsStr);

print("JavaScript String Length: " + jsStr.length);

// Java String

var javaStr = new java.lang.String("Hello Java String");

print("Java String: " + javaStr);

print("Java String Length: " + javaStr.length());

JavaScript String: Hello JavaScript String

JavaScript String Length: 23

Java String: Hello Java String

Java String Length: 17

使用重载的 Java 方法

Java 在编译时解析重载方法的方法调用。也就是说,Java 编译器确定代码运行时将调用的方法的签名。考虑清单 6-1 所示的PrintTest类的代码。您可能会在第二行得到不同的输出。

清单 6-1。在 Java 中使用重载方法

// PrintTest.java

package com.jdojo.script;

public class PrintTest {

public void print(String str) {

System.out.println("print(String): " + str);

}

public void print(Object obj) {

System.out.println("print(Object): " + obj);

}

public void print(Double num) {

System.out.println("print(Double): " + num);

}

public static void main(String[] args) {

PrintTest pt = new PrintTest();

Object[] list = new Object[]{"Hello", new Object(), 10.5};

for(Object arg : list) {

pt.print(arg);

}

}

}

print(Object): Hello

print(Object): java.lang.Object@affc70

print(Object): 10.5

当运行PrintTest类时,对print()方法的所有三个调用都调用PrintTest类的同一个版本print(Object)。当代码被编译时,Java 编译器将调用pt.print(arg)视为对带有Object类型参数的print()方法的调用(这是arg的类型),因此将该调用绑定到print(Object)方法。

在脚本语言中,变量的类型在运行时是已知的,而不是在编译时。脚本语言的解释器根据方法调用中参数的运行时类型适当地解析重载的方法调用。以下 JavaScript 代码的输出显示了对PrintTest类的print()方法的调用在运行时根据参数的类型进行解析。您可能会在第二行得到稍微不同的输出:

// JavaScript Code

// Create an object of the Java class called PrintTest

var PrintTest = Java.type("com.jdojo.script.PrintTest");

var pt = new PrintTest();

// Create a JavaScript array with three elements

var list = ["Hello", new Object(), 10.5];

// Call the overloaded method print() of the PrintTest class

// passing each object in an array at time

for each(var element in list) {

pt.print(element);

}

print(String): Hello

print(Object): jdk.nashorn.internal.scripts.JO@405818

print(Double): 10.5

JavaScript 允许您显式选择重载方法的特定版本。您可以传递要用对象引用调用的重载方法的签名。以下代码片段选择了print(Object)版本:

// JavaScript Code

var PrintTest = Java.type("com.jdojo.script.PrintTest");

var pt = new PrintTest();

pt"print(java.lang.Object)"; // Calls print(Object)

pt"print(java.lang.Double)"; // Calls print(Double)

print(Object): 10.5

print(Double): 10. 5

使用 Java 数组

在 Rhino 和 Nashorn 中,用 JavaScript 创建 Java 数组的方式是不同的。在 Rhino 中,您需要使用java.lang.reflect.Array类的newInstance()静态方法创建一个 Java 数组。Nashorn 也支持这种语法。下面的代码片段展示了如何使用 Rhino 语法创建和访问 Java 数组:

// Create a java.lang.String array of 2 elements, populate it, and print

// the elements. In Rhino, you were able to use java.lang.String as

// the first argument, but in Nashorn, you need to use

// java.lang.String.class instead.

var strArray = java.lang.reflect.Array.newInstance(java.lang.String.class, 2);

strArray[0] = "Hello";

strArray[1] = "Array";

for(var i = 0; i < strArray.length; i++) {

print(strArray[i]);

}

Hello

Array

要创建原始类型数组,如intdouble等,您需要将它们的TYPE常量用于它们对应的包装类,如下所示:

// Create an int array of 2 elements, populate it, and print the elements

var intArray = java.lang.reflect.Array.newInstance(java.lang.Integer.TYPE, 2);

intArray[0] = 100;

intArray[1] = 200;

for(var i = 0; i < intArray.length; i++) {

print(intArray[i]);

}

100

200

Nashorn 支持创建 Java 数组的新语法。首先,使用Java.type()方法创建适当的 Java 数组类型,然后使用熟悉的new操作符创建数组。以下代码片段显示了如何在 Nashorn 中创建两个元素的String[]:

// Get the java.lang.String[] type

var StringArray = Java.type("java.lang.String[]");

// Create a String[] array of 2 elements

var strArray = new StringArray(2);

strArray[0] = "Hello";

strArray[1] = "Array";

for(var i = 0; i < strArray.length; i++) {

print(strArray[i]);

}

Hello

Array

Nashorn 支持以同样的方式创建原始类型的数组。以下代码片段在 Nashorn 中创建了两个元素的int[]:

// Get the int[] type

var IntArray = Java.type("int[]");

// Create a int[] array of 2 elements

var intArray = new IntArray(2);

intArray[0] = 100;

intArray[1] = 200;

for(var i = 0; i < intArray.length; i++) {

print(intArray[i]);

}

100

200

我将在第七章中详细讨论如何使用 Java 和 JavaScript 数组。

扩展 Java 类实现接口

JavaScript 允许您在 JavaScript 中扩展 Java 类和实现 Java 接口。以下部分描述了实现这一点的不同方法。

使用脚本对象

您需要创建一个包含接口方法实现的脚本对象,并使用new操作符将其传递给 Java 接口的构造函数。在 Java 中,接口没有构造函数,也不能和new操作符一起使用,除非创建匿名类。然而,JavaScript 让你做到了这一点。

在第五章中,我们已经用四个抽象方法创建了Calculator接口。清单 6-2 再次显示了该接口的代码,供您参考。

清单 6-2。Java 中的计算器界面

// Calculator.java

package com.jdojo.script;

public interface Calculator {

double add (double n1, double n2);

double subtract (double n1, double n2);

double multiply (double n1, double n2);

double divide (double n1, double n2);

}

在第五章中,我们创建了一个calculator JavaScript 对象,其脚本如清单 6-3 所示。

清单 6-3。Java 中的计算器界面

// calculator.js

// Create an object

var calculator = new Object();

// Add four methods to the prototype to the calculator object

calculator.add = function (n1, n2) n1 + n2;

calculator.subtract = function (n1, n2) n1 - n2;

calculator.multiply = function (n1, n2) n1 * n2;

calculator.divide = function (n1, n2) n1 / n2;

注意 JavaScript 中的calculator对象包含了 Java Calculator接口的所有抽象方法的实现。下面的语句创建了一个Calculator接口的实现:

// Load the calculator object

load("calculator.js");

// Get the Java interface type

var Calculator = Java.type("com.jdojo.script.Calculator");

// Create an instance of the com.jdojo.script.Calculator interface

// passing its constructor a calculator JavaScript object

var calc = new Calculator(calculator);

现在您可以开始使用calc对象,就好像它是Calculator接口的实现一样,如下所示:

// Use the instance of teh Calculator interface

var x = 15.0, y = 10.0;

var addResult = calc.add(x, y);

var subResult = calc.subtract(x, y);

var mulResult = calc.multiply(x, y);

var divResult = calc.divide(x, y);

printf("calc.add(%.2f, %.2f) = %.2f", x, y, addResult);

printf("calc.subtract(%.2f, %.2f) = %.2f", x, y, subResult);

printf("calc.multiply(%.2f, %.2f) = %.2f", x, y, mulResult);

printf("calc.divide(%.2f, %.2f) = %.2f", x, y, divResult);

calc.add(15.00, 10.00) = 25.00

calc.subtract(15.00, 10.00) = 5.00

calc.multiply(15.00, 10.00) = 150.00

calc.divide(15.00, 10.00) = 1.50

使用匿名的类语法

该方法使用的语法与在 Java 中创建匿名类的语法非常相似。以下语句实现了 Java Calculator接口,并创建了该实现的实例:

// Get the Java interface type

var Calculator = Java.type("com.jdojo.script.Calculator");

// Create an instance of the com.jdojo.script.Calculator interface

// using an anonymous class-like syntax

var calc = new Calculator() {

add: (function (n1, n2) n1 + n2),

subtract: (function (n1, n2) n1 - n2),

multiply: (function (n1, n2) n1 * n2),

divide: (function (n1, n2) n1 / n2)

};

现在你可以像以前一样使用calc对象。

使用 JavaAdapter 对象

JavaScript 允许您实现多个接口,并使用JavaAdapter类扩展一个类。然而,与 JDK 捆绑在一起的 Rhino JavaScript 实现已经覆盖了JavaAdapter的实现,它只允许你实现一个接口;它不允许你扩展一个类。JavaAdapter构造函数的第一个参数是要实现的接口,第二个参数是实现接口抽象方法的脚本对象。要在 Nashorn 中使用JavaAdapter对象,需要加载 Rhino 兼容性模块。下面的代码片段使用JavaAdapter实现了Calculator接口:

// Need to load the compatibility module in Nashorn.

// You do not need to the following load() call in Rhino.

load("nashorn:mozilla_compat.js");

// Load the script that creates the calculator JavaScript object

load("calculator.js");

var calc = new JavaAdapter(com.jdojo.script.Calculator, calculator);

现在你可以像以前一样使用calc对象。它是com.jdojo.script.Calculator接口的一个实例,其实现由在calculator.js文件的脚本中定义的calculator对象中声明的方法提供。

使用 Java.extend()方法

Nashorn 提供了一种更好的方法来扩展一个类并使用Java.extend()方法实现多个接口。在Java.extend()方法中,您最多可以传递一个类类型和多个接口类型。它返回一个组合了所有传入类型的类型。您需要使用前面讨论过的类似匿名类的语法来为新类型的抽象方法提供实现,或者覆盖被扩展类型的现有方法。下面的代码片段使用了Java.extend()方法来实现Calculator接口:

// Get the Calculator interface type

var Calculator = Java.type("com.jdojo.script.Calculator");

// Get a type that extends the Calculator type

var CalculatorExtender = Java.extend(Calculator);

// Implement the abstract methods in CalculatorExtender

// using an anonymous class like syntax

var calc = new CalculatorExtender() {

add: (function (n1, n2) n1 + n2),

subtract: (function (n1, n2) n1 - n2),

multiply: (function (n1, n2) n1 * n2),

divide: (function (n1, n2) n1 / n2)

};

// Use the instance of teh Calculator interface

var x = 15.0, y = 10.0;

var addResult = calc.add(x, y);

var subResult = calc.subtract(x, y);

var mulResult = calc.multiply(x, y);

var divResult = calc.divide(x, y);

printf("calc.add(%.2f, %.2f) = %.2f", x, y, addResult);

printf("calc.subtract(%.2f, %.2f) = %.2f", x, y, subResult);

printf("calc.multiply(%.2f, %.2f) = %.2f", x, y, mulResult);

printf("calc.divide(%.2f, %.2f) = %.2f", x, y, divResult);

calc.add(15.00, 10.00) = 25.00

calc.subtract(15.00, 10.00) = 5.00

calc.multiply(15.00, 10.00) = 150.00

calc.divide(15.00, 10.00) = 1.50

您可以使用Java.extend()方法来扩展具体类、抽象类和接口。下面的代码扩展了具体的类java.lang.Thread并实现了Calculator接口。新的实现覆盖了Thread类的run()方法:

// Get the Calculator interface type

var Calculator = Java.type("com.jdojo.script.Calculator");

var Thread = Java.type("java.lang.Thread");

// Get a type that extends the Calculator type

var ThreadCalcExtender = Java.extend(Thread, Calculator);

// Implement the abstract methods in CalculatorExtender

// using an anonymous class like syntax

var calcThread = new ThreadCalcExtender() {

add: (function (n1, n2) n1 + n2),

subtract: (function (n1, n2) n1 - n2),

multiply: (function (n1, n2) n1 * n2),

divide: (function (n1, n2) n1 / n2),

run: function () {

var n1 = Math.random();

var n2 = Math.random();

printf("n1 = %.2f, n2 = %.2f", n1, n2);

var addResult = this.add(n1, n2);

printf("calc.add(%.2f, %.2f) = %.2f", n1, n2, addResult);

}

};

// Start the thread

calcThread.start();

n1 = 0.61, n2 = 0.66

calc.add(0.61, 0.66) = 1.27

使用 JavaScript 函数

有时一个 Java 接口只有一个方法。在这些情况下,您可以传递一个 JavaScript 函数对象来代替接口的实现。Java 中的Runnable接口只有一个方法run()。当需要在 JavaScript 中使用Runnable接口的实例时,可以传递一个 JavaScript function 对象。下面的代码片段展示了如何创建一个Thread对象并启动它。在Thread类的构造函数中,传递的是 JavaScript 函数对象myRunFunc,而不是Runnable接口的实例:

function myRunFunc() {

print("A thread is running.");

}

// Call Thread(Runnable) constructor and pass the myRunFunc function

// object that will serve as an implementation for the run() method of

// the Runnable interface.

var thread = new java.lang.Thread(myRunFunc);

thread.start();

A thread is running.

访问超类的方法

在 Java 中,当您可以使用关键字super访问超类的方法时。当您在 JavaScript 中扩展一个类时,您也可以使用Java.super()方法访问超类的方法。该方法采用 JavaScript 中已扩展的 JavaScript 对象,并返回一个引用,该引用可用于调用超类的方法。考虑清单 6-4 所示的Person类的代码。

清单 6-4。一个人类

// Person.java

package com.jdojo.script;

public class Person {

private String firstName;

private String lastName;

public Person(String firstName, String lastName){

this.firstName = firstName;

this.lastName = lastName;

}

public String getFirstName() {

return firstName;

}

public void setFirstName(String firstName) {

this.firstName = firstName;

}

public String getLastName() {

return lastName;

}

public void setLastName(String lastName) {

this.lastName = lastName;

}

public String getFullName() {

return firstName + " " + lastName;

}

}

考虑清单 6-5 中的代码。它扩展了Person类并覆盖了getFullName()方法。

清单 6-5。使用 Java.super()方法访问超类方法

// supermethod.js

var Person = Java.type("com.jdojo.script.Person");

var PersonExtender = Java.extend(Person);

// Extend the Person class and override the getFullName() method

var john = new PersonExtender("John", "Jacobs") {

getFullName: function () {

// You can use the variable john here that is declared outside.

var _super_ = Java.super(john);

var fullName = _super_.getFullName();

return fullName.toUpperCase();

}

}

// Get john's full name using the extended class implementation

var johnFullName = john.getFullName() ;

// Get the reference of john's super

var johnSuper = Java.super(john);

// Get john's full name from the Person class

var johnSuperFullName = johnSuper.getFullName();

// Print Names

print("Extended full name:", johnFullName);

print("Super full name:", johnSuperFullName);

Extended full name: JOHN JACOBS

Super full name: John Jacobs

注意扩展的Person类的getFullName()方法引用了在函数外部声明的名为john的变量。下面的语句指定了一个对象的引用,该对象可用于调用john对象的超类的方法。

var _super_ = Java.super(john);

被覆盖的方法调用Person类的“getFullName()方法,将名称转换成大写,并返回它。代码再次获取超类引用,如下所示:

// Get the reference of john's super

var johnSuper = Java.super(john);

最后,代码打印从Person类和扩展的Person类返回的值,以显示您确实能够调用超类方法。

Tip

除了使用Java.super(obj)方法获取obj的超类对象引用并在其上调用方法,还可以使用obj.super$MethodName(args)语法调用名为obj的对象的超类的方法。例如,在示例中,您可以使用john.super$getFullName()来调用对象john上的Person类的getFullName()方法。

使用 Lambda 表达式

JavaScript 支持可以用作 lambda 表达式的匿名函数。下面是一个匿名函数,它将一个数字作为参数并返回其平方值:

function (n) {

return n * n;

}

下面是一个使用匿名函数作为 lambda 表达式在 JavaScript 中创建一个Runnable对象的例子。在Thread类的构造函数中使用了Runnable对象。

var Thread = Java.type("java.lang.Thread");

// Create a Thread using a Runnable object. The Runnable object is

// created using an anonymous function as a lambda expressions.

var thread = new Thread(function () {

print("Hello Thread");

});

// Start the thread

thread.start();

使用 lambda 表达式的 JavaScript 代码的 Java 等效代码如下:

// Create a Thread using a Runnable object. The Runnable object is

// created using a lambda expression.

Thread thread = new Thread(() -> {

System.out.println("Hello Thread");

});

// Start the thread

thread.start();

摘要

在脚本中创建 Java 对象之前,您需要将 Java 类型导入到脚本中。在脚本中有四种导入类型的方法:使用Packages全局对象,使用Java.type()方法,使用importPackage()importClass()函数,以及在with子句中使用JavaImporter。Nashorn 将javajavaxorgcomedunet声明为全局变量,它们分别是Packages.javaPackages.javaxPackages.orgPackages.comPackages.eduPackages.net的别名。因此,您可以使用这些包中任何类型的完全限定名来引用该类型。

您需要使用new操作符和 Java 类型在脚本中创建 Java 对象。使用Java.type()方法可以让您以统一的方式导入 Java 类型,包括数组类型。创建 array 对象的方法与创建任何其他类型的对象的方法相同。

大多数时候,调用重载的 Java 方法是由 Nashorn 解决的。如果希望从脚本调用重载方法的特定版本,可以使用括号标记来指定特定的方法签名。例如,名为pt的对象引用上的pt"print(java.lang.Object)" calls the print(java.lang.Object)方法将 10.5 作为参数传递给该方法。

Nashorn 允许您使用Java.extend()方法在脚本中扩展接口、抽象类和具体类。它允许您使用Java.super()方法调用对象上的超类方法。

七、集合

在本章中,您将学习:

  • 纳申中的数组是什么
  • 如何使用数组文字和Array对象在 Nashorn 中创建数组
  • 如何使用Array对象的不同方法
  • 如何使用类似数组的对象
  • 如何在 Nashorn 中创建和使用类型化数组
  • 如何在 Nashorn 中使用 Java 集合
  • 如何在 Nashorn 中创建 Java 数组
  • 如何将 Java 数组转换成 Nashorn 数组,反之亦然

纳森的数组是什么?

Nashorn 中的数组是一个专门的对象,称为Array对象,用于表示值的有序集合。一个Array对象以一种特殊的方式对待某些属性名。如果一个属性名可以转换成一个介于 0 和 2 32 -2(包括 0 和 2)之间的整数,这样的属性名称为数组索引,属性称为元素。换句话说,数组中的元素是一个特殊的属性,它的名称是数组索引。除了向数组中添加元素之外,还可以像对 Nashorn 对象一样添加任何其他属性。注意,在一个Array对象中,每个数组索引都是一个属性,但是每个属性并不是一个数组索引。例如,您可以将两个名为“0”和“name”的属性添加到数组中,其中“0”是数组索引,因为它可以转换为整数,而“name”只是一个属性。

每个Array对象都有一个名为length的属性,其值大于所有元素的索引。添加元素时,length会自动调整。如果length改变,索引大于或等于新length的元素被删除。注意,与 Java 数组不同,Nashorn 数组是可变长度数组。也就是说,Nashorn 数组不是定长数组;当添加和删除元素或者length属性改变时,它们可以扩展和收缩。

有两种类型的数组:密集数组和稀疏数组。在密集数组中,所有元素的索引都是连续的。Java 数组总是密集数组。在稀疏数组中,所有元素的索引可能不连续。Nashorn 阵列是稀疏阵列。例如,在 Nashorn 中可以有一个数组,该数组在索引 1000 处有一个元素,而在索引 0 到 999 之间没有任何元素。

与 Java 不同,Nashorn 中的数组没有类型。数组中的元素可以是混合类型,一个元素可以是数字,另一个是字符串,另一个是对象,等等。Nashorn 也支持类型化数组,但是它们的工作方式与 Java 数组完全不同。我将在本章的类型化数组部分讨论类型化数组。

创建数组

在 Nashorn 中创建数组有两种方法:

  • 使用数组文本
  • 使用Array对象

使用数组文本

数组文字是表示一个Array对象的表达式。数组文字也称为数组初始值设定项。它是用括号括起来的逗号分隔的表达式列表;列表中的每个表达式代表数组中的一个元素。以下是使用数组文字的示例:

// An array with no elements, also called an empty array

var emptyArray = [];

// An array with two elements

var names = ["Ken", "Li"];

// An array with four element. Elements are of mixed types.

var misc = [1001, "Ken", 1003, new Object()];

// Print the array length and its elements

print("Array's length: " + emptyArray.length + ", elements: " + emptyArray);

print("Array's length: " + names.length + ", elements: " + names);

print("Array's length: " + misc.length + ", elements: " + misc);

Array's length: 0, elements:

Array's length: 2, elements: Ken,Li

Array's length: 4, elements: 1001,Ken,1003,[object Object]

每个Array对象都包含一个toString()方法,该方法以字符串形式返回逗号分隔的数组元素列表。在添加到列表之前,每个元素都被转换为一个字符串。示例中调用了所有数组的toString()方法来打印它们的内容。

忽略数组文本中的尾随逗号。以下两个数组被认为是相同的。两者都有三个要素:

var empIds1 = [10, 20, 30];  // Without a trailing comma. empIds1.length is 3

var empIds2 = [10, 20, 30,]; // Same as [10, 20, 30]. empIds2.length is 3

数组文本中的元素是数组对象的索引属性。对于密集数组,第一个元素的索引为 0,第二个元素的索引为 1,第三个元素的索引为 3,依此类推。我将很快讨论稀疏数组的索引方案。考虑以下具有三个元素的密集阵列。图 7-1 显示了数组中元素的值及其索引。

var names = ["Fu", "Li", "Ho"]

A978-1-4842-0713-0_7_Fig1_HTML.jpg

图 7-1。

Array elements and their indexes in a three-element dense array

可以像访问对象属性一样访问数组元素。唯一的区别是元素的属性名是一个整数。比如在names数组中,names[0]是指第一个元素"Fu"names[1]是指第二个元素"Li"names[2]是指第三个元素"Ho"。下面的代码创建一个密集数组,并使用一个for循环来访问和打印数组的所有元素:

// Create an array with three elements

var names = ["Fu", "Li", "Ho"]

// Print all array elements

for(var i = 0, len = names.length; i < len; i++) {

print("names[" + i + "] = " + names[i]);

}

names[0] = Fu

names[1] = Li

names[2] = Ho

向数组中添加元素等同于在不存在的索引处赋值。下面的代码创建一个包含三个元素的数组,并添加第四个和第五个元素。最后,代码打印数组中的所有元素:

// Create an array with three elements

var names = ["Fu", "Li", "Ho"]

// Add fourth element

names[3] = "Su"; // Adds an element at index 3

// Add fifth element

names[4] = "Bo"; // Adds an element at index 4

// Print all array elements

for(var i = 0, len = names.length; i < len; i++) {

print("names[" + i + "] = " + names[i]);

}

names[0] = Fu

names[1] = Li

names[2] = Ho

names[3] = Su

names[4] = Bo

回想一下,数组是一个对象,因此您可以像向任何其他对象添加属性一样向数组添加属性。如果属性名不是索引,那么该属性将只是一个属性,而不是一个元素。非元素属性对数组的length没有贡献。下面的代码创建一个数组,添加一个元素和一个非元素属性,并打印详细信息:

// Create an array with three elements

var names = ["Fu", "Li", "Ho"]

// Add fourth element

names[3] = "Su"; // Adds an element at index 3

// Add a non-element property to the array. The property name is

// "nationality" that is not an index, so it does not define an element.

// Rather, it is a simply property.

names["nationality"] = "Chinese";

print("names.length = " + names.length);

// Print all array elements using a for loop

print("Using a for loop:");

for(var i = 0, len = names.length; i < len; i++) {

print("names[" + i + "] = " + names[i]);

}

// Print all properties of the array using a for..in loop

print("Using a for..in loop:");

for(var prop in names) {

print("names[" + prop + "] = " + names[prop]);

}

names.length = 4

Using a for loop:

names[0] = Fu

names[1] = Li

names[2] = Ho

names[3] = Su

Using a for..in loop:

names[0] = Fu

names[1] = Li

names[2] = Ho

names[3] = Su

names[nationality] = Chinese

关于这个例子,有几点需要注意:

  • 它创建了一个包含三个元素的数组,索引分别为 0、1 和 2。此时,数组的length为 3。
  • 它在索引 3 处添加了值为“Su”的元素。通过添加这个元素,数组的length自动增加到 4。
  • 它添加了一个名为“国籍”的属性这个属性只是一个属性,而不是一个元素,因为它的名称是一个不能转换为索引的字符串。添加该属性不会影响数组的length。也就是说,length停留在 4。
  • 当它使用 for 循环打印数组时,名为"nationality"的属性不会被打印,因为代码循环遍历索引,而不是所有属性。
  • 当它使用for..in循环时,所有元素和“nationality”属性都被打印出来,因为for..in循环遍历对象的所有属性。这证明了一个数组的所有元素都是属性,但所有属性都不是元素。

请注意,如果属性名可以转换为 0 到 2 32 -2(含)之间的整数,则该属性名被视为索引。检查属性名是否是索引的真正测试是应用以下条件。假设属性名为prop,是一个字符串。如果下面的表达式返回true,则属性名是一个索引;否则,它只是一个属性名:

ToString(ToUint32(prop)) = prop

这里,假设ToUint32()是将属性名转换成无符号 32 位整数的函数,而ToString()是将整数转换成字符串的函数。换句话说,如果将字符串属性名转换为无符号 32 位整数,然后再转换回字符串,从而得到原始的属性名,那么这样的属性名就是索引。如果属性名只是有效范围内的一个数字,如果不包含小数部分,则它是一个索引。下面的代码演示了这条规则:

// Create an array with two elements

var names = ["Fu", "Li"]

// Adds an element at the index 2

names[2.0] = "Su";

// Adds a property named "2.0", not an element at index 2

names["2.0"] = "Bo";

// Adds an element at index 3

names["3"] = "Do";

print("names.length = " + names.length);

// Print all properties of the array using a for..in loop

print("Using a for..in loop:");

for(var prop in names) {

print("names[" + prop + "] = " + names[prop]);

}

names.length = 4

Using a for..in loop:

names[0] = Fu

names[1] = Li

names[2] = Su

names[3] = Do

names[2.0] = Bo

可以将负数属性添加到数组中。注意,作为属性名的负数不符合索引的条件,所以它只是添加一个属性,而不是元素:

// Create an array with two elements

var names = ["Fu", "Li"]

// Adds property with the name "-1", not an element.

names[-1] = "Do"; // names.length is still 2

print("names.length = " + names.length);

names.length = 2

还可以使用数组文本创建稀疏数组。使用逗号而不指定元素列表中的元素会创建一个稀疏数组。请注意,在稀疏数组中,元素的索引是不连续的。下面的代码创建一个稀疏数组:

var names = ["Fu",,"Lo"];

names数组包含两个元素。它们位于索引 0 和 2 处。索引 1 处的元素丢失,这由两个连续的逗号表示。names阵的length是什么?是 3,不是 2。回想一下,数组的length总是大于所有元素的最大索引。数组中的最大索引是 2,所以length是 3。当你尝试阅读names[1]这个不存在的元素时会发生什么?读取names[1]被简单地视为从names对象中读取名为“1”的属性,而名为“1”的属性并不存在。回想一下Chapter 4,读取一个不存在的对象属性会返回undefined。因此,names[1]将简单地返回undefined,而不会导致任何错误。如果您给names[1]赋值,那么您将在索引 1 处创建一个新元素,并且该数组将不再是一个稀疏数组。以下代码显示了这条规则:

// Create a sparse array with 2 existing and 1 missing elements

var names = ["Fu",,"Lo"]; // names.length is 3

print("names.length = " + names.length);

for(var prop in names) {

print("names[" + prop + "] = " + names[prop]);

}

// Add an element at index 1.

names[1] = "Do";  // names.length is still 3

print("names.length = " + names.length);

for(var prop in names) {

print("names[" + prop + "] = " + names[prop]);

}

names.length = 3

names[0] = Fu

names[2] = Lo

names.length = 3

names[0] = Fu

names[1] = Do

names[2] = Lo

以下是稀疏数组的更多示例。注释解释了数组:

var names = [,];   // A sparse array. length = 1 and no elements

names = [,,];      // A sparse array. length = 2 and no elements

names = [,,,];     // A sparse array. length = 3 and no elements

names = [,,,7,,2]; // A sparse array. length = 6 and 2 elements

你能说出下面两个数组的区别吗?

var names1 = [,,];

var names2 = [undefined,undefined];

两个数组都有length 2。名为names1的数组是一个稀疏数组。names1中索引 0 和 1 处的元素不存在。读数names1[0]names1[1]将返回undefined。名为names2的数组是一个密集数组。names2中索引 0 和 1 处的元素存在,并且都被设置为undefined。读数names2[0]names2[1]将返回undefined

如何知道一个数组是否稀疏?在Array对象中没有检查稀疏数组的内置方法。你需要自己检查它,记住如果一个属性名是一个索引(从 0 到 ??)在数组中不存在,那么它就是一个稀疏数组。在遍历数组元素一节中,我将讨论几种检查稀疏数组的方法。

使用数组对象

Nashorn 包含一个名为Array的内置函数对象。它用于创建和初始化数组。它可以作为函数或构造函数调用。它作为函数或构造函数使用的方式是一样的。它的签名是:

Array(arg1, arg2, arg3,...)

Array对象可以接受零个或多个参数。它的初始化行为取决于传递的参数的数量和类型,这些参数可以分为三类:

  • 不传递任何参数
  • 一个参数被传递
  • 传递了两个或多个参数

不传递任何参数

当没有参数传递给Array构造函数时,它创建一个空数组,将数组的length设置为零:

var names1 = new Array(); // Same as: var names = [];

传递一个参数

当一个参数传递给Array构造函数时,参数的类型决定了新数组的创建方式:

  • 如果参数是一个数字,并且是 0 到 2 32 -1(含)范围内的整数,则该参数被视为数组的length。否则,抛出一个RangeError异常。
  • 如果参数不是数字,则创建一个数组,将传递的参数作为该数组的唯一元素。数组的length被设置为 1。

以下代码在 Nashorn 中创建了最大可能的数组:

var names = new Array(Math.pow(2, 32) -1); // The biggest possible array

print("names.length = " + names.length);

names.length = 4294967295

以下语句创建一个数组,其中length为 10。数组中还不存在任何元素:

var names = new Array(10);

以下数组创建表达式抛出了一个RangeError异常,因为参数是一个数字,并且它不是有效范围内的整数或者超出了范围:

var names1 = new Array(34.89);           // Not an integer

var names1 = new Array(Math.pow(2, 32)); // Out of range

var namess = new Array(-10);             // Out of range

以下代码将一个非数字参数传递给Array构造函数,该构造函数使用传递的参数作为唯一元素创建一个数组,并将数组的length设置为 1:

var names1 = new Array("Fu"); // Creates an array with one element "Fu"

var names2 = new Array(true); // Creates an array with one element true

传递两个或多个参数

当两个或更多参数被传递给Array构造函数时,它用指定的参数创建一个密集数组。length被设置为传递的参数数量。以下语句创建一个带有三个传递参数的数组,并将length设置为 3:

var names = new Array("Fu", "Li". "Do");

不能使用Array构造函数创建稀疏数组。在Array构造函数中使用连续逗号或尾随逗号会抛出一个SyntaxError异常:

var names1 = new Array("Fu", "Li",, "Do");  // A SyntaxError

var names2 = new Array("Fu", "Li", "Do", ); // A SyntaxError

您可以通过在不连续的索引处添加元素或删除现有元素来创建稀疏数组,从而使索引变得不连续。我将在下一节讨论删除数组的元素。下面的代码创建一个密集数组,并添加一个不连续的元素使其成为一个稀疏数组:

// Creates a dense array with elements at indexes 0 and 1.

var names = new Array("Fu", "Li");  // names.length is set to 2

print("After creating the array: names.length = " + names.length);

// Add an element at index 4, skipping index 2 and 3.

names[4] = "Do"; // names.length is set to 5, making names a sparse array

print("After adding an element at index 4: names.length = " + names.length);

for(var prop in names) {

print("names[" + prop + "] = " + names[prop]);

}

After creating the array: names.length = 2

After adding an element at index 4: names.length = 5

names[0] = Fu

names[1] = Li

names[4] = Do

删除数组元素

删除数组元素或数组的非元素属性与删除对象的属性相同。使用delete操作符删除一个数组元素。如果从密集数组的中间或开头删除一个元素,数组将变得稀疏。下面的代码显示了如何从数组中删除元素:

// Creates a dense array with elements at indexes 0, 1, and 2.

var names = new Array("Fu", "Li", "Do");

print("Before deleting:");

print("names.length = " + names.length + ", Elements = " + names);

// Delete the element at index 1

delete names[1]; // names.length remains 3

print("AFter deleting:");

print("names.length = " + names.length + ", Elements = " + names);

Before deleting:

names.length = 3, Elements = Fu,Li,Do

AFter deleting:

names.length = 3, Elements = Fu,,Do

您可以将数组的元素设置为不可配置和不可写,这样它们就不能被删除和修改。删除不可配置的元素没有任何效果。在严格模式下,删除不可配置的元素会产生错误。下面的代码演示了这一点:

var names = new Array("Fu", "Li", "Do");

// Make the element at index 1 non-configurable

Object.defineProperty(names, "1", {configurable: false});

print("Before deleting:");

print("names.length = " + names.length + ", Elements = " + names);

delete names[1]; // Will not delete "Li" as it is non-configurable.

print("AFter deleting:");

print("names.length = " + names.length + ", Elements = " + names);

Before deleting:

names.length = 3, Elements = Fu,Li,Do

AFter deleting:

names.length = 3, Elements = Fu,Li,Do

数组长度

每个Array对象都有一个名为length的属性,当在数组中添加和删除元素时会自动维护该属性。length属性使数组不同于其他类型的对象。对于密集数组,length比数组中最大的索引大 1。对于稀疏数组,它保证大于所有元素(现有的和缺失的)的最大索引。

数组的length属性是可写的。也就是说,您也可以在代码中更改它。如果您将length设置为大于当前值的值,length将被更改为新值,从而在末尾创建一个稀疏数组。如果将length设置为一个小于其当前值的值,从末尾开始的所有元素都将被删除,直到找到一个大于或等于新的length值的不可删除元素。也就是说,将length设置为较小的值会使数组收缩为不可删除的元素。下面的例子将阐明这一规则:

var names = new Array("Fu", "Li", "Do", "Ho");

print("names.length = " + names.length + ", Elements = " + names);

print("Setting length to 10...");

names.length = 10;

print("names.length = " + names.length + ", Elements = " + names);

print("Setting length to 0...");

names.length = 0;

print("names.length = " + names.length + ", Elements = " + names);

print("Recreating the array...");

names = new Array("Fu", "Li", "Do", "Ho");

print("names.length = " + names.length + ", Elements = " + names);

print('Making "Do" non-configurable...');

// Makes "Do" non-configurable (non-deletable)

Object.defineProperty(names, "2", {configurable:false});

print("Setting length to 0...");

names.length = 0; // Will delete only "Ho" as "Do" is non-deletable

print("names.length = " + names.length + ", Elements = " + names);

names.length = 4, Elements = Fu,Li,Do,Ho

Setting length to 10...

names.length = 10, Elements = Fu,Li,Do,Ho,,,,,,

Setting length to 0...

names.length = 0, Elements =

Recreating the array...

names.length = 4, Elements = Fu,Li,Do,Ho

Making "Do" non-configurable...

Setting length to 0...

names.length = 3, Elements = Fu,Li,Do

Array对象的length属性是可写的、不可数的和不可配置的。如果不希望有人在代码中更改它,可以将其设置为不可写。在数组中添加和删除元素时,不可写的length属性仍然会自动改变。以下代码显示了这条规则:

var names = new Array("Fu", "Li", "Do", "Ho");

// Make the length property non-writable.

Object.defineProperty(names, "length", {writable:false});

// The length property cannot be changed directly anymore

names.length = 0; // No effects

// Add a new element

names[4] = "Nu"; // names.length changes from 4 to 5

迭代数组元素

如果您对迭代数组的所有属性(包括元素)感兴趣,可以简单地使用for..infor..each..in语句。这些语句不应该以任何特定的顺序迭代数组。正如这一节的标题所暗示的,我将讨论如何只迭代数组的元素,尤其是当数组是稀疏的时候。如果您只向数组中添加元素(而不是任何非元素属性),这是您在大多数情况下都会做的,那么使用for..infor..each..in语句对于密集和稀疏都很好。

使用 for 循环

如果您知道数组是密集的,您可以使用简单的for循环来迭代数组,如下所示:

// Create a dense array

var names = new Array("Fu", "Li", "Do");

// Use a for loop to iterate all elements of an array

for(var i = 0, len = names.length; i < len; i++) {

print("names[" + i + "]=" + names[i]);

}

names[0]=Fu

names[1]=Li

names[2]=Do

如果你有一个稀疏的数组,for循环不起作用。如果您尝试访问稀疏数组中缺少的元素,它将返回undefined,但不会告诉您是缺少元素还是现有元素的值是undefined。您可以通过使用in操作符来检查被迭代的索引是否存在,从而摆脱这个限制。如果索引存在,则元素存在;如果索引不存在,则它是稀疏数组中缺少的元素。下面的代码演示了这种方法:

// Create a sparse array with an element set to undefined

var names = ["Fu", "Li", , "Do", undefined, , "Lu"];

// Use a for lop to iterate all elements of an array

for (var i = 0, len = names.length; i < len; i++) {

// Check if the index being visited exists in the array

if (i in names) {

print("names[" + i + "]=" + names[i]);

}

}

names[0]=Fu

names[1]=Li

names[3]=Do

names[4]=undefined

names[6]=Lu

考虑下面的代码。它创建一个只有一个元素的稀疏数组。该数组是 Nashorn 中可能的最大数组,元素被添加到数组的最后一个索引处。实际上,你永远也不会有这个大数组。我使用它只是为了证明使用for循环并不是访问稀疏数组中所有元素的最有效方式:

// Create an empty array

var names = new Array();

// Add one element to the end of the biggest possible  array

names[4294967294] = "almost lost";

// Use a for loop to iterate all elements of an array

for (var i = 0, len = names.length; i < len; i++) {

// Check if the index being visited exists in the array

if (i in names) {

print("names[" + i + "]=" + names[i]);

}

}

在这个例子中使用for循环是非常低效的,因为它必须访问 4294967293 个索引,才能到达最后一个有值的索引。只访问数组中的一个元素花费的时间太长。我将很快使用for..in循环解决这个运行缓慢的for循环问题。

使用 forEach()方法

Array.prototype对象包含一个名为forEach()的方法,该方法按照升序为数组中的每个元素调用回调函数。它只访问稀疏数组中的现有元素。它的签名是:

forEach (callbackFunc [, thisArg])

这里,callbackFunc是按升序对数组中的每个元素调用一次的函数。传递给它三个参数:元素的值、元素的索引和被迭代的对象。thisArg是可选的;如果指定了它,它将被用作每次调用callbackFuncthis值。

forEach()方法在开始执行之前设置它将访问的索引范围。如果在执行过程中添加了超出该范围的元素,则不会访问这些元素。如果初始设置范围内的元素被删除,这些元素也不会被访问:

// Create a sparse array with an element set to undefined

var names = ["Fu", "Li", , "Do", undefined, , "Lu"];

// Define the callback function

var visitor = function (value, index, array) {

print("names[" + index + "]=" + value);

};

// Print all elements

names.forEach(visitor);

names[0]=Fu

names[1]=Li

names[3]=Do

names[4]=undefined

names[6]=Lu

如果您使用forEach()方法访问一个大的稀疏数组,您将会遇到与使用我在上一节中讨论的for循环相同的问题。

使用 for-in 循环

让我们使用for..in循环来解决访问长稀疏数组中元素的低效问题。使用for..in循环带来了另一个问题,它遍历数组的所有属性,而不仅仅是元素。但是,它不会迭代丢失的元素。您需要一种方法来区分简单属性和现有元素。只需使用数组索引的定义就可以做到这一点。数组索引是一个介于 0 和 2 32 -2 之间的整数。如果属性是数组索引,则它是一个元素;否则,它只是一个属性,而不是一个元素。清单 7-1 包含一个名为isValidArrayIndex()的函数的代码。该函数将属性名作为参数。如果属性名是有效的数组索引,则返回true;否则,它返回false

清单 7-1。isValidArrayIndex()函数的代码

// arrayutil.js

var UPPER_ARRAY_INDEX = Math.pow(2, 32) - 2;

Object.defineProperty(this, "UPPER_ARRAY_INDEX", {writable: false});

function isValidArrayIndex(prop) {

// Convert the prop to a Number

var numericProp = Number(prop);

// Check if prop is a number

if (String(numericProp) === prop) {

// Check if is an integer

if (Math.floor(numericProp) === numericProp) {

// Check if it is in the valid array index range

if (numericProp >= 0 &&                             numericProp <= UPPER_ARRAY_INDEX) {

return true;

}

}

}

return false;

}

下面的代码使用for..in循环只迭代稀疏数组的元素。它利用isValidArrayIndex()函数来确定一个属性是否是有效的数组索引。请注意,代码在数组中最大可能的索引处添加了一个元素。for..in循环非常快速地到达数组的所有元素。

// load the script that contains isValidArrayIndex() function

load("arrayutil.js");

// Create a sparse array with an element set to undefined

var names = ["Fu", "Li", , "Do", undefined, , "Lu"];

// Add some properties to the array

names["nationality"] = "Chinese";  // A property

names[4294967294] = "almost lost"; // An element at the largest

// possible array index

names[3.2] = "3.2";   // A property

names[7.00] = "7.00"; // An element

// Print all elements, ignoring the non-element properties

for(var prop in names) {

if (isValidArrayIndex(prop)) {

// It is an element

print("names[" + prop + "]=" + names[prop]);

}

}

names[0]=Fu

names[1]=Li

names[3]=Do

names[4]=undefined

names[6]=Lu

names[7]=7.00

names[4294967294]=almost lost

检查数组

Array对象包含一个isArray()静态方法,可以用来检查一个对象是否是一个数组。下面的代码演示了它的用法:

var obj1 = [10, 20];     // An array

var obj2 = {x:10, y:20}; // An object, but not an array

print("Array.isArray(obj1) =",  Array.isArray(obj1));

print("Array.isArray(obj2) =",  Array.isArray(obj2));

Array.isArray(obj1) = true

Array.isArray(obj2) = false

多维数组

Nashorn 有任何特殊的构造来支持多维数组。您需要使用数组的数组(曲折数组)来创建多维数组,其中数组的元素可以是数组。通常,您需要嵌套的for循环来填充和访问多维数组的元素。下面的代码演示了如何创建、填充和打印 3x3 矩阵的元素。代码使用Array对象的join()方法,使用制表符作为分隔符来连接数组的元素:

var ROWS = 3;

var COLS = 3;

// Create a 3x3 array, so you pre-allocate the memory

var matrix = new Array(ROWS);

for(var i = 0; i < ROWS; i++) {

matrix[i] = new Array(COLS);

}

// Populate the array

for(var i = 0; i < ROWS; i++) {

for(var j = 0; j < COLS; j++) {

matrix[i][j] = i + "" + j;

}

}

// Print the array elements

for(var i = 0; i < ROWS; i++) {

var rowData = matrix[i].join("\t");

print(rowData);

}

00        01        02

10        11        12

20        21        22

数组对象的方法

Array.prototype对象定义了几个方法,作为所有Array对象的实例方法。在这一节中,我将讨论这些方法。所有数组方法都是有意泛型的,因此它们不仅可以应用于数组,还可以应用于任何类似数组的对象。

串联元素

concat(arg1, arg2,...)方法创建并返回一个新数组,该数组包含调用该方法的数组对象的元素,后跟指定的参数。如果一个参数是一个数组,数组的元素被连接起来。如果参数是嵌套数组,它将被展平一级。此方法不会修改原始数组和作为参数传递的数组。如果原始数组或参数包含对象,则创建对象的浅表副本。使用concat()方法的例子如下:

var names = ["Fu", "Li"];

// Assigns ["Fu", "Li", "Do", "Su"] to names2

var names2 = names.concat("Do", "Su");

// Assigns ["Fu", "Li", "Do", "Su", ["Lu", "Zu"], "Yu"] to names3

var names3 = names.concat("Do", ["Su", ["Lu","Zu"]], "Yu");

连接数组元素

join(separator)方法将数组的所有元素转换成字符串,使用separator将它们连接起来,并返回结果字符串。如果未指定separator,则使用逗号作为分隔符。该方法作用于数组的所有元素,从索引 0 到等于length - 1的索引。如果元素是undefinednull,则使用空字符串。以下是使用join()方法的一些例子:

var names = ["Fu", "Li", "Su"];

var namesList1 = names.join();    // Assigns "Fu,Li,Su" to namesList1

var namesList2 = names.join("-"); // Assigns "Fu-Li-Su" to namesList2

var ids = [10, 20, , 30, undefined, 40, null]; // A sparse array

var idsList1 = ids.join();    // Assigns "10,20,,30,,40," to idsList1

var idsList2 = ids.join("-"); // Assigns "10-20--30--40-" to idsList2

反转数组元素

方法以相反的顺序重新排列数组的元素并返回数组。使用这种方法的例子如下:

var names = ["Fu", "Li", "Su"];

names.reverse(); // Now, the names array contains ["Su","Li","Fu"]

// Assigns "Fu,Li,Su" to reversedList

var reversedList = names.reverse().join();

分割数组

slice(start, end)方法从startend之间的索引返回一个数组,该数组包含原始数组的子数组。如果startend为负,则分别作为start + lengthend + length处理,其中length是数组的长度。startend都被限制在 0 和length之间(包括 0 和 ??)。如果未指定end,则假定为length。如果startend(互斥)包含一个稀疏范围,那么得到的子数组将是稀疏的。以下是使用slice()方法的例子:

var names = ["Fu", "Li", "Su"];

// Assigns ["Li","Su"] to subNames1\. end = 5 will be replaced with end = 3.

var subNames1 = names.slice(1, 5);

// Assigns ["Li","Su"] to subNames2\. start = -1 is used as start = 1 (-2 + 3).

var subNames12 = names.slice(-2, 3);

var ids = [10, 20,,,30, 40, 40]; // A sparse array

// Assigns [20,,,30,40] to idsSubList whose length = 5

var idsSubList = ids.slice(1, 6);

拼接数组

根据传递的参数,splice()方法可以执行插入和/或删除。它的签名是:

splice (start, deleteCount, value1, value2,...)

该方法删除从索引start开始的deleteCount元素,并插入指定的参数(value1value2等)。)在索引start。它返回一个数组,其中包含从原始数组中删除的元素。在删除和/或插入之后,现有元素的索引被调整,因此它们是连续的。如果deleteCount为 0,则插入指定值而不删除任何元素。以下是使用该方法的示例:

var ids = [10, 20, 30, 40, 50];

// Replace 10 and 20 in the array with 100 and 200

var deletedIds  = ids.splice(1, 2, 100, 200);

print("ids = " + ids);

print("deletedIds = " + deletedIds);

// Keep the first 3 elements and delete the rest

var deletedIds2  = ids.splice(3, 2);

print("ids = " + ids);

print("deletedIds2 = " + deletedIds2);

ids = 10,100,200,40,50

deletedIds = 20,30

ids = 10,100,200

deletedIds2 = 40,50

对数组排序

sort(compareFunc)方法就地对数组进行排序,并返回排序后的数组。compareFunc参数是一个有两个参数的函数,是可选的。如果未指定,则数组按字母顺序排序,如果需要,在比较期间将元素转换为字符串。未定义的元素被排序到末尾。

如果指定了compareFunc,它将被传递两个元素,它的返回值将决定这两个元素的顺序。假设它被传递了 x 和 y,如果 x < y 返回负值,如果x = y返回零,如果x > y返回正值。下面的代码通过不指定compareFunc对一个整数数组进行升序排序。随后,它通过指定一个compareFunc对同一个数组进行降序排序:

var ids = [30, 10, 40, 20];

print("ids = " + ids);

// Sort the array

ids.sort();

print("Sorted ids = " + ids);

// A comparison function to sort ids in descending order

var compareDescending = function (x, y) {

if (x > y) {

return -1;

}

else if (x < y) {

return 1;

}

else {

return 0;

}

};

ids.sort(compareDescending);

print("Sorted in descending order, ids = " + ids);

ids = 30,10,40,20

Sorted ids = 10,20,30,40

Sorted in descending order, ids = 40,30,20,10

在端点添加和移除元素

以下四种方法允许您在数组的开头和结尾添加和移除元素:

  • unshift(value1, value2,...):unshift()方法将参数添加到数组的前面,因此它们出现在数组开头的顺序与它们作为参数出现的顺序相同。它返回数组的新的length。调整现有元素的索引以为新元素腾出空间。
  • shift():shift()方法移除并返回数组的第一个元素,将所有其他元素左移一个位置。
  • push(value1, value2,...):push()方法与unshift()方法的工作原理相同,除了它在数组的末尾添加元素。
  • pop():pop()方法与shift()方法工作方式相同,除了它移除并返回数组的最后一个元素

以下是使用这些方法的示例:

var ids = [10, 20, 30];

print("ids: " + ids);

ids.unshift(100, 200, 300);

print("After ids.unshift(100, 200, 300): " + ids);

ids.shift();

print("After ids.shift(): " + ids);

ids.push(1, 2, 3);

print("After ids.push(1, 2, 3): " + ids);

ids.pop();

print("After ids.pop(): " + ids);

ids: 10,20,30

After ids.unshift(100, 200, 300): 100,200,300,10,20,30

After ids.shift(): 200,300,10,20,30

After ids.push(1, 2, 3): 200,300,10,20,30,1,2,3

After ids.pop(): 200,300,10,20,30,1,2

使用这些方法,您可以实现堆栈、队列和双端队列。让我们使用push()pop()方法实现一个堆栈。清单 7-2 包含了Stack对象的代码。它将堆栈的数据保存在私有的Array对象中。它提供了isEmpty()push()pop()peek()方法来执行堆栈操作。它覆盖了Object.prototypetoString()方法,以字符串形式返回堆栈中的当前元素。清单 7-3 包含了测试Stack对象的代码。

清单 7-2。堆栈对象声明

// stack.js

// Define the constructor for the Stack object

function Stack(/*varargs*/) {

// Define a private array to keep the stack elements

var data = new Array();

// If any arguments were passed to the constructor, add them to the stack

for (var i in arguments) {

data.push(arguments[i]);

}

// Define methods

this.isEmpty = function () {

return (data.length === 0);

};

this.pop = function () {

if (this.isEmpty()) {

throw new Error("Stack is empty.");

}

return data.pop();

};

this.push = function (arg) {

data.push(arg);

return arg;

};

this.peek = function () {

if (this.isEmpty()) {

throw new Error("Stack is empty.");

}

else {

return data[data.length - 1];

}

};

this.toString = function () {

return data.toString();

};

}

清单 7-3。测试堆栈对象

// stacktest.js

load("stack.js");

// Create a Stack with initial 2 elements

var stack = new Stack(10, 20);

print("Stack = " + stack);

// Push an element

stack.push(40);

print("After push(40), Stack = " + stack);

// Pop two elements

stack.pop();

stack.pop();

print("After 2 pops, Stack = " + stack);

print("stack.peek() = " + stack.peek());

print("stack.isEmpty() = " + stack.isEmpty());

// Pop the last element

stack.pop();

print("After another pop(), stack.isEmpty() = " + stack.isEmpty());

Stack = 10,20

After push(40), Stack = 10,20,40

After 2 pops, Stack = 10

stack.peek() = 10

stack.isEmpty() = false

After another pop(), stack.isEmpty() = true

搜索数组

有两种方法,indexOf()lastIndexOf(),可以让您在数组中搜索指定的元素。他们的签名是:

  • indexOf(searchElement, fromIndex)
  • lastIndexOf(searchElement, fromIndex)

indexOf()方法从fromIndex开始在数组中搜索指定的searchElement。如果没有指定fromIndex,则假定为 0,这意味着搜索整个数组。该方法使用===运算符返回数组中等于searchElement的第一个元素的索引。如果没有找到searchElement,则返回-1。

lastIndexOf()方法的工作方式与indexOf()方法相同,除了它从末尾到开头执行搜索。也就是说,它返回数组中最后找到的元素的索引,该索引等于searchElement。下面的代码显示了如何使用这些方法:

var ids = [10, 20, 30, 20];

print("ids = " + ids);

print("ids.indexOf(20) = " + ids.indexOf(20));

print("ids.indexOf(20, 2) = " + ids.indexOf(20, 2));

print("ids.lastIndexOf(20) = " + ids.lastIndexOf(20));

print("ids.indexOf(25) = " + ids.lastIndexOf(25));

ids = 10,20,30,20

ids.indexOf(20) = 1

ids.indexOf(20, 2) = 3

ids.lastIndexOf(20) = 3

ids.indexOf(25) = -1

评估谓词

您可以检查一个数组中的所有或部分元素的谓词是否计算为true。以下两种方法允许您执行此操作:

  • every(predicate, thisArg)
  • some(predicate, thisArg)

第一个参数predicate是一个函数,对数组中的每个现有元素调用一次。向该函数传递三个参数:元素的值、元素的索引和数组对象。如果指定了第二个参数thisArg,它将被用作由predicate指定的函数调用的this值。

如果指定的predicate函数为数组中的每个现有元素返回真值,则every()方法返回true。否则返回false。一旦predicate返回一个元素的 falsy 值,该方法就返回false

如果predicate返回至少一个元素的真值,则some()方法返回true。否则返回false。一旦predicate返回一个元素的真值,该方法就返回true。以下示例显示如何检查数字数组是否包含任何/所有偶数/奇数:

var ids = [10, 20, 30, 20];

print("ids = " + ids);

var hasAnyEven = ids.some(function (value, index, array) {

return value %2 === 0;

});

var hasAllEven = ids.every(function (value, index, array) {

return value %2 === 0;

});

var hasAnyOdd = ids.some(function (value, index, array) {

return value %2 === 1;

});

var hasAllOdd = ids.every(function (value, index, array) {

return value %2 === 1;

});

print("ids has any even numbers: " + hasAnyEven);

print("ids has all even numbers: " + hasAllEven);

print("ids has any odd numbers: " + hasAnyOdd);

print("ids has all odd numbers: " + hasAllOdd);

ids = 10,20,30,20

ids has any even numbers: true

ids has all even numbers: true

ids has any odd  numbers: false

ids has all odd  numbers: false

将数组转换为字符串

你可以用任何方式将数组转换成字符串。然而,toString()toLocaleString()方法提供了两个内置的实现来将数组元素转换成字符串。toString()方法返回一个字符串,该字符串是在没有指定任何分隔符的情况下通过调用数组上的join()方法返回的。也就是说,它以字符串形式返回元素列表,其中元素用逗号分隔。toLocaleString()方法调用所有数组元素的toLocaleString()方法,使用特定于本地的分隔符连接返回的字符串,并返回最终的字符串。每当您将数组对象用作print()方法的参数或将其与字符串连接操作符一起使用时,您就一直在使用Array对象的toString()方法。

数组的流状处理

Java 8 引入了 Streams API,允许您使用 map-filter-reduce 等几种模式将 Java 集合作为流进行处理。Nashorn 中的Array对象提供了将类似流的处理应用于数组的方法。这些方法是:

  • map()
  • filter()
  • reduce()
  • reduceRight()
  • forEach()
  • some()
  • every()

我已经在前一节详细讨论了最后三种方法。我将在本节讨论其他方法。

方法将一个数组的元素映射到另一个值,并返回一个包含映射值的新数组。原始数组不会被修改。它的签名是:

map(callback, thisArg)

第一个参数callback是为数组中的每个元素调用的函数;向该函数传递三个参数:元素的值、元素的索引和数组本身。函数的返回值是新的(映射的)数组的元素。第二个参数用作函数调用中的this值。下面是一个使用map()方法的例子。它将数字数组的每个元素映射到它们的正方形:

var nums = [1, 2, 3, 4, 5];

// Map each element in the nums array to their squares

var squaredNums = nums.map(function (value, index, data) value * value);

print(nums);

print(squaredNums);

1,2,3,4,5

1,4,9,16,25

filter()方法返回一个新数组,其中包含传递谓词的原始数组的元素。它的签名是:

filter(callback, thisArg)

第一个参数callback是为数组中的每个元素调用的函数;向该函数传递三个参数:元素的值、元素的索引和数组本身。如果函数返回一个真值,则该元素包含在返回的数组中;否则,该元素将被排除。第二个参数用作函数调用中的this值。下面的代码展示了如何使用filter()方法从数组中过滤出偶数:

var nums = [1, 2, 3, 4, 5];

// Filter out even numbers, keep odd numbers only

var oddNums = nums.filter(function (value, index, data) (value % 2 !== 0));

print(nums);

print(oddNums);

1,2,3,4,5

1,3,5

reduce()方法对数组应用归约操作,将其归约为单个值,比如计算一个数字数组中所有元素的总和。它返回一个计算值。它的签名是:

reduce(callback, initialValue)

第一个参数callback是用四个参数调用的函数:前一个值、当前值、当前索引和数组本身。

如果未指定initialValue:

  • 对于第一次调用callback,数组的第一个元素作为前一个值传递,第二个元素作为当前值传递;第二个元素的索引作为索引传递
  • 对于对callback的后续调用,从先前调用返回的值作为先前的值传递。当前值和索引是当前元素的值和索引

如果指定了initialValue:

  • 对于第一次调用callbackinitialValue作为前一个值传递,第一个元素作为当前值传递;第一个元素的索引作为索引传递
  • 对于对callback的后续调用,前一次调用返回的值作为前一次值传递;当前值和索引是当前元素的值和索引

除了从头到尾处理数组元素之外,reduceRight()方法的工作方式与reduce()方法相同。下面的代码展示了如何使用reduce()reduceRight()方法来计算数组中所有数字的总和,以及连接字符串数组的元素:

var nums = [1, 2, 3, 4, 5];

// Defines a reducer function to compute sum of elements of an array

var sumReducer = function(previous, current, index, data) {

return previous + current;

};

var sum = nums.reduce(sumReducer);

print("Numbers :" + nums);

print("Sum: " + sum);

// Defines a reducer function to concatenate elements of an array

var concatReducer = function(previous, current, index, data) {

return previous + "-" + current;

};

var names = ["Fu", "Li", "Su"];

var namesList = names.reduce(concatReducer);

var namesListRight = names.reduceRight(concatReducer);

print("Names: " + names);

print("Names Reduced List: " + namesList);

print("Names Reduced Right List: " + namesListRight);

Numbers :1,2,3,4,5

Sum: 15

Names: Fu,Li,Su

Names Reduced List: Fu-Li-Su

Names Reduced Right List: Su-Li-Fu

您还可以将这些方法链接起来,对数组执行复杂的处理。以下代码显示了如何在一条语句中计算数组中所有正奇数的平方和:

var nums = [-2, 1, 2, 3, 4, 5, -11];

// Compute the sum of squares of all positive odd numbers

var sum = nums.filter(function (value, index, data) value > 0 && value % 2 !== 0)

.map(function (value, index, data) value * value)

.reduce(function (prev, curr, index, data) prev + curr, 0);

print("Numbers: " + nums);

print("Sum of squares of positive odd elements: " + sum);

Numbers: -2,1,2,3,4,5,-11

Sum of squares of positive odd elements: 35

类似数组的对象

数组有两个不同于常规对象的特征:

  • 它有一个length属性
  • 它的元素有特殊的整数属性名

您可以定义任何具有这两个特征的对象,并将它们称为类数组对象。下面的代码定义了一个名为list的对象,它是一个类似数组的对象:

// Creates an array-like object

var list = {"0":"Fu", "1":"Su", "2":"Li", length:3};

Nashorn 包含一些类似数组的对象,比如 String 和arguments对象。Array.prototype对象中的大多数方法都是通用的,这意味着它们可以在任何类似数组的对象上调用,而不一定只在数组上调用。下面的代码显示了如何在类似数组的对象上调用Array.prototype对象的join()方法:

// An array-like object

var list = {"0":"Fu", "1":"Su", "2":"Li", length:3};

var joinedList = Array.prototype.join.call(list, "-");

print(joinedList);

Fu-Su-Li

字符串对象也是一个类似数组的对象。它维护一个只读的length属性,每个字符都有一个索引作为它的属性名。您可以对字符串执行以下类似数组的操作:

// A String obejct

var str = new String("ZIP");

print("str[0] = " + str[0]);

print("str[1] = " + str[1]);

print("str[2] = " + str[2]);

print('str["length"] = ' + str["length"]);

使用 String 对象的charAt()length()方法可以获得相同的结果。下面的代码将字符串转换为大写,删除英文字母的元音字母,并使用连字符作为分隔符连接所有字符。所有这些都是使用Array.prototype对象上的方法完成的,就好像字符串是一个字符数组一样:

// A String object

var str = new String("Nashorn");

// Use the map-filter-reduce patern on the string

var newStr = Array.prototype.map.call(str, (function (v, i, d) v.toUpperCase()))

.filter(function (v, i, d) (v !== "A" && v !== 'E' && v !== 'I' && v !== 'O' && v !== 'U'))

.reduce(function(prev, cur, i, data) prev + "-" + cur);

print("Original string: " + str);

print("New string: " + newStr);

Original string: Nashorn

New string: N-S-H-R-N

类型化数组

Nashorn 中的类型化数组是类似数组的对象。它们提供了被称为缓冲区的原始二进制数据的视图。同一个缓冲区可以有多个类型化视图。假设你有一个 4 字节的缓冲区。您可以拥有一个 8 位有符号整数类型的缓冲区数组视图,它将表示四个 8 位有符号整数。同时,您可以拥有同一缓冲区的 32 位无符号整数类型数组视图,该视图可以表示一个 32 位无符号整数。

Tip

类型化数组是一个类似数组的对象,提供内存中原始二进制数据的类型化视图。Nashorn 中类型化数组实现的规范可以在 https://www.khronos.org/registry/typedarray/specs/latest/ 找到。

类型化数组中的缓冲区由一个ArrayBuffer对象表示。直接或间接地使用一个ArrayBuffer对象,您可以创建一个类型化的数组视图。一旦有了类型化数组视图,就可以使用类似数组的语法写入或读取类型化数组视图支持的特定类型的数据。在下一节中,我将讨论如何使用ArrayBuffer对象。在随后的章节中,我将讨论不同类型的类型化数组视图(或者简称为类型化数组)。

ArrayBuffer 对象

一个ArrayBuffer对象表示原始二进制数据的固定长度缓冲区。ArrayBuffer的内容没有类型。您不能直接修改ArrayBuffer中的数据;为此,您必须在其上创建并使用一个类型化视图。它包含一些属性和方法,可以将其内容复制到另一个ArrayBuffer中,并查询其大小。

ArrayBuffer构造函数接受一个参数,即缓冲区的长度(以字节为单位)。创建后,ArrayBuffer对象的长度不可更改。该对象有一个名为byteLength的只读属性,表示缓冲区的长度。以下语句创建一个 32 字节的缓冲区,并打印其长度:

// Create a ArrayBuffer of 32 bytes

var buffer = new ArrayBuffer(32);

// Assigns 32 to len

var len = buffer.byteLength;

Tip

当您创建一个ArrayBuffer时,它的内容被初始化为零。在创建时,无法用非零值初始化其内容。ArrayBuffer中的每个字节使用一个从零开始的索引。第一个字节的索引为 0,第二个为 1,第三个为 2,依此类推。

Nashorn 实现的类型化数组规范在ArrayBuffer对象中包含一个isView(args)静态方法。不过在 Java 8u40 中似乎并没有被 Nashorn 实现。一个错误已经在bugs.openjdk.java.net/browse/JDK-8061959备案。如果指定的参数表示数组缓冲区视图,该方法返回true。例如,如果您将一个类似于Int8ArrayInt16ArrayDataView等的对象传递给这个方法,它将返回true,因为这些对象表示一个数组缓冲区视图。如果您向该方法传递一个简单的对象或原始值,它将返回false。下面的代码显示了如何使用此方法。注意,从 Java 8u40 开始,这个方法在 Nashorn 中不存在,代码将抛出一个异常:

// Creates an array buffer view of length 4

var int8View = new Int8Array(4);

// Assigns true to isView1

var isView1 = ArrayBuffer.isView(int8View);

// Assigns false to isView2

var isView2 = ArrayBuffer.isView({});

ArrayBuffer对象包含一个slice(start, end)方法,该方法创建并返回一个新的ArrayBuffer,它的内容是从索引startend的原始ArrayBuffer的副本,包含在内,不包含在内。如果startend为负,则表示从缓冲区末端开始的索引。如果未指定end,则默认为ArrayBufferbyteLength属性。startend都夹在 0 和byteLength—1之间。以下代码显示了如何使用slice()方法:

// Create an ArrayBuffer of 4 bytes. Bytes have indexes 0, 1, 2, and 3

var buffer = new ArrayBuffer(4);

// Manipulate buffer using one of the typed views here...

// Copy the last 2 bytes from buffer to buffer2

var buffer2 = buffer.slice(2, 4);

// Copy the bytes from buffer from index 1 to the end (last 3 bytes)

// to buffer3

var buffer3 = buffer.slice(1);

数组缓冲区的视图

一个ArrayBuffer有两种视图:

  • 类型化数组视图
  • 数据视图视图
类型化数组

类型化数组视图是类似数组的对象,处理特定类型的值,如 32 位有符号整数。它们提供了向ArrayBuffer读写特定类型数据的方法。在类型化数组视图中,所有数据值的大小都相同。表 7-1 包含类型化数组视图的列表、它们的大小和描述。表中的大小是类型化数组视图的一个元素的大小。例如,Int8Array包含长度为 1 个字节的元素。你可以把Int8Array想象成 Java 中的byte数组类型,把Int16Array想象成 Java 中的short数组类型,把Int32Array想象成 Java 中的int数组类型。

Tip

类型化数组是一个固定长度、类似密集数组的对象,而Array对象是一个可变长度的数组,可以是稀疏的也可以是密集的。

表 7-1。

The List of Typed Array Views, Their Sizes, and Descriptions

类型化数组视图字节大小描述
Int8Arrayone8 位二进制补码有符号整数
Uint8Arrayone8 位无符号整数
Uint8ClampedArrayone8 位无符号整数(箝位)
Int16ArrayTwo16 位二进制补码有符号整数
Uint16ArrayTwo16 位无符号整数
Int32Arrayfour32 位二进制补码有符号整数
Uint32Arrayfour32 位无符号整数
Float32Arrayfour32 位 IEEE 浮点
Float64Arrayeight64 位 IEEE 浮点

让我们区分两种数组类型:Uint8ArrayUint8ClampedArray。两个数组中的元素都包含从 0 到 255 的 8 位无符号整数。Uint8Array使用模 256 来存储数组中的值,而Uint8ClampedArray将值固定在 0 到 255 之间(包括 0 和 255)。例如,如果您在一个UInt8array中存储 260,则存储 4,因为 260 对 256 取模是 4。如果你在一个Uint8ClampedArray中存储 260,存储 255 是因为 260 大于 255,上限被箝位在 255。类似地,存储-2 在 a Uint8Array中存储 254,在Uint8ClampedArray中存储 0。

每个类型化数组视图对象定义了以下两个属性:

  • BYTES_PER_ELEMENT
  • name

属性包含类型数组元素的大小,以字节为单位。它在视图类型中的值与表 0-1 中“以字节为单位的大小”栏中的值相同。name属性包含一个作为视图名称的字符串,它与表中的“类型化数组视图”列相同。例如,Int16Array.BYTES_PR_ELEMNET是 2,Int16Array.name"Int16Array"

所有类型化数组视图都提供了四个构造函数。下面是Int8Array的四个构造函数。您可以用其他类型化数组视图的名称替换名称Int8Array,以获得它们的构造函数:

  • Int8Array(arrayBuffer, byteOffset, elementCount)
  • Int8Array(length)
  • Int8Array(typedArray)
  • Int8Array(arrayObject)

第一个构造函数从一个ArrayBuffer创建一个类型化数组视图。您可以创建指定ArrayBuffer的完整或部分视图。如果byteOffsetelementCount未指定,它会创建一个完整的视图ArrayBuffer. byteOffset是缓冲区中从开始的字节偏移量,elementCount是将占用缓冲区的数组元素的数量。

第二个构造函数将类型化数组的length作为一个参数,创建一个适当大小的ArrayBuffer,并返回完整的ArrayBuffer的视图。

第三和第四个构造函数让你从另一个类型化数组和一个Array对象创建一个类型化数组;新类型化数组的内容从指定数组的内容初始化。创建一个适当大小的新ArrayBuffer来保存复制的内容。

以下代码显示了如何使用不同的构造函数创建Int8Array对象(字节数组):

// Create an ArrayBuffer of 8 bytes

var buffer = new ArrayBuffer(8);

// Create an Int8Array that is a full view of buffer

var fullView = new Int8Array(buffer);

// Create an Int8Array that is the first half view of buffer

var firstHalfView = new Int8Array(buffer, 0, 4);

// Create an Int8Array that is the copy of the firstHalfView array

var copiedView = new Int8Array(firstHalfView);

// Create an Int8Array using elements from an Array object

var ids = new Int8Array([10, 20, 30]);

所有类型化数组对象都具有以下属性:

  • length:数组中元素的个数
  • byteLength:数组的长度,以字节为单位
  • buffer:类型化数组使用的底层ArrayBuffer对象的引用
  • byteOffset:从其ArrayBuffer开始的偏移量,以字节为单位

一旦创建了类型化数组,就可以将其作为简单的数组对象使用。您可以使用括号符号和索引来设置和读取它的元素。如果设置的值不是类型化数组类型的类型,则该值会被适当地转换。例如,将 23.56 设置为一个Int8Array中的一个元素会将 23 设置为值。下面的代码演示如何读写类型化数组的内容:

// Create an Int8Array of 3 elements. Each element is an 8-bit sign integer.

var ids = new Int8Array(3);

// Populate the array

ids[0] = 10;

ids[1] = 20.89; // 20 will be stored

ids[2] = 140;   // -116 is stored as byte's range is -128 to 127.

// Read the elements

print("ids[0] = " + ids[0]);

print("ids[1] = " + ids[1]);

print("ids[2] = " + ids[2]);

ids[0] = 10

ids[1] = 20

ids[2] = -116

下面的代码从一个Array对象创建一个Int32Array,并读取所有元素:

// An Array object

var ids = [10, 20, 30];

// Create an Int32Array from ids

var typedIds = new Int32Array(ids);

// Read elements from typedids

for(var i = 0, len = typedIds.length; i < len; i++) {

print("typedIds[" + i + "] = " + typedIds[i]);

}

typedIds[0] = 10

typedIds[1] = 20

typedIds[2] = 30

也可以使用相同的ArrayBuffer来存储不同类型的值。回想一下,ArrayBuffer上的视图是打印出来的,而不是ArrayBuffer本身;它只包含原始的二进制数据。下面的代码创建了一个 8 字节的ArrayBuffer,在前 4 个字节中存储一个 32 位有符号整数,在后 4 个字节中存储一个 32 位有符号浮点数:

// Create an 8=byte buffer

var buffer = new ArrayBuffer(8);

// Create an Int32Array view for the first 4 bytes. byteOffset is 0

// and element count is 1

var id = new Int32Array(buffer, 0, 1);

// Create a Float32Array view for the second 4 bytes. The first 4 bytes

// will be used for integer value. byteOffset is 4 and element count is 1

var salary = new Float32Array(buffer, 4, 1);

// Use the Int32Array view to store an integer

id[0] = 1001;

// Use the Float32Array view to store a floating-point number

salary[0] = 129019.50;

// Read and print the two values using the two views

print("id = " + id[0]);

print("salary = " + salary[0]);

id = 1001

salary = 129019.5

当通过提供ArrayBuffer的长度来创建类型化数组时,ArrayBuffer的长度或大小必须是元素大小的倍数。例如,当您创建一个Int32Array时,其缓冲区的大小必须是 4 的倍数(32 位等于 4 字节)。以下代码将引发异常:

var buffer = new ArrayBuffer(15);

// Throws an exception because an element of Int32Array takes 4 bytes and

// 15, which is the buffer size for the view, is not a multiple of 4

var id = new Int32Array(buffer);

以下代码显示了如何使用slice()方法复制ArrayBuffer的一部分,并在复制的缓冲区上创建一个新视图:

// Create an ArrayBuffer of 4 bytes

var buffer = new ArrayBuffer(4);

// Create an Int8Array from buffer

var int8View1 = new Int8Array(buffer);

// Populate the array

int8View1[0] = 10;

int8View1[1] = 20;

int8View1[2] = 30;

int8View1[3] = 40;

print("In original buffer:")

for(var i = 0; i < int8View1.length; i++) {

print(int8View1[i]);

}

// Copy the last two bytes from buffer to buffer2

var buffer2 = buffer.slice(2, 4);

// Create an Int8Array from buffer2

var int8View2 = new Int8Array(buffer2);

print("In copied buffer:");

for(var i = 0; i < int8View2.length; i++) {

print(int8View2[i]);

}

In original buffer:

10

20

30

40

In copied buffer:

30

40

因为类型化数组是类似数组的对象,所以您可以在类型化数组上使用Array对象的大多数方法。下面的代码展示了如何使用join()方法连接Int32Array的元素:

// Create an Int32Array of 4 elements

var ids = new Int32Array(4);

// Populate the array

ids[0] = 101;

ids[1] = 102;

ids[2] = 103;

ids[3] = 104;

// Call the join() method of the Array.prototype object and print the result

var idsList = Array.prototype.join.call(ids);

print(idsList);

101,102,103,104
数据视图视图

DataView视图提供了从ArrayBuffer中读取和写入不同类型数据的底层接口。通常,当一个ArrayBuffer中的数据包含同一个ArrayBuffer的不同区域中不同类型的数据时,您会使用一个DataView。注意DataView不是一个类型化数组;它只是一个ArrayBuffer的视图。DataView构造函数的签名是:

DataView(arrayBuffer, byteOffset, byteLength)

它从byteOffset索引中引用byteLength字节,创建指定arrayBuffer的视图。如果byteLength未指定,则引用从byteOffset开始到结束的arrayBuffer。如果byteOffsetbyteLength都未指定,则引用入口arrayBuffer

与类型化数组视图类型不同,DataView对象可以使用混合数据类型值,因此它没有length属性。像类型化数组一样,它有bufferbyteOffsetbyteLength属性。它包含以下 getter 和 setter 方法,用于读取和写入不同类型的值:

  • getInt8(byteOffset)
  • getUint8(byteOffset)
  • getInt16(byteOffset, littleEndian)
  • getUint16(byteOffset, littleEndian)
  • getInt32(byteOffset, littleEndian)
  • getUint32(byteOffset, littleEndian)
  • getFloat32(byteOffset, littleEndian)
  • getFloat64(byteOffset, littleEndian)
  • setInt8(byteOffset, value)
  • setUint8(byteOffset, value)
  • setInt16(byteOffset, value, littleEndian)
  • setUint16(byteOffset, value, littleEndian)
  • setInt32(byteOffset, value, littleEndian)
  • setUint32(byteOffset, value, littleEndian)
  • setFloat32(byteOffset, value, littleEndian)
  • setFloat64(byteOffset, value, littleEndian)

多字节值类型的 getters 和 setters 有一个名为littleEndian的布尔型可选参数。它指定正在读取和设置的值是小端还是大端格式,如果未指定,则该值被假定为大端格式。如果您读取的数据来自不同的来源,并且具有不同的字节顺序,这将非常有用。下面的代码使用一个DataView从一个ArrayBuffer中读写一个 32 位有符号整数和一个 32 位浮点数:

// Create an ArrayBuffer of 8 bytes

var buffer = new ArrayBuffer(8);

// Create a DataView from the ArrayBuffer

var data = new DataView(buffer);

// Use the first 4 bytes to store a 32-bit signed integer

data.setInt32(0, 1001);

// Use the second 4 bytes to store a 32-bit floating-point number

data.setFloat32(4, 129019.50);

var id = data.getInt32(0);

var salary = data.getFloat32(4);

print("id = " + id);

print("salary = " + salary);

id = 1001

salary = 129019.5

使用列表、地图和集合

Nashorn 不提供内置对象来表示通用地图和集合。您可以将 Nashorn 中的任何对象用作键为字符串的地图。可以在 Nashorn 中创建对象来表示地图和集合。然而,这样做就像重新发明轮子一样。Java 编程语言提供了许多类型的集合,包括映射和集合。您可以直接在 Nashorn 中使用这些集合。关于如何在 Nashorn 脚本中使用 Java 类的更多细节,请参考Chapter 6。我将在这一节讨论在 Nashorn 中受到特殊对待的 Java ListMap

使用 Java 列表作为 Nashorn 数组

Nashorn 允许您将 Java List视为 Nashorn 数组来读取和更新List的元素。注意,它不允许您使用数组索引向List添加元素。您可以使用索引来访问和更新List的元素。Nashorn 为 Java List的实例添加了一个length属性,因此您可以将List视为一个类似数组的对象。清单 7-4 展示了一个在 Nashorn 脚本中使用 Java List的例子。

清单 7-4。在 Nashorn 脚本中将 java.util.List 作为数组对象进行访问和更新

// list.js

var ArrayList = Java.type("java.util.ArrayList");

var nameList = new ArrayList();

// Add few names using List.add() Java method

nameList.add("Lu");

nameList.add("Do");

nameList.add("Yu");

// Print the List

print("After adding names:");

for(var i = 0, len = nameList.size(); i < len; i++) {

print(nameList.get(i));

}

// Update names using array indexes

nameList[0] = "Su";

nameList[1] = "So";

nameList[2] = "Bo";

// The following statement will throw an IndexOutOfBoundsException because

// it is trying to add a new element using teh array syntax. You can only

// update an element, not add a new element, using the array syntax.

// nameList[3] = "An";

print("After updating names:");

for(var i = 0, len = nameList.length; i < len; i++) {

print(nameList[i]);

}

// Sort the list in natural order

nameList.sort(null);

// Use the Array.prototype.forEach() method to print the list

print("After sorting names:");

Array.prototype.forEach.call(nameList, function (value, index, data) {

print(value);

});

After adding names:

Lu

Do

Yu

After updating names:

Su

So

Bo

After sorting names:

Bo

So

Su

该示例执行几项操作:

  • 创建一个java.util.ArrayList的实例
  • 使用 Java List接口的add()方法向列表中添加三个元素
  • 使用List接口的size()get()方法打印元素
  • 使用用于访问数组元素的 Nashorn 语法更新元素并打印它们。它使用 Nashorn 属性length来获取列表的大小
  • 使用 Java List.sort()方法按照自然顺序对元素进行排序
  • 使用Array.prototype.forEach()方法打印排序后的元素

使用 Java 地图作为 Nashorn 对象

您已经看到 Nashorn 对象只是存储字符串-对象对的映射。Nashorn 允许使用 Java Map作为 Nashorn 对象,其中你可以使用Map中的键作为对象的属性。清单 7-5 显示了如何使用 Java Map作为 Nashorn 对象。

清单 7-5。使用 Java 地图作为 Nashorn 对象

// map.js

// Create a Map instance

var HashMap = Java.type("java.util.HashMap");

var map = new HashMap();

// Add key-value pairs to the map using Java methods

map.put("Li", "999-11-0001");

map.put("Su", "999-11-0002");

// You can treat the Map as an object and add key-value pairs

// as if you are adding proeprties to the object

map["Yu"] = "999-11-0003";

map["Do"] = "999-11-0004";

// Access values using the Java Map.get() method and the bracket notation

var liPhone = map.get("Li");  // Java way

var suPhone = map.get("Su");  // Java way

var yuPhone = map["Yu"];      // Nashorn way

var doPhone = map["Do"];      // Nashorn way

print("Li's Phone: " + liPhone);

print("su's Phone: " + suPhone);

print("Yu's Phone: " + yuPhone);

print("Do's Phone: " + doPhone);

Li's Phone: 999-11-0001

su's Phone: 999-11-0002

Yu's Phone: 999-11-0003

Do's Phone: 999-11-0004

使用 Java 数组

您可以在 Nashorn 脚本中使用 Java 数组。在 Rhino 和 Nashorn 中,用 JavaScript 创建 Java 数组的方式是不同的。在 Rhino 中,你需要使用java.lang.reflect.Array类的newInstance()静态方法创建一个 Java 数组,这种方法效率很低,也很有限。Nashorn 支持一种创建 Java 数组的新方法,我将很快讨论这种方法。Nashorn 也支持这种语法。以下代码显示了如何使用 Rhino 语法创建和访问 Java 数组:

// Create a java.lang.String array of 2 elements, populate it, and print the

// elements. In Rhino, you were able to use java.lang.String as the first

// argument; in Nashorn, you need to use java.lang.String.class instead.

var strArray = java.lang.reflect.Array.newInstance(java.lang.String.class, 2);

strArray[0] = "Hello";

strArray[1] = "Array";

for(var i = 0; i < strArray.length; i++) {

print(strArray[i]);

}

Hello

Array

要创建原始类型数组,如数组intdouble等,您需要将它们的TYPE常量用于它们对应的包装类,如下所示:

// Create an int array of 2 elements, populate it, and print the elements

var intArray = java.lang.reflect.Array.newInstance(java.lang.Integer.TYPE, 2);

intArray[0] = 100;

intArray[1] = 200;

for(var i = 0; i < intArray.length; i++) {

print(intArray[i]);

}

100

200

Nashorn 支持创建 Java 数组的新语法。首先,使用Java.type()方法创建适当的 Java 数组类型,然后使用熟悉的new操作符创建数组。下面的代码展示了如何在 Nashorn 中创建两个元素的String[]:

// Get the java.lang.String[] type

var StringArray = Java.type("java.lang.String[]");

// Create a String[] array of 2 elements

var strArray = new StringArray(2);

// Populate the array

strArray[0] = "Hello";

strArray[1] = "Array";

for(var i = 0; i < strArray.length; i++) {

print(strArray[i]);

}

Hello

Array

Nashorn 支持以同样的方式创建原始类型的数组。以下代码在 Nashorn 中创建两个元素的int[]:

// Get the int[] type

var IntArray = Java.type("int[]");

// Create a int[] array of 2 elements

var intArray = new IntArray(2);

intArray[0] = 100;

intArray[1] = 200;

for(var i = 0; i < intArray.length; i++) {

print(intArray[i]);

}

100

200

Tip

如果你想在 Nashorn 中创建多维 Java 数组,数组类型将会是你在 Java 中声明的数组。比如,Java.type("int[][]")会导入一个 Java int[][]数组类型;在导入的类型上使用 new 操作符将创建int[][]数组。

当需要 Java 数组时,可以使用 Nashorn 数组。Nashorn 将执行必要的转换。假设你有一个PrintArray类,如清单 7-6 所示,它包含一个接受String数组作为参数的print()方法。

清单 7-6。PrintArray 类

// PrintArray.java

package com.jdojo.script;

public class PrintArray {

public void print(String[] list) {

System.out.println("Inside print(String[] list):" + list.length);

for(String s : list) {

System.out.println(s);

}

}

}

以下脚本将一个 Nashorn 数组传递给PrintArray.print(String[])方法。Nashorn 负责将本机数组转换成一个String数组,如输出所示:

// Create a JavaScript array and populate it with three strings

var names = new Array();

names[0] = "Rhino";

names[1] = "Nashorn";

names[2] = "JRuby";

// Create an object of the PrintArray class

var PrintArray = Java.type("com.jdojo.script.PrintArray");

var pa = new PrintArray();

// Pass a JavaScript array to the PrintArray.print(String[] list) method

pa.print(names);

Inside print(String[] list):3

Rhino

Nashorn

JRuby

Nashorn 使用Java.to()Java.from()方法支持 Java 和 Nashorn 数组之间的数组类型转换。Java.to()方法将 Nashorn 数组类型转换为 Java 数组类型;它将 array 对象作为第一个参数,将目标 Java 数组类型作为第二个参数。目标数组类型可以指定为字符串或类型对象。下面的代码片段将 Nashorn 数组转换成 Java String[]:

// Create a JavaScript array and populate it with three integers

var personIds = [100, 200, 300];

// Convert the JavaScript integer array to Java String[]

var JavaStringArray = Java.to(personIds, "java.lang.String[]")

如果省略Java.to()函数中的第二个参数,Nashorn 数组将被转换为 Java Object[]

方法将 Java 数组类型转换成 Nashorn 数组。该方法将 Java 数组作为参数。以下代码片段显示了如何将 Java int[]转换为 JavaScript 数组:

// Create a Java int[]

var IntArray = Java.type("int[]");

var personIds = new IntArray(3);

personIds[0] = 100;

personIds[1] = 200;

personIds[2] = 300;

// Convert the Java int[] array to a Nashorn array

var jsArray = Java.from(personIds);

// Print the elements in the Nashorn array

for(var i = 0; i < jsArray.length; i++) {

print(jsArray[i]);

}

100

200

300

Tip

从一个 Nashorn 函数返回一个 Nashorn 数组到 Java 代码是可能的。您需要在 Java 代码中提取原生数组的元素,因此,您需要在 Java 中使用特定于 Nashorn 的类。不建议使用这种方法。您应该将 Nashorn 数组转换为 Java 数组,并从 Nashorn 函数返回 Java 数组,这样 Java 代码只处理 Java 类。

数组到 Java 集合的转换

只要有可能,Nashorn 就提供从 Nashorn 数组到 Java 数组、List s 和Map s 的自动转换。当 Nashorn 数组被转换成 Java Map时,数组元素的索引成为Map中的键,元素的值成为相应索引的值。考虑一个名为ArrayConversion的 Java 类,如清单 7-7 所示。它包含四个接受数组、List s 和Map s 作为参数的方法。

清单 7-7。一个名为 ArrayConversion 的 Java 类

// ArrayConversion.java

package com.jdojo.script;

import java.util.Arrays;

import java.util.List;

import java.util.Map;

public class ArrayConversion {

public static void printInts(int[] ids) {

System.out.print("Inside printInts():");

System.out.println(Arrays.toString(ids));

}

public static void printDoubles(double[] salaries) {

System.out.print("Inside printDoubles(double[]):");

System.out.println(Arrays.toString(salaries));

}

public static void printList(List<Integer> idsList) {

System.out.print("Inside printList():");

System.out.println(idsList);

}

public static void printMap(Map<?,?> phoneMap) {

System.out.println("Inside printMap():");

phoneMap.forEach((key, value) -> {

System.out.println("key = " + key + ", value = " + value);

});

}

}

考虑清单 7-8 所示的 Nashorn 脚本。它将 Nashorn 数组传递给期望数组、List s 和Map s 的 Java 方法。输出证明所有方法都被调用,并且 Nashorn 运行时提供自动转换。请注意,当 Nashorn 数组包含与 Java 类型不匹配的类型元素时,这些元素会根据 Nashorn 类型转换规则转换为适当的 Java 类型。例如,Nashorn 中的一个字符串被转换为 Java int中的 0。

清单 7-8。测试 ArrayConversion 类的 Nashorn 脚本

// arrayconversion.js

var ArrayConversion = Java.type("com.jdojo.script.ArrayConversion");

ArrayConversion.printInts([1, 2, 3]);

ArrayConversion.printInts([1, "Hello", 3]); // "hello" is converted to 0

// Non-integers will be converted to corresponding integers per Nashorn rules

// when a Nashorn array is converted to a Java array. true and false are

// converted to 1 and 0, and 10.3 is converted to 10.

ArrayConversion.printInts([1, true, false, 10.3]);

// Nashorn array to Java double[] conversion

ArrayConversion.printDoubles([10.89, "Hello", 3]);

// Nashorn array to Java List conversion

ArrayConversion.printList([10.89, "Hello", 3]);

// Nashorn array to Java Map conversion

ArrayConversion.printMap([10.89, "Hello", 3]);

Inside printInts():[1, 2, 3]

Inside printInts():[1, 0, 3]

Inside printInts():[1, 1, 0, 10]

Inside printDoubles(double[]):[10.89, NaN, 3.0]

Inside printList():[10.89, Hello, 3]

Inside printMap():

key = 0, value = 10.89

key = 1, value = Hello

key = 2, value = 3

有时候,Nashorn 不可能自动地将 Nashorn 数组转换成 Java 数组——特别是当 Java 方法重载时。考虑下面引发异常的代码:

var Arrays = Java.type("java.util.Arrays");

var str = Arrays.toString([0, 1, 2]); // Throws a java.lang.NoSuchMethodException

Arrays.toString()方法被重载,Nashorn 无法决定调用哪个版本的方法。在这种情况下,您必须显式地将 Nashorn 数组转换为特定的 Java 数组类型,或者选择要调用的特定 Java 方法。以下代码显示了这两种方法:

var Arrays = Java.type("java.util.Arrays");

// Explicitly convert Nashorn array to Java int[]

var str1 = Arrays.toString(Java.to([1, 2, 3], "int[]"));

// Explicitly choose the Arrays.toString(int[]) method to call

var str2 = Arrays["toString(int[])"]([1, 2, 3]);

摘要

Nashorn 中的数组是一个专门的对象,称为Array对象,用于表示值的有序集合。一个Array对象以一种特殊的方式对待某些属性名。如果一个属性名可以转换成一个介于 0 和 2 32 -2(包括 0 和 2)之间的整数,这样的属性名称为数组索引,属性称为元素。一个Array对象有一个称为length的特殊属性,代表数组中元素的数量。

可以使用数组文字或使用Array对象的构造函数来创建数组。数组文字是用括号括起来的逗号分隔的表达式列表,组成数组的元素。Nashorn 中的数组是可变长度的,可以是密集的,也可以是稀疏的。数组中的元素也可以是不同的类型。

您可以使用括号符号和数组元素的索引来访问数组元素。您可以使用delete操作符删除数组元素。您可以使用for循环、for..in循环和for..each..in循环来迭代数组元素。for..infor..each..in循环遍历数组的元素和非元素属性。Array.prototype对象包含一个forEach()方法,该方法只迭代数组元素,并为每个元素回调传递的函数。

Array对象包含几个有用的方法来处理数组元素。如果指定的object是一个数组,Array.isArray(object)静态方法返回true;否则,它返回false。其他方法如concat()join()slice()splice()sort()等等都在Array.prototype对象中。

类似数组的对象是一个 Nashorn 对象,它有一个length属性和属性名,这些属性名是有效的数组索引。String 对象和arguments对象是类数组对象的例子。Array对象中的大多数方法都是通用的,它们可以用在任何类似数组的对象上。

Nashorn 支持类型化数组。类型化数组是包含原始二进制数据的ArrayBuffer对象的类型化视图。不同类型的数组是可用的,比如Int8Array处理 8 位有符号整数,Int32Array处理 32 位有符号整数,等等。一旦创建了类型化数组,就可以像数组一样使用它来处理特定类型的值。DataView对象提供了一个低级接口来处理一个ArrayBuffer对象,其二进制数据可能包含不同类型的数值。

所有 Nashorn 对象都是地图。Nashorn 允许您以一种特殊的方式对待 Java Map s,允许您使用带键的括号符号作为索引来访问它们的值。Nashorn 让你像对待 Nashorn 数组一样对待 Java。您可以使用括号符号读取和更新 Java List的元素,其中元素的索引被视为数组中的索引。Nashorn 为 Java List对象创建了一个名为length的特殊属性,表示List中元素的数量。如果在 Nashorn 中需要任何其他类型的集合,可以使用 Java 中相应的集合。

Nashorn 允许您在脚本中使用 Java 数组。您需要使用Java.type()方法在脚本中导入 Java 数组类型。比如说。Java.type("int[]")返回一个代表 Java int[]的 Java 类型。一旦获得了 Java 数组类型,就需要使用new操作符来创建数组。Nashorn 还支持 Nashorn 到 Java 和 Java 到 Nashorn 的数组转换。Java.to()方法将 Nashorn 数组转换成 Java 数组,而Java.from()方法将 Java 数组转换成 Nashorn 数组。Nashorn 尝试执行从 Nashorn 数组到 Java 数组、List s 和Map s 的自动转换,前提是类型转换的选择是唯一的。在其他情况下,您将需要执行从 Nashorn 数组到集合的显式转换。