mathematica-J/Link用户指南

J/Link简介

欢迎访问J/Link,这是一个集成了Wolfram语言和Java的产品。J/Link允许您以一种完全透明的方式从Wolfram语言调用Java,它还允许您从Java程序使用和控制Wolfram语言内核。对于Wolfram语言用户来说,J/Link使整个现有和未来的Java类成为对Wolfram语言环境的自动扩展。对于Java程序员来说,J/Link将Wolfram语言转换为脚本shell,允许您一次一行一行地试验、构建和测试Java类。它还使Java成为编写使用Wolfram语言的计算服务的程序的理想语言。

J/Link最独特的特性是,它允许您将任意Java类加载到Wolfram语言中,然后直接从Wolfram语言创建Java对象、调用方法和访问字段。因此,您可以使用Wolfram语言来“编写”任意Java程序的功能,实际上就是用Wolfram语言编写Java程序。从本质上说,从Java可以做的任何事情,现在都可以从Wolfram语言完成,也许更容易,因为您是在一个真正的解释环境中工作的。

例如,您现在可以完全使用Wolfram语言代码创建一个基于java的用户界面。可以是用于长时间计算的简单进度条,也可以是指导用户完成计算的对话框或复杂的向导。这样的接口是完全可移植的,可以充分利用AWT、Swing或任何其他用户接口类库。

  • 从Wolfram语言调用Java方法
  • 编写使用Wolfram系统服务的Java程序
  • 为Wolfram语言创建替代前端
  • 为Wolfram语言程序创建对话框和其他弹出式用户界面元素
  • 在客户端或服务器上编写使用Wolfram语言内核的applet
  • 编写使Wolfram系统服务对HTTP客户机可用的servlet

J/Link是为最终用户和开发人员设计的。让Wolfram语言用户透明地调用任何Java方法的特性也让开发人员为Wolfram语言创建复杂的商业附加组件。想要为Wolfram语言编写自定义前端,或者使用Wolfram语言作为另一个程序的计算引擎的程序员会发现,使用带有J/Link的Java比使用C或c++中的传统Wolfram符号传输协议(WSTP)接口更容易。

最后,J/Link提供了完整的源代码。这包括用Wolfram语言、Java和c编写的组件。您可以检查代码以补充文档,获得您自己程序的提示,更好地理解如何使用高级特性,或者只是看看它是如何工作的。

在本手册中,假设您对Java和Wolfram语言有一定的了解。即使您不了解Java,也可以很容易地使用J/Link作为一种从Wolfram语言调用现有Java类的方法。这只需要学习哪些类和方法是可用的。Java语言的语法和复杂性是不相关的,因为您将编写Wolfram语言程序,而不是Java程序。

J/Link and WSTP

使这一切工作的底层粘合剂是WSTP(Wolfram Symbolic Transfer Protocol),这是Wolfram Research的协议,用于在Wolfram语言和其他程序之间来回发送数据和命令。在其核心上,J/Link是一个用于Java的WSTP开发人员工具包,尽管它远不止于此。实际上,J/Link的最佳特性是,对于大量的使用,它完全隐藏了WSTP,因此用户和程序员不需要了解它。这个类对应于所谓的“可安装”或“模板”WSTP程序,它们插入到Wolfram语言中并扩展其功能。对于所有类型的WSTP程序,J/Link提供了比传统的C WSTP编程接口更高级的功能层。这使得Java成为编写与Wolfram语言交互的程序最简单、最方便的语言。

从Wolfram语言调用Java

序言

J/Link为Wolfram语言用户提供了直接与来自Wolfram语言的任意Java类交互的能力。您可以在Wolfram语言中直接创建对象和调用方法。您不需要编写任何Java代码,也不需要以任何方式准备您想要使用的Java类。您也不需要知道关于Wolfram符号传输协议(WSTP)的任何信息。实际上,所有的Java都变成了对Wolfram语言的透明扩展,几乎就好像所有现有的和将来的Java类都是用Wolfram语言本身编写的。

这个工具被称为“可安装的Java”,因为它概括了Wolfram语言通过Install函数插入用其他语言编写的扩展的能力。稍后您将看到,与C或c++等语言相比,J/Link如何极大地简化了Java的这个过程。事实上,J/Link使这个过程完全消失了,这就是为什么Java成为了Wolfram语言的透明扩展。

尽管Java经常被称为解释语言,但这实际上是一种误用。使用Java必须编写一个完整的程序,编译它,然后执行它(有些环境允许您交互地执行Java代码行,但是这些是特殊的工具,类似的工具存在于传统语言(如C)中)。Wolfram语言用户的工作在一个真正的解释,交互式的环境,让他们尝试功能和构建和测试程序一次一行。J/Link为Java程序员带来了同样的生产环境。可以说,Wolfram语言成为了Java的脚本语言。

对于Wolfram语言用户来说,J/Link的“可安装Java”特性作为对Wolfram语言的扩展,打开了Java类不断扩展的世界;对于Java用户,它允许将非常强大和通用的Wolfram语言环境用作交互开发、试验和测试Java类的外壳。

加载J/Link包

第一步是加载J/Link包文件。

Needs["JLink`"]

启动Java运行时

InstallJava

下一步是启动Java运行时,并将其“安装”到Wolfram语言中。这个函数是InstallJava。

InstallJava[]	launch the Java runtime and prepare it for use from the Wolfram Language
ReinstallJava[]	quit and restart the Java runtime if it is already running
JavaLink[]	give the LinkObject that is being used to communicate with the Java runtime

启动Java运行时。

InstallJava[]

LinkObject["d:\\jdk122\\bin\\java", 5, 2]

在一个会话中可以多次调用InstallJava。在第一次调用之后,它什么都不做。因此,在您编写的任何程序中调用InstallJava是安全的,而不考虑用户是否已经调用了它。

InstallJava创建一个用于启动Java运行时(通常称为“Java”)的命令行,并为其指定一些初始参数。在极少数情况下,您需要控制命令行上的内容,因此InstallJava为此使用了许多选项。大多数用户不需要使用这些选项,实际上您应该避免使用它们。程序员不应该假设他们有能力控制Java运行时的启动,因为它可能已经在运行了。如果出于某种原因,您必须应用选项来控制Java运行时的启动,请使用ReinstallJava而不是InstallJava。

ClassPath->None	use the default class path of your Java runtime
ClassPath->"dirs"	use the specified directories and jar files
CommandLine->"cmd"	use the specified command line to launch the Java runtime, instead of "java"

InstallJava选项。

控制用于启动Java的命令

InstallJava和ReinstallJava的一个重要选项是命令行。这指定用于启动Java的命令行的第一部分。此选项的一种用法是,如果您的系统上安装了多个Java运行时,并且您想调用一个特定的运行时。

ReinstallJava[CommandLine -> "d:\\full\\path\\to\\java.exe"]

默认情况下,InstallJava将启动与Mathematica 4.2及以后版本绑定的Java运行时。如果您有较早版本的Wolfram语言,那么在大多数系统上将使用的默认命令行是java。如果java可执行文件不在您的系统路径上,您可以使用InstallJava指向它。此选项的另一个用途是为Java指定其他选项没有涉及的参数。下面是一个指定详细垃圾收集的示例,并定义了一个名为foo的属性以拥有值栏。

ReinstallJava[CommandLine -> "/path/to/java -verbosegc -Dfoo=bar"]

重写类路径

类路径是Java运行时在其中查找类的目录集。当您从系统的命令行启动Java程序时,Java使用的类路径包括一些默认位置和CLASSPATH环境变量中指定的任何位置(如果存在的话)。但是,如果使用-classpath命令行选项指定一组位置,则忽略CLASSPATH环境变量。InstallJava和ReinstallJava的类路径选项的工作方式相同。如果保持默认值为Automatic,那么J/Link将在其类搜索路径中包含CLASSPATH环境变量的内容。如果将其设置为None或字符串,则不会使用类路径的内容。如果将其设置为字符串,请使用与设置CLASSPATH环境变量相同的语法,这在Windows和Linux/OSX中是不同的。

ReinstallJava[
 ClassPath -> "c:\\my\\java\\dir;d:\\MyJavaStuff.jar"]  (* Windows *)
ReinstallJava[
 ClassPath -> "/my/java/dir:/home/me/MyJavaStuff.jar"]  (* OSX/Linux *)

Link有自己的控制类搜索路径的机制,非常灵活。J/Link不仅自动搜索Wolfram语言应用程序目录中的类,而且还允许在Java运行时动态添加新的搜索位置。这意味着在Java首次启动时使用ClassPath选项配置类路径并不是很重要。ClassPath选项的一个设置有时很有用,那就是None,它可以防止J/Link从ClassPath的内容中找到任何类。如果在开发目录中有某个类的实验版本,并且希望确保J/Link使用的是该版本,而不是类路径中出现的旧版本,那么您可能希望这样做。“Java类路径”完整地介绍了J/Link如何搜索类,以及如何将位置添加到这个搜索路径。

加载类

LoadJavaClass

LoadJavaClass["classname"]	load the specified class into Java and the Wolfram Language
LoadClass["classname"]	deprecated name from earlier versions of J/Link; use LoadJavaClass instead

加载类。

要在Wolfram语言中使用Java类,必须首先将其加载到Java运行时中,并且必须在Wolfram语言中设置某些定义。这是通过LoadJavaClass函数完成的。LoadJavaClass接受一个字符串,指定类的完全限定名(即带有所有句点的完整层次结构名)。

urlClass = LoadJavaClass["java.net.URL"]

JLink`JavaClass["java.net.URL", 0]

返回值是一个头为JavaClass的表达式。这个JavaClass表达式可以在J/Link中的许多地方使用,因此您可能希望将它分配给一个变量,就像这里所做的那样。实际上,在J/Link中需要将类指定为参数的任何地方,您都可以使用JavaClass表达式、作为字符串的完全限定类名或类的对象。注意,您不能通过简单地键入一个有效的JavaClass表达式,它必须由LoadJavaClass返回。

加载类后,可以调用类中的静态方法,创建类的对象,并调用这些对象的方法和访问字段。您可以使用类的任何公共构造函数、方法或字段。

StaticsVisible->True	make static methods and fields accessible by just their names, not in a special context
AllowShortContext->False	make static methods and fields accessible only in their fully qualified class context
UseTypeChecking->False	suppress the type checking that is normally inserted in definitions for calls into Java

LoadJavaClass选项。

“Java类路径”讨论了J/Link查找类的方式和位置的细节。J/Link将能够在类路径、特殊的Java extensions目录以及用户甚至在J/Link运行时都可以控制的一组额外目录中找到类。

何时调用LoadJavaClass

通常情况下,您不需要显式地用LoadJavaClass装入类。如后面所述,当使用JavaNew创建Java对象时,可以将类名作为字符串提供。如果类还没有加载,则LoadJavaClass将由JavaNew在内部调用。实际上,任何时候Java对象返回到Wolfram语言,如果需要,它的类就会自动加载。这似乎意味着没有什么理由使用LoadJavaClass。您想要或需要显式地使用LoadJavaClass有许多原因:

  • 您需要调用类的静态方法,而您将不会或尚未创建该类的对象。在调用类的任何静态方法之前,必须先加载类。
  • 您需要使用LoadJavaClass其中一个选项。当LoadJavaClassJavaNew内部调用时,它是使用默认选项设置调用的。
  • 您希望看到在定义良好的时间报告与装入类相关的错误。
  • 您希望控制用户在加载类时所经历的初始延迟。如果一个类或它的父类非常大,加载它可能需要几秒钟(尽管很少需要那么长时间)。您可能希望避免在用户希望非常快的函数中出现神秘的延迟。
  • 您希望保留LoadJavaClass返回的JavaClass表达式,以便在其他函数中使用它。尽管所有接受JavaClass的函数也可以接受类名字符串,但出于可读性的目的,您可能更喜欢使用已命名的JavaClass变量。它也比使用字符串稍微快一些,但除非在循环中多次使用它,否则不会察觉到这一点。
  • 您会觉得它使您的代码更加具有自注释性。

在J/Link中加载类的操作只在J/Link会话中执行一次(会话是InstallJavaUninstallJava之间的时间段)。您可以在给定的类上任意多次调用LoadJavaClass,在第一个调用之后的每个调用都立即返回JavaClass表达式,而不做任何工作。这很重要,因为这意味着您永远不必担心一个类是否已经被加载了。如果您不确定,请调用LoadJavaClass

为广大用户编写代码的开发人员应该总是在每个需要LoadJavaClass的函数中调用它们所需要的任何类。当您的包被读入时,不适合在包的代码体中调用LoadJavaClass,因为用户可能会在您的包被读入后退出并重新启动Java运行时(即UninstallJavaInstallJava)。为了安全起见,每个使用J/Link的用户级函数都应该调用InstallJavaLoadJavaClass(如果需要LoadJavaClass;见下面)。如果不需要,这两个调用执行得非常快。

如前所述,加载一个类在某些情况下可能需要几秒钟。当加载一个类时,它的所有超类都将连续加载,沿着继承层次结构向上遍历。因为给定的类实际上只加载一次,如果您加载另一个类,该类与先前加载的类共享一些相同的超类,那么这些超类将不必再次加载。这意味着,如果任何共享超类都很大,那么加载第二个类将比第一个类快得多。这方面的一个例子是加载java.awt package中的类。类java.awt.Component非常大,所以第一次加载从它继承的类比如java.awt.Button时,会有明显的延迟。随后加载从Component派生的其他类将会快得多。

静态成员的上下文和可见性

LoadJavaClass有两个选项,可以让您控制静态方法和字段的命名和可见性。要理解这些选项,您需要理解它们所帮助解决的问题。由于还没有讨论如何调用Java方法,所以这个解释有点超前。加载类时,将使用Wolfram语言创建定义,允许您调用该类对象的方法和访问该类的字段。静态成员与非静态成员的处理方式完全不同。对于非静态成员,这些问题都不会出现,因此本节只讨论静态成员。假设您有一个名为com.foobar.MyClass的类,它包含一个名为foo的静态方法。当您加载这个类时,必须为foo设置一个定义,以便可以按名称调用它,比如foo[args]。问题变成:你希望在什么上下文中定义符号foo,你希望这个上下文中是可见的(例如,在$ContextPath上)?

J/Link总是在一个反映其完全限定类名的上下文中为foo创建一个定义:com’foobar’MyClass’foo。这样做是为了避免与可能出现在其他上下文中的名为foo的符号发生冲突。但是,你可能会发现每次输入完整的上下文名称来调用foo是很笨拙的,就像在com’foobar’MyClass’foo[args]中一样。AllowShortContext->True选项(这是默认设置)将导致J/Link也使foo的定义可以在一个缩短的上下文中访问,这个上下文中只包含类名,没有层次结构包名前缀。在这个示例中,这意味着您可以简单地通过MyClass’foo[args]来调用foo。如果您需要避免使用短上下文,因为在您的Wolfram系统会话中已经有一个同名的上下文,那么您可以使用AllowShortContext->False。这就迫使所有的名字只能放在“深”的上下文中。请注意,即使AllowShortContext->True,静态的名称也会放在深层上下文中,因此如果愿意,您总是可以使用深层上下文来引用符号。

因此,AllowShortContext允许您控制定义符号名称的上下文。另一个选项StaticsVisible控制这个上下文是否可见(放在$ContextPath上)。默认值是StaticsVisible->False,所以在引用符号时必须使用上下文名称,如MyClass’foo[args]。如果StaticsVisible->TrueMyClass'将被放在$ContextPath上,所以你可以只写foo[args]。有默认是True的有点危险——每次加载一个类时,可能会突然创建大量的名称,并使其在Wolfram系统会话中可见,如果已经存在相同名称的符号,就可能出现各种“隐藏”问题。这个问题在Java中尤其严重,因为Java中的方法和字段名称通常以小写字母开头,这也是Wolfram语言中用户定义符号的惯例。一些Java类用x、y、width等名称定义静态方法和字段,因此很可能出现隐藏错误(有关上下文和隐藏问题的讨论,请参阅“Contexts”)。由于这些原因,StaticsVisible->True只推荐用于您已经编写的类,或您熟悉其内容的类。在这种情况下,它可以节省一些输入,提高代码的可读性,并防止忘记输入包前缀这种太容易的错误。一个典型的例子是实现古老的“addtwo”WSTP示例程序。在Java中,它可能是这样的:

public class AddTwo {
	public static int addtwo(int i, int j) {return i + j;}
}

使用默认的StaticsVisible->False,您将不得不调用addtwo作为addtwo’addtwo[3,4]。设置StaticsVisible->True允许您编写更明显的addtwo[3,4]。

请注意,这些选项仅适用于静态方法和字段。正如后面所讨论的,非静态的处理方式可以使上下文和可见性问题完全消失。

内部类

内部类是定义在另一个公共类中的公共类。例如,类javax.swing.Box有一个名为Filler的内部类。在Java程序中引用填充类时,通常使用外部类名,后跟句点,然后是内部类名。

Box.Filler f = new Box.Filler(...);

您可以使用带有J/Link的内部类,但是您需要使用类的真正内部名称,它有一个$,而不是句点,分隔外部和内部类名。

filler = JavaNew["java.swing.Box$Filler", \[Ellipsis]]

如果您查看Java编译器生成的类文件,您将看到内部类的这些以$分隔的类名。

Java和Wolfram语言之间的类型转换

在遇到创建Java对象和调用方法的操作之前,应该检查Wolfram语言和Java之间的类型映射。当Java方法向Wolfram语言返回结果时,该结果将自动转换为Wolfram语言表达式。例如,Java整数类型(例如byte、short、int等)被转换为Wolfram语言的整数,Java实数类型(float、double)被转换为Wolfram语言的实数。下表显示了完整的转换集。这些转换以两种方式工作——例如,当将一个Wolfram语言整数发送到需要字节值的Java方法时,该整数将自动转换为Java字节。

Java类型Wolfram语言类型
byte , char , short , int , longInteger
Byte , Character , Short , Integer , Long , BigIntegerInteger
float , doubleReal
Float , Double , BigDecimalReal
booleanTrue or False
StringString
arrayList
controlled by user (see “Complex Numbers”)Complex
ObjectJavaObject
Exprany expression
nullNull

Java和Wolfram语言中的对应类型。

Java数组被映射到适当深度的Wolfram语言列表。因此,当您调用一个采用double[]的方法时,您可能会传递它{1.0,2.0,N[Pi],1.23}。类似地,返回两个深度整数数组(即int[][])的方法可能会向Wolfram语言返回表达式{{1,2,3},{5,3,1}}

在大多数情况下,J/Link将允许您向一个方法提供一个Wolfram语言整数,该方法的类型是一个实类型(浮点型或双精度型)。类似地,使用double[]的方法可以传递一组混合整数和实数。唯一不能这样做的情况是,一个方法在同一个参数槽中只有实数和整数类型的两个签名不同。例如,考虑一个具有以下方法的类:

public void foo(byte b, Object obj);
public void foo(float f, Object obj);
public void bar(float f, Object obj);

J/Link将为方法foo——创建两个Wolfram语言定义,一个需要一个整数作为第一个参数并调用第一个签名,另一个需要一个实数作为第一个参数并调用第二个签名。为方法栏创建的定义将接受一个整数或一个实数作为第一个参数。换句话说,J/Link将自动将整数转换为实数,除非在这种转换使调用给定方法的哪个签名不明确的情况下。但是,这并不是严格正确的,因为J/Link并没有尽可能努力地确定在每个参数位置上是否存在实数和整数的不确定性。一个位置上的模糊性将导致J/Link放弃,并要求在所有参数位置上进行精确的类型匹配。这听起来有点混乱,但是您会发现,在大多数情况下,J/Link允许您将整数或带有整数的列表传递给分别以实数或实数数组作为参数的方法。如果没有,则调用将失败并伴有错误消息,您将不得不使用Wolfram语言的N函数显式地将所有整数转换为实数。

创建对象

要实例化Java对象,请使用JavaNew函数。JavaNew的第一个参数是对象的类,指定为从LoadJavaClass返回的JavaClass表达式,或者指定为给出完全限定类名的字符串(例如,拥有带有所有句点的完整包前缀)。如果您希望为对象的构造函数提供任何参数,它们将以序列的形式跟随在类之后。

JavaNew[cls,Subscript[arg, 1],\[Ellipsis]]	construct a new object of the specified class and return it to the Wolfram Language
JavaNew["classname",Subscript[arg, 1],\[Ellipsis]]	construct a new object of the specified class and return it to the Wolfram Language

构建Java对象。

例如,这将创建一个新的框架。

frm = JavaNew["java.awt.Frame"]

JLink`Objects`JavaObject100369839357953

JavaNew的返回值是一个奇怪的表达式,看起来头好像是JavaObject,只是它被括在尖括号中。尖括号用于表示表达式的显示形式与其内部表示形式有很大不同。这些表达式将被称为JavaObject表达式。JavaObject表达式的显示方式显示了它们的类名,但是您应该将它们视为不透明的,这意味着您不能将它们分开或深入它们的内部。只能在采用JavaObject表达式的J/Link函数中使用它们。例如,如果obj是一个JavaObject,你不能使用First[obj]来获得它的类名。相反,有一个用于此目的的J/Link函数ClassName[obj]。

JavaNew调用一个适合传递进来的参数类型的Java构造函数,然后将对象引用返回给Wolfram语言。这就是您应该将JavaObject表达式——视为对Java对象的引用的方式,与Java语言本身中的对象引用非常相似。不管您构造的是什么类型的对象,返回给Wolfram语言的内容都不大。特别是,对象的数据(即它的字段)不会被发送回Wolfram语言。实际的对象保留在Java端,而Wolfram语言获得对它的引用。

Frame类有第二个构造函数,它接受字符串形式的标题。下面是调用该构造函数的方法。

frm = JavaNew["java.awt.Frame", "My Example Frame"]

JLink`Objects`JavaObject76386741977089

注意,仅仅构造一个框架并不会导致它出现。这需要一个单独的步骤(调用框架的show或setVisible方法可以工作,但是您将在后面看到,J/Link提供了一个特殊的函数JavaShow,用于使Java窗口出现并出现在前台)。

前面的示例通过将类名指定为字符串来指定类。您还可以使用JavaClass表达式,这是LoadJavaClass返回的特殊表达式,它以一种特别有效的方式标识类。当您将类名指定为字符串时,如果还没有加载类,则会加载它。

frameClass = LoadJavaClass["java.awt.Frame"];
frm = JavaNew[frameClass, "My Example Frame"];

在Wolfram语言中,JavaNew并不是获取对Java对象引用的唯一方法。许多方法和字段都返回对象,当您调用这样的方法时,将创建一个JavaObject表达式。使用这些对象的方式与使用JavaNew显式构造的对象相同。

此时,您可能想知道引用计数以及返回到Wolfram语言的对象是如何清理的。这些问题在“Wolfram语言中的对象引用”中进行了讨论。

J/Link还有另外两个用于创建Java对象的函数,称为MakeJavaObjectMakeJavaExpr。这些专门的函数在“MakeJavaObjectMakeJavaExpr”一节中进行了描述。

调用方法和访问字段

语法

用于调用Java方法和访问字段的Wolfram语言语法非常类似于Java语法。下面的框比较了Wolfram语言和Java调用构造函数、方法、字段、静态方法和静态字段的方法。您可以看到,使用Java的Wolfram语言程序的编写方式几乎与Java程序完全相同,只是Wolfram语言使用[]而不是()作为参数,并且Wolfram语言使用@而不是Java的.(dot)作为“成员访问”操作符。

一个例外是,对于静态方法,Wolfram语言使用上下文标记’来代替Java的点。这与Java的用法相似,Java在这种情况下使用点实际上是作为范围解析操作符(如c++中的::)。尽管Wolfram语言不使用这个术语,但它的范围解析操作符是上下文标记。Java的分层包名称直接映射到Wolfram语言的分层上下文。

JavaWolfram语言
constructorsMyClass obj=new MyClass(args);obj=JavaNew[“MyClass”,args];
methodsobj.methodName(args);obj@methodName[args]
fieldsobj.fieldName=1;value=obj.fieldName;obj@fieldName=1;value=obj@fieldName;
static methodsMyClass.staticMethod(args);MyClass`staticMethod[args];
static fieldsMyClass.staticField=1;value=MyClass.staticField;MyClassstaticField=1;value=MyClassstaticField;

Java和Wolfram语言的语法比较。

您可能已经熟悉了@作为将函数应用于参数的Wolfram语言操作符:f@x相当于更常用的f[x]。J/Link不会因为一些特殊的操作而篡夺@——它只是一个稍微伪装的普通函数应用程序。这意味着您根本不必使用@。下面是调用方法的等效方法。

obj@method[args];
obj[method[args]];

第一种形式保留了Java语法到Wolfram语言语法的自然映射,本教程将专门使用它。

当您调用方法或字段并返回结果时,J/Link会根据“Java和Wolfram语言类型转换”中的表自动将参数和结果与它们的Wolfram语言表示进行转换。

方法调用可以在Wolfram语言中绑定,就像在Java中一样。例如,如果meth1返回一个Java对象,您可以用Java obj.meth1().meth2()编写。在Wolfram语言中,这变成了obj@meth1[]@meth2[]。注意这里有一个明显的问题:Wolfram语言的@操作符组在右边,而Java的点组在左边。换句话说,Java中的obj.meth1().meth2()实际上是(obj.meth1()).meth2(),而Wolfram语言中的obj@meth1[]@meth2[]通常是obj@(meth1[]@meth2[])。之所以使用单词“normal”,是因为J/Link会自动将链接调用分组到左侧,就像Java一样。它通过为JavaObject表达式定义规则来实现这一点,而不是通过改变@操作符的属性,这样@的全局行为就不会受到影响。这种链接行为只适用于方法调用,而不适用于字段。您不能执行以下操作。

(* These are incorrect. You cannot chain calls after a field access. *)
x = obj@field@method[args];
x = obj@field1@field2;

你必须把它们分成两行。例如,上面的第二行将变成以下内容。

temp = obj@field1;
x = temp@field2;

在Java中,像其他面向对象语言一样,方法和字段名由调用它们的对象限定作用域。换句话说,当您编写obj.meth()时,Java知道您正在调用驻留在对象类中的名为meth的方法,即使在其他类中可能存在名为meth的其他方法。J/Link为Wolfram语言符号保留了这个范围,这样就不会与同名的现有符号发生冲突。当您编写obj@meth[]时,与system中任何名为meth的符号没有冲突。Wolfram语言在这个调用的求值中使用的符号meth就是J/Link为这个类设置的符号。下面是一个使用字段的示例。首先,创建一个Point对象。

pt = JavaNew["java.awt.Point"]
JLink`Objects`JavaObject1

Point类拥有名为x和y的字段,它们保存着它的坐标。然而,用户的会话中也可能包含名为x或y的符号。你建立了一个x的定义它会告诉你什么时候求值。

x := Print["gotcha"]

现在为名为x的字段设置一个值(在Java中会写成pt.x = 42)。

pt@x = 42;

您将注意到没有打印“gotcha”。在具有Print定义的全局上下文中的符号x与在求值这行代码时使用的符号x之间不存在冲突。J/Link保护@右边的方法和字段的名称,这样它们就不会与可视上下文中可能存在的任何对这些符号的定义冲突或依赖。下面是一个方法示例,它以不同的方式演示了这个问题。

frm = JavaNew["java.awt.Frame"];
frm@show[]

尽管在这里创建了一个新的show符号,但是J/Link使用的show是驻留在java’awt’Frame上下文中的那个,它已经为它设置了必要的定义。

总之,对于非静态方法和字段,您永远不必担心名称冲突和阴影,无论您处于什么上下文中,也无论当前$ContextPath是什么。然而,对于静态成员来说,情况并非如此。静态方法和字段是按其全名调用的,没有对象引用,因此前面没有对象来限定名称的范围。下面是一个调用Java垃圾收集器的静态方法调用的简单示例。您需要在调用静态方法之前调用LoadJavaClass,以确保类已经加载。

runtime = Runtime`getRuntime[];
runtime@gc[];

Java名称中的下划线

Java名称中可以包含在Wolfram语言符号中不合法的字符。唯一常见的是下划线。J/Link将类、方法和字段名中的下划线映射为“U”。注意,这种映射只在名称以符号形式而不是字符串形式使用时使用。例如,假设您有一个名为com.acme.My_Class的类。当您将这个类名作为字符串引用时,您可以使用下划线。

LoadJavaClass["com.acme.My_Class"];
JavaNew["com.acme.My_Class"];

但是当您在这样的类中调用静态方法时,层次上下文名称是符号的,因此必须将下划线转换为U。

com`acme`MyUClass`staticMethod[];
MyUClass`staticMethod[];

同样的规则也适用于方法和字段名。许多Java字段名中都有下划线,例如java.awt.frame.TOP_ALIGNMENT。要在代码中引用此方法,请使用U。

LoadJavaClass["java.awt.Frame"];
Frame`TOPUALIGNMENT

0.

在提供字符串的情况下,保留下划线。

Fields["java.awt.Frame", "*_ALIGNMENT"]

{
 {"static final float BOTTOM_ALIGNMENT"},
 {"static final float CENTER_ALIGNMENT"},
 {"static final float LEFT_ALIGNMENT"},
 {"static final float RIGHT_ALIGNMENT"},
 {"static final float TOP_ALIGNMENT"}
}

获取关于类和对象的信息

J/Link有一些有用的函数,向您展示给定类或对象可用的构造函数、方法和字段。

Constructors[cls]	return a table of the public constructors and their arguments
Constructors[obj]	constructors for this object's class
Methods[cls]	return a table of the public methods and their arguments
Methods[cls,"pat"]	show only methods whose names match the string pattern pat
Methods[obj]	show methods for this object's class
Fields[cls]	return a table of the public fields
Fields[cls,"pat"]	show only fields whose names match the string pattern pat
Fields[obj]	show fields for this object's class
ClassName[cls]	return, as a string, the name of the class represented by cls
ClassName[obj]	return, as a string, the name of this object's class
GetClass[obj]	return the JavaClass representing this object's class
ParentClass[obj]	return the JavaClass representing this object's parent class
InstanceOf[obj,cls]	return True if this object is an instance of cls, False otherwise
JavaObjectQ[expr]	return True if expr is a valid reference to a Java object, False otherwise

获取关于类和对象的信息。

可以将对象或类赋予构造函数、方法和字段。类可以通过它的全称指定为字符串,也可以通过JavaClass表达式来指定:

urlClass = LoadJavaClass["java.net.URL"];
urlObject = JavaNew["java.net.URL", "http://www.wolfram.com"];
(* The next three lines are equivalent *)
Methods[urlClass]
Methods[urlObject]
Methods["java.net.URL"]

通过删除Java关键字public、final(只删除方法而不是字段)、synchronized、native、volatile和transient,这些函数返回的声明得到了简化。声明将始终是公开的,其他修饰符可能与通过J/Link使用无关。

MethodsFields采用一个选项Inherited,该选项指定是包含从超类和接口继承的成员,还是只显示在类本身中声明的成员。默认值是Inherited->True

Inherited->False	show only members that are declared in the class itself, not inherited from superclasses or interfaces

方法和字段的选项。

还有一些附加的函数提供关于对象和类的信息。这些函数是ClassNameGetClassParentClassInstanceOfJavaObjectQ。它们在很大程度上是不言自明的。函数的InstanceOf类似于Java语言的instanceof操作符。JavaObjectQ对于编写只匹配有效Java对象的模式非常有用。

Stringify[obj_?JavaObjectQ] := obj[toString[]]

当且仅当其参数是对Java对象的有效引用,或者它是映射到Java的空对象的符号Null时,JavaObjectQ返回True

退出或重新启动Java

当您在Wolfram系统会话中使用完Java后,您可以通过调用UninstallJava[]退出Java运行时。

UninstallJava[]	quit the Java runtime
ReinstallJava[]	restart the Java runtime

退出Java运行时。

除了退出Java之外,UninstallJava还清除在装入类时用Wolfram语言创建的许多符号和定义。当Java退出时,所有未完成的JavaObject表达式都将无效。它们将不再满足JavaObjectQ,并且它们将以原始符号的形式出现,如JLink 'Objects 'JavaObject12345678,而不是<<JavaObject[classname]>>。

大多数用户没有理由调用UninstallJava。您应该将Java运行时视为Wolfram语言不可分割的一部分——启动它,然后让它运行。所有使用J/Link的代码共享相同的Java运行时,并且可能有您正在使用的包在您甚至不知道的情况下使用Java。关闭Java可能会损害它们的功能。编写包的开发人员不应该在包中调用UninstallJava。您不能假设当您的应用程序使用了J/Link后,您的用户也就不再使用它了。

需要停止和重新启动Java的唯一常见原因是在积极开发希望从Wolfram语言调用的Java类时。类加载到Java运行时后,就不能卸载它。如果您想修改和重新编译您的类,您需要重新启动Java来重新加载修改后的版本。即使在这种情况下,也不会调用UninstallJava。相反,您将调用ReinstallJava,它只是简单地调用UninstallJava,然后再次调用InstallJava

版本信息

J/Link提供了三个提供版本信息的符号。这些符号提供的信息类型与Wolfram语言本身中的对应符号相同,只是它们位于JLink’information’上下文中,而不是在$ContextPath上,因此必须使用它们的全名来指定它们。

JLink`Information`$Version	a string giving full version information
JLink`Information`$VersionNumber	a real number giving the current version number
JLink`Information`$ReleaseNumber	an integer giving the release number (the last digit in a full x.x.x version specification)
ShowJavaConsole[]	the console window will show version information for the Java runtime and the J/Link Java component

J/Link版本信息。

JLink`Information`$Version
"J/Link Version 4.0.1"
JLink`Information`$VersionNumber
4.9
JLink`Information`$ReleaseNumber
1

在“Java控制台窗口”中描述的ShowJavaConsole[]函数也将显示一些有用的版本信息。它显示了所使用的Java运行时的版本以及用Java编写的J/Link部分的版本。J/Link Java组件的版本应该与J/Link Wolfram语言组件的版本匹配。

控制类路径:J/Link如何找到类

Java类路径

类路径告诉Java运行时、编译器和其他工具在哪里可以找到第三方和用户定义的类——类,这些类不是Java“扩展”或Java平台本身的一部分。类路径一直是Java用户和程序员混淆的一个来源。

Java可以找到属于标准Java平台的类(所谓的“引导”类)、使用所谓的“扩展”机制的类和类路径上的类,类路径由CLASSPATH环境变量或启动Java时由命令行选项控制。J/Link可以加载和使用Java运行时通过这些普通机制可以找到的任何类。此外,J/Link可以查找在一组额外位置(在启动时在类路径上指定的位置之外)中的类、资源和本机库。这组额外的位置可以在Java运行时添加到其中。

J/Link提供了两种方法来改变Java用于查找类的搜索路径。第一种方法是通过ClassPath选项ReinstallJava。第二种方法优于在启动时修改类路径,即向J/Link搜索的特殊位置集添加新的目录和jar文件。这两种方法将在接下来的两小节中进行描述。

重写启动类路径

要想通过标准的Java类路径访问一个类,必须采用以下方法之一:

  • 它位于.zip或.jar文件中,该文件本身在类路径中命名。
  • 它是一个松散的类文件,位于类路径上的目录下的适当嵌套目录中。

“适当嵌套”意味着类文件必须位于反映类的完整包名的层次结构目录中。例如,假设目录c:\MyClasses位于类路径上。如果您有一个不在包中的类(在代码的开头没有包语句),那么它的类文件应该直接放在c:\MyClasses中。如果包com.acme中有一个类。,它的类文件需要在目录c:\MyClasses\com\acme\stuff中。注意,jar和zip文件必须在类路径上显式地命名,不能直接将它们放入本身在类路径上命名的目录中。目录问题与jar和zip文件无关,这意味着无论jar文件中类的层次结构如何,您只需在类路径上命名jar文件本身,就可以找到其中的所有类。

如果希望为不属于标准Java平台或扩展的类指定路径,可以使用ClassPath选项ReinstallJava。为ClassPath选项提供的值是一个字符串,用于命名所需的目录和zip或jar文件。这个字符串是平台相关的;路径是以您的平台的本机样式指定的,在Linux上分隔符是冒号,在Windows上是分号。下面是一些典型的规范。

ReinstallJava[
 ClassPath -> 
  "c:\\MyJavaDir\\MyPackage.jar;c:\\MyJavaDir"]  (* Windows *)
ReinstallJava[
 ClassPath -> 
  "~/MyJavaDir/MyPackage.jar:~/MyJavaDir"]              (* Linux/OSX *)

ClassPath的默认设置是Automatic,这意味着使用ClassPath环境变量的值。如果您将ClassPath设置为其他内容,那么J/Link将忽略ClassPath环境变量——它将无法找到这些类。换句话说,如果使用ClassPath规范,就会丢失CLASSPATH环境变量。这类似于Java运行时和编译器的-classpath命令行选项的行为,如果您熟悉这些工具的话。

建议用户避免ClassPath选项。如果您需要ClassPath选项提供的动态控制,那么应该使用更强大、更方便的AddToClassPath机制,下一节将对此进行描述。使用ClassPath选项的最常见原因是,如果您特别想防止使用ClassPath环境变量的内容。为此,设置ClassPath->None

动态修改类路径

标准Java类路径的一个不方便之处是,在Java运行时启动后,它不能被更改。J/Link有自己的类装入器,它在一组超出标准Java类路径的特殊位置进行搜索。这为J/Link提供了一种极其强大和灵活的查找类的方法。要向这个额外的集合添加位置,请使用AddToClassPath函数。

AddToClassPath["location",...]	add the specified directories or jar files to J/Link's class search path

向搜索路径添加类。

Java启动后,您可以随时调用AddToClassPath,它将立即生效。这个额外的类搜索路径的一个方便特性是,如果您添加了一个目录,那么该目录中的任何jar或zip文件都将被搜索。这意味着您不必像使用标准Java类路径那样,单独命名jar文件。对于松散类文件,嵌套规则与类路径相同,这意味着如果一个类位于包com.acme.stuff中。您调用了AddToClassPath[“d:\myClasses”],然后需要将类文件放入d:\myClasses\com\acme\stuff中。

使用AddToClassPath对搜索路径所做的更改仅适用于当前Java会话。如果您退出并重新启动java,您将需要再次调用AddToClassPath

除了您自己用AddToClassPath添加的位置之外,J/Link还自动包括标准Wolfram语言应用程序位置($UserBaseDirectory/AddOns/Applications$BaseDirectory/AddOns/Applications<Mathematica dir>/AddOns/Applications<Mathematica dir>/AddOns/ExtraPackages)中任何目录的任何Java子目录。该特性旨在为为Wolfram语言(使用Java和J/Link作为其实现的一部分)创建应用程序的开发人员提供极其简单的部署。这在“部署使用J/Link的应用程序”中有更详细的描述,但是即使是编写使用J/Link的类的普通Java程序员也可以利用它。只需创建一个插件/应用程序的子目录,比如MyStuff,然后在其中创建一个Java子目录,然后将类或jar文件放入其中。J/Link将能够找到并使用它们。当然,松散类文件必须放置在Java目录的适当嵌套子目录中,对应于它们的包名(如果有的话),如所述。

AddToClassPath函数是在J/Link 2.0中引入的。以前版本的J/Link有一个名为$ExtraClassPath的变量,它指定了一个额外位置列表。您可以添加到这个列表。

AppendTo[$ExtraClassPath, "d:\\MyClasses"];

$ExtraClassPath在J/Link 2.0中不推荐使用,但它仍然可以工作。与使用AddToClassPath相比,$ExtraClassPath的一个优点是,对$ExtraClassPath所做的更改会在Java运行时重启后持续存在。

检查类路径

JavaClassPath函数返回一组目录和jar文件,J/Link将在其中搜索类。这包括用AddToClassPath$ExtraClassPath添加的所有位置,以及任何标准Wolfram语言应用程序位置中的应用程序目录的Java子目录。它不显示构成标准Java平台本身的jar文件,也不显示Java extensions目录中的jar文件。Java程序总是可以找到这些类。

JavaClassPath[]	gives the complete set of directories and jar files in which J/Link will search for classes

检查类搜索路径。

直接使用J/Link的类装入器

如前所述,J/Link使用自己的类装入器来允许它在启动类路径之外的动态位置集中查找类和其他资源。基本上,使用J/Link加载的不属于Java平台本身的所有类都将由这个类加载器加载。这样做的一个后果是,从Wolfram语言调用Java的Class.forName()方法通常不会工作。

LoadJavaClass["java.lang.Class"];
cls = Class`forName["some.class.that.only.JLink.can.find"]

Java::excptn: A Java exception occurred: java.lang.ClassNotFoundException: some.class.that.only.JLink.can.find
	at java.net.URLClassLoader$1.run(Unknown Source)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(Unknown Source)
	at java.lang.ClassLoader.loadClass(Unknown Source)
	at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
	at java.lang.ClassLoader.loadClass(Unknown Source)
	at java.lang.ClassLoader.loadClassInternal(Unknown Source)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Unknown Source)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source).

$Failed

问题是Class.forName()发现类使用一个默认的类装入器,而不是J/Link类装入器,这个默认的类装入器不知道J/Link的特殊目录查找类(实际上,它甚至不知道启动类的路径,因为它知道J/Link启动Java的细节)。如果您正在将Java代码翻译成Wolfram语言,或者您只是想为给定的类获取一个类对象,请注意这个问题。解决方法是强制使用J/Link的类装入器。一种方法是使用Class.forName()的三参数形式,它允许您指定要使用的类装入器。

LoadJavaClass["com.wolfram.jlink.JLinkClassLoader"];
cls = Class`forName["some.class.that.only.JLink.can.find", True, JLinkClassLoader`getInstance[]]

一种更简单的方法是使用JLinkClassLoader的静态classFromName方法。

cls = JLinkClassLoader`classFromName["some.class.that.only.JLink.can.find"]

您应该将这个classFromName()方法看作是Class.forName()的替代品。当您发现自己想要从以字符串形式给出的类名中获取类对象时,请记住使用JLinkClassLoader.classFromName()。

Class.forName()在Java代码中并不常见。使用它的一个原因是当需要创建对象,但在编译时不知道它的类。例如,类名可能来自首选项文件,或者以其他方式以编程方式确定。通常,下一行创建类的实例。

// Java code
Class cls = Class.forName("SomeClassThatImplementsInterfaceX");
X obj = (X) cls.newInstance();

如果要将这样的代码转换为Wolfram语言程序,只需调用JavaNew即可执行此操作。

这里的要点是,对于Class.forName()的常见用法,您不必逐行将其转换为Wolfram语言——您可以通过调用JavaNew来复制该功能。

性能问题

调用Java的开销

Java程序的速度高度依赖于Java运行时。在某些类型的程序上,例如,那些把大部分时间花在严格的数字处理循环中的程序,Java的速度可以接近编译、优化的C语言的速度。

对于计算密集型程序,Java是一个很好的选择。您的情况可能有所不同,但是在进行一些简单的速度测试之前,不要排除任何类型的程序使用Java。对于要求较低的程序(不需要提高每一分速度),使用J/Link而不是使用C编写传统WSTP“可安装”程序的简单性使得Java成为一个明显的选择。

在很大程度上,J/Link的速度问题不是Java执行的速度。相反,瓶颈是执行Java调用的速度,这本身主要受到WSTP的速度以及对于每个Java调用必须用Wolfram语言进行的处理的限制。Java调用的最大速率高度依赖于您使用的操作系统和Java运行时。一个快速的Windows机器每秒可以执行超过5000个Java方法调用,如果它们是静态方法,则可以执行更多,因为在Wolfram语言中需要的预处理更少。在一些操作系统上,结果会更少。您应该记住,无论调用做什么,对Java的调用都有一个或多或少固定的开销,在速度较慢的机器上,这个开销可能高达0.001秒。许多Java方法的执行时间要比这个时间短得多,因此调用的总时间通常由J/Link调用的固定周转时间所控制,而不是Java本身的速度。

对于大多数用途,一个调用到Java的开销不是一个问题,但是如果你有一个循环调用Java 500000次,你就会有一个问题(除非你的程序很长J/Link成本可以忽略不计,在这种情况下,您有一个更大的问题!)。如果您的Wolfram语言程序的结构要求对Java进行大量调用,那么您可能需要对其进行重构,以便在Java端执行更多操作,从而减少跨越Java-Wolfram语言边界的次数。这可能需要编写一些Java代码,不幸的是,这就违背了能够使用Wolfram语言编写任意Java程序的功能的J/Link前提。有些使用Java的情况是不能用这种方式编写脚本的,为此,您需要用Java编写更多的功能,而用Wolfram语言编写更少的功能。

加速发送大数组

你可以发送和接收大多数“原始”Java类型的数组(例如字节,short, int, float, double),几乎和c语言程序一样快。可以快速传递的类型集对应于WSTP C API具有放置数组的单个函数的类型集。Java类型long(64位)、boolean和String没有快速WSTP函数,因此发送或接收这些类型要慢得多。如果可能的话,尽量避免使用这些类型的超大数组(比如超过100,000个元素)。

一个对移动多维数组的速度有很大影响的设置是用来控制是否允许“不规则”数组。正如在“不规则数组”中所讨论的,J/Link的默认行为是要求所有数组都是矩形的。但是Java不要求数组符合这个限制,如果您想发送或接收不规则数组,您可以在Wolfram系统会话中调用AllowRaggedArrays[True]。这导致J/Link切换到一个更慢的方法来读取和写入数组。除非需要,否则不要使用此设置,不再需要时就关闭它。

当装入带有一个方法的类,该方法接受int[][]时,J/Link为调用此方法而创建的Wolfram语言中的定义使用一个模式测试,该模式测试要求其参数为一个二维整数数组。如果数组非常大,比如500 * 500,那么这个测试可能会花费大量时间,可能与将数组实际传输到Java所花费的时间类似。如果希望避免测试数组参数所花费的时间,可以将变量$RelaxedTypeChecking设置为True。如果这样做,就需要自己确保发送的数组具有正确的类型和维数。如果传递了一个错误的数组,就会得到一个WSTP错误,但是这不会给J/Link造成任何问题(除了调用将返回$Failed之外)。

您可能不希望在很长一段时间内将$RelaxedTypeChecking设置为True,而且如果您正在编写供他人使用的代码,您肯定不希望在他们的会话中更改它的值。$RelaxedTypeChecking用于块构造中,在块构造中,它在短时间内被赋值为True

Block[{$RelaxedTypeChecking = True}, obj[meth[someLargeArray]]]

$RelaxedTypeChecking只对数组有效,这是J/Link为其创建的模式测试相对于对Java的实际调用开销较大的唯一类型。

另一个加速J/Link程序的优化是使用ReturnAsJavaObject来避免在Wolfram语言和Java之间来回传递不必要的大数组或字符串。ReturnAsJavaObject将在“ReturnAsJavaObject”一节中讨论。

优化实例

接下来检查一个简单的步骤示例,您可以采取这些步骤来提高J/Link程序的速度。Java有一个强大的DecimalFormat类,可以使用它将Wolfram语言的数字格式化为所需的格式,以便输出到文件中。在这里,您创建了一个DecimalFormat对象,该对象将数字格式化为精确的小数点后四位。

fmt = JavaNew["java.text.DecimalFormat", "#.0000"];

要使用fmt对象,需要调用其format()方法,提供需要格式化的数字。

fmt@format[12.34]

12.3400

这将返回一个具有所请求格式的字符串。现在,假设您希望在将20000个数字写入文件之前使用此功能格式化它们。

data = Table[Random[], {40000}];

Map[fmt@format[#] &, data];

Map调用(调用格式方法40000次)在某台PC上花费46秒(这是挂钟时间,而不是Timing函数的结果,对于大多数系统上的WSTP程序来说,这不是精确的)。显然,这是不可接受的。作为第一步,您尝试使用MethodFunction,因为您多次调用相同的方法。

methodFunc = MethodFunction[fmt, format];

注意,使用fmt作为MethodFunction的第一个参数。第一个参数仅仅指定类;与J/Link中几乎所有接受类规范的函数一样,如果愿意,可以使用类的对象。所创建的MethodFunction可以用于DecimalFormat类的任何对象,而不仅仅是fmt对象。

Map[methodFunc[fmt, #] &, data];

使用methodFunc,现在需要36秒。在速度上有轻微的改进,比J/Link的早期版本要小得多。这意味着你每秒会接到大约1100个调用,但它的速度还不够快。惟一要做的是编写自己的Java方法,该方法接受一个数字数组,对它们进行格式化,并返回一个字符串数组。这将把从Wolfram语言到Java的调用数量从40000减少到1。

下面是必需的普通Java类的代码。请注意,这段代码并没有暗示将通过J/Link从Wolfram语言调用它。如果您想在Java中使用此功能,那么您将编写完全相同的代码。

public class FormatArray {
	public static String[] format(java.text.DecimalFormat fmt,double[] d) {
		String[] result=new String[d.length];
		for (int i = 0; i < d.length; i++)
			result[i] = fmt.format(d[i]);
		return result;
	}
}

这个新版本只需要不到2秒。

LoadJavaClass["FormatArray"];
FormatArray`format[fmt, data];

引用计数和内存管理

对象引用在Wolfram语言中

早期对JavaObject表达式的处理避免讨论更深层次的问题,如引用计数和唯一性。每当一个Java对象引用返回给Wolfram语言时,不管是作为方法或字段的结果,还是显式调用JavaNew的结果,J/Link都会查看这个对象的引用在此会话之前是否已经被发送。如果没有,它就用Wolfram语言创建一个JavaObject表达式,并为它设置许多定义。这是一个比较耗时的过程。如果这个对象已经被发送到Wolfram语言,那么在大多数情况下J/Link只是创建一个与之前创建的相同的JavaObject表达式。这是一个更快的操作。

最后一条规则有一些例外,这意味着有时当一个对象返回到Wolfram语言时,会为它创建一个新的、不同的JavaObject表达式,即使这个相同的对象之前已经被发送到Wolfram语言。具体来说,当对象的hashCode()值自上次在Wolfram语言中出现以来发生变化时,创建的JavaObject表达式将会不同。除了要记住SameQ不是比较JavaObject表达式以确定它们是否引用同一对象的有效方法之外,您实际上不需要关心这些细节。必须使用SameObjectQ函数。

SameObjectQ[Subscript[obj, 1],Subscript[obj, 2]]	return True if the JavaObject expressions Subscript[obj, 1] and Subscript[obj, 2] refer to the same Java object, False otherwise

比较JavaObject表达式。

这里有一个例子。

pt = JavaNew["java.awt.Point", 1, 1]

«JavaObject["java.awt.Point"]»

变量pt引用一个Java Point对象。现在把它放进一个容器里,这样你以后就可以取出来了。

vec = JavaNew["java.util.Vector"];
vec@add[pt];

现在更改其中一个字段的值。对于Point对象,更改其字段之一的值将更改其hashCode()值。

pt@x = 2;

现在比较pt给出的JavaObject表达式和请求将向量的第一个元素返回到Wolfram语言时创建的JavaObject表达式。即使它们都是对同一个Java对象的引用,但JavaObject表达式是不同的。

pt === vec@elementAt[0]

False

因为您不能使用SameQ(===)来决定Wolfram语言中的两个对象引用是否引用同一Java对象,所以J/Link为此提供了一个函数SameObjectQ

SameObjectQ[pt, vec@elementAt[0]]

True

您可能想知道为什么需要使用SameObjectQ函数。为什么不直接调用对象的equals()方法呢?对于这个例子,它当然给出了正确的结果。

pt@equals[vec@elementAt[0]]

True

这种技术的问题是equals()并不总是比较对象引用。任何类都可以自由地重写equals(),以提供比较该类的两个对象所需的任何行为。有些类使用equals()比较对象的“内容”,比如String类,它使用equals进行字符串比较。Java提供了两种不同的相等操作,==操作符和equals()方法。操作符==总是比较引用,当且仅当引用指向同一对象时返回true,但是equals()经常被重写用于其他类型的比较。因为Java中没有方法调用模仿应用于对象引用的语言的==操作符的行为,所以J/Link需要一个SameObjectQ函数来为Wolfram语言程序员提供这种行为。

在需要大量比较对象引用是否相等的特殊情况下,SameObjectQ相对于SameQ比较缓慢可能会成为一个问题。可能导致引用完全相同Java对象的两个JavaObject表达式不是SameQ的唯一原因是,在创建这两个JavaObject表达式期间,对象的hashCode()值发生了变化。如果您知道没有发生这种情况,那么您可以安全地使用SameQ来测试它们是否引用相同的对象。

ReleaseJavaObject

Java语言有一个称为“垃圾收集”的内置功能,用于释放程序不再使用的对象所占用的内存。当任何地方都不存在对对象的引用时,对象就符合垃圾收集的条件,除了其他未被引用的对象。当一个对象返回给Wolfram语言,由于调用JavaNew或作为一个方法调用的返回值或字段访问J/Link代码拥有特殊的参考对象在Java端,以确保它不能被垃圾收集而Wolfram使用的语言。如果您知道不再需要在Wolfram系统会话中使用给定的Java对象,那么您可以显式地告诉J/Link释放它的引用。执行此操作的函数是ReleaseJavaObject。除了释放Java中特定于Wolfram语言的引用之外,ReleaseJavaObject还清除了在Wolfram语言中创建的对象的内部定义。在Wolfram语言中使用该对象的任何后续尝试都将失败。

frm = JavaNew["java.awt.Frame"]

«JavaObject["java.awt.Frame"]»

现在告诉Java您不再需要使用来自Wolfram语言的这个对象。

ReleaseJavaObject[frm]

现在引用frm是错误的。

ReleaseJavaObject[obj]	let Java know that you are done using obj in the Wolfram Language
ReleaseObject[obj]	deprecated; replaced by ReleaseJavaObject in J/Link 2.0
JavaBlock[expr]	all novel Java objects returned to the Wolfram Language during the evaluation of expr will be released when expr finishes
BeginJavaBlock[]	all novel Java objects returned to the Wolfram Language between now and the matching EndJavaBlock[] will be released
EndJavaBlock[]	release all novel objects seen since the matching BeginJavaBlock[]
LoadedJavaObjects[]	return a list of all objects that are in use in the Wolfram Language
LoadedJavaClasses[]	return a list of all classes loaded into the Wolfram Language

J/Link内存管理功能。

调用ReleaseJavaObject并不一定会导致该对象被垃圾收集。在Java中很可能存在对它的其他引用。ReleaseJavaObject并没有告诉Java丢弃该对象,只是它不需要仅仅为了Wolfram语言的缘故而保留。

关于J/Link为发送给Wolfram语言的对象维护的引用的一个重要事实是,对于每个对象,无论它返回给Wolfram语言多少次,都只保留一个引用。确保在调用ReleaseJavaObject之后,永远不要试图通过任何可能存在于您的Wolfram系统会话中的对该对象的引用来使用该对象,这是您的责任。

frm = JavaNew["java.awt.Frame"];
b1 = JavaNew["java.awt.Button"];

框架类的add()方法返回添加的对象,因此b2引用与b1相同的对象。

b2 = frm@add[b1];

如果您调用ReleaseJavaObject[b1],受影响的不是Wolfram语言符号b1,而是b1所引用的Java对象。因此,使用b2也是错误的(或者以任何其他方式引用这个按钮对象,如%)。

在日常使用中,调用ReleaseJavaObject通常是不必要的。如果你没有在你的会话中大量使用Java,那么你通常不需要关心跟踪什么对象可能需要或可能不需要了,你可以让它们堆积起来。不过,在某些特殊时刻,Java中的内存使用将非常重要,您可能需要ReleaseJavaObject提供的额外控制。

JavaBlock

ReleaseJavaObject主要是为编写代码供他人使用的开发人员提供的。因为您永远无法预测您的代码将如何被使用,所以开发人员应该始终确保他们的代码清除它所创建的任何不必要的引用。对此最有用的函数可能是JavaBlock

JavaBlock自动释放在表达式求值过程中遇到的对象。通常,一个Wolfram语言程序需要用JavaNew创建一些Java对象;操作它们,可能会导致其他对象作为方法调用的结果返回给Wolfram语言;最后返回一些结果,如数字或字符串。Wolfram语言在这个操作过程中遇到的每个Java对象只在程序的生命周期中需要,就像在Wolfram语言中通过BlockModule提供的局部变量,以及在C、c++、Java和许多其他语言中通过块作用域构造(例如,{})提供的局部变量一样。JavaBlock允许您将一个代码块标记为具有这样的属性:在求值期间返回给Wolfram语言的任何新对象都将被视为临时对象,并在JavaBlock结束时释放。

需要注意的是,前面的句子说的是“new objects”。JavaBlock不会导致在计算过程中遇到的每个对象都被释放,只会释放那些第一次遇到的对象。已经被Wolfram语言看到的对象不会受到影响。这意味着您不必担心JavaBlock会主动释放一个对象,这个对象对于那个求值来说并不是真正临时的。

仅仅在使用JavaNew创建的每个对象上调用ReleaseJavaObject是不够的,因为许多Java方法调用返回对象。您可能对这些返回值不感兴趣,或者您可能永远不会将它们分配给一个命名变量,因为它们可能与其他调用链接在一起(如obj@returnsObject[]@foo[]),但您仍然需要释放它们。使用JavaBlock是确保在代码块结束时释放所有新对象的一种简单方法。

JavaBlock[expr]返回expr返回的内容。

MyFunc[args__] :=
 	JavaBlock[
  		Module[{locals},
   			...		
   		]
  	]

编写一个函数来创建和操作大量JavaObject表达式,然后返回其中一个表达式,其余的都是临时的,这是很常见的。为了方便这一点,如果JavaBlock的返回值是单个JavaObject,那么它将不会被释放。

MyOtherFunc[args__] :=
 	JavaBlock[
  		Module[{obj},
   			...
        			obj = JavaNew["java.awt.Frame"];
     			... 
    			Return[obj]  (* OK: 
   obj will not be released when JavaBlock finishes. *)
   		]
  	]

在J/Link 2.1中新增了KeepJavaObject函数,它允许您指定一个或一系列在JavaBlock结束时不应该被释放的对象。在单个对象或对象序列上调用KeepJavaObject意味着当第一个封装JavaBlock结束时,这些对象将不会被释放。如果有一个外部封装的JavaBlock,对象将在它结束时被释放,但是,如果您想要对象逃出嵌套的JavaBlock表达式集,您必须在每个级别调用KeepJavaObject。或者,您可以调用KeepJavaObject[obj,Manual],其中手动参数告诉J/Link对象不应该被任何封装的JavaBlock表达式释放。释放此类对象的唯一方法是手动调用ReleaseJavaObject。下面是一个使用KeepJavaObject的示例,它允许您在不释放两个对象的情况下返回一个包含两个对象的列表。

MyOtherFunc[args__] :=
	Module[{obj1, obj2, obj3}, 
		JavaBlock[
			obj1 = JavaNew["java.awt.Frame"]; 
			obj2 = JavaNew["java.awt.Button"]; 
     		obj3 = JavaNew["SomeTemporaryObject"];
     		...
     		KeepJavaObject[obj1, obj2];
     		{obj1, obj2}
     	]
     ]

可以使用BeginJavaBlock和EndJavaBlock在多个评估中提供与JavaBlock相同的功能。EndJavaBlock释放所有新的Java对象返回到Wolfram语言自从之前匹配BeginJavaBlock。这些函数主要在开发期间使用,这时您可能想在会话中设置一个标记,做一些工作,然后释放从那时起返回给Wolfram语言的所有新对象。BeginJavaBlock和EndJavaBlock可以嵌套。每个BeginJavaBlock都应该有一个匹配的EndJavaBlock,尽管忘记调用EndJavaBlock不是一个严重的错误,即使你已经嵌套了它们的级别——你只会无法释放一些对象。

LoadedJavaObjects和LoadedJavaClasses

LoadedJavaObjects[]返回当前在Wolfram语言中引用的所有Java对象的列表。这包括用JavaNew显式创建的所有对象,以及作为Java方法调用或字段访问的结果返回给Wolfram语言的所有对象。它不包括已通过ReleaseJavaObjectJavaBlock释放的对象。LoadedJavaObjects主要用于调试。在处理某个函数之前和之后调用它是非常有用的。如果列表增长,您的函数泄漏引用,您需要检查它对JavaBlock和/或ReleaseJavaObject的使用。

LoadedJavaClasses[]返回一个JavaClass表达式列表,该列表表示加载到Wolfram语言中的所有类。与LoadedJavaObjects一样,LoadedJavaClasses类主要用于调试。注意,在调用LoadJavaClass之前,您不必确定类是否已经被加载。如果类已经被加载,LoadJavaClass只返回适当的JavaClass表达式,而不做任何事情。

异常

怎样处理异常

J/Link自动处理Java异常。如果在Java调用期间抛出未捕获的异常,您将在Wolfram系统中得到一条消息。下面是一个试图将实数格式化为整数的示例。

LoadClass["java.lang.Integer"];
Integer`parseInt["1234.5"]

Java::excptn: A Java exception occurred: java.lang.ArrayIndexOutOfBoundsException.

$Failed

如果在方法返回结果给Wolfram语言之前抛出异常,如本例所示,调用的结果将是$Failed。正如后面在“向Wolfram语言手动返回结果”中所讨论的,可以编写自己的方法,在返回结果之前手动将结果发送给Wolfram语言。在这种情况下,如果在将结果发送到Wolfram语言之后,但是在方法返回之前抛出异常,您将得到一个报告异常的警告消息,但是调用的结果将不受影响。

如果Java代码是在包含调试信息的情况下编译的,那么作为异常结果得到的Wolfram系统消息将显示到异常发生点的完整堆栈跟踪,以及每个文件中确切的行号。

JavaThrow函数

在某些情况下,您可能希望在Java中引发异常。这可以通过JavaThrow函数完成。JavaThrow是J/Link 2.0中的新内容,应该被认为是实验性的。它的行为可能会在未来的版本中改变。

JavaThrow[exceptionObj]	throw the given exception object in Java

从Wolfram语言抛出Java异常。

您只希望在本身从Java调用的Wolfram语言代码中使用JavaThrow。对于用Wolfram语言编写的J/Link程序来说,同时涉及从Wolfram语言到Java的调用和从Java到Wolfram语言的调用是很常见的。这种对Wolfram语言的“回调”在创建Java用户界面的Wolfram语言程序中被广泛使用,如后面“创建窗口和其他用户界面元素”一节中详细描述的那样。例如,您可以关联一个在用户单击Java按钮时调用的Wolfram语言函数。这个Wolfram语言函数是从Java直接调用的,您可能希望它的行为像Java方法一样,包括能够抛出Java异常。

在用户界面操作(如单击按钮)的回调中抛出异常的例子不是很现实,因为Java中通常没有捕捉此类异常的东西;因此,它们基本上被忽略了。一个更有意义的例子是一个混合使用Java和Wolfram语言代码的程序,出于开发灵活性和易用性的原因,您调用了一个Wolfram语言函数来实现可以抛出异常的Java方法的“核心”。作为一个具体的例子,假设您正在使用SAX(用于XML的简单API) API使用Java和Wolfram语言进行XML处理。SAX处理基于一组处理程序方法,在解析XML文档期间发生某些事件时调用这些处理程序方法。每个这样的方法都可以抛出一个SAXException来指示错误并停止解析。您希望在Wolfram语言代码中实现这些处理程序方法,因此需要一种从Wolfram语言抛出SAXException的方法。下面是startDocument()方法的一个假设示例,SAX引擎在文档处理开始时调用该方法。

startDocument[] := 
 If[! readyToAcceptParsingEvents, 
  JavaThrow[
   JavaNew["org.xml.sax.SAXException", 
    "Mathematica code has not been initialized"]]]

在调用JavaThrow之后,Wolfram语言函数的其余部分正常执行,但是没有返回给Java的结果。

按值”和“按引用”返回对象

引用和值

J/Link提供了某些Wolfram语言表达式与其对应的Java表达式之间的映射。这意味着,当这些Wolfram语言表达式在Wolfram语言和Java之间传递时,它们会自动地与相应的Java表达式进行转换。例如,Java整数类型(long、short、int等)被转换为Wolfram语言的整数,而Java实类型(float和double)被转换为Wolfram语言的实数。另一个映射是在Wolfram语言中将Java对象转换为JavaObject表达式。这些JavaObject表达式是对Java对象的引用,它们在Wolfram语言中没有任何意义,除非它们被J/Link操作。但是,一些Java对象在Wolfram语言中具有有意义的值,并且这些对象默认转换为值。此类对象的例子有字符串和数组。

那么,您可以说,除了少数特殊情况外,Java对象在默认情况下“通过引用”返回给Wolfram语言。这些特殊情况是字符串、数组、复数(稍后讨论)、BigDecimal和BigInteger(稍后讨论)以及“包装器”类(例如java.lang.Integer)。您可以说这些异常情况是“按值”返回的。“Java和Wolfram语言之间的类型转换”中的表显示了这些特殊的Java对象类型是如何映射到Wolfram语言值的。

总之,每个在Wolfram语言中具有有意义的值表示的Java对象都被转换为这个值,因为这是最有用的行为。但是,有时您可能需要重写此默认行为。这样做的最常见原因可能是为了避免在WSTP上不必要地传输大型表达式。

ReturnAsJavaObject[expr]	a Java object returned by expr will be in the form of a reference
ByRef[expr]	deprecated; replaced by ReturnAsJavaObject in J/Link 2.0
JavaObjectToExpression[obj]	give the value of the Java object obj as a Wolfram Language expression
Val[obj]	deprecated; replaced by JavaObjectToExpression in J/Link 2.0

“通过引用”和“通过值”控制。

ReturnAsJavaObject

考虑这样一种情况,在类MyClass中有一个名为arrayAbs()的静态方法,它接受一个双精度数数组并返回一个新数组,其中每个元素都是参数数组中相应元素的绝对值。这个方法的声明看起来像double[] arrayAbs(double[] a).这就是从Wolfram语言中调用这样一个方法的方式。

LoadJavaClass["MyClass", StaticsVisible -> True];
arrayAbs[{1., -2., 3., 4.}]

{1., 2., 3., 4.}

示例展示了您可能希望该方法如何工作:传递一个Wolfram语言列表并返回一个列表。现在假设您有另一个名为arraySqrt()的方法,它的作用类似于arrayAbs(),只是它执行的是sqrt()函数而不是abs()。

arraySqrt[arrayAbs[{1., -2., 3., 4.}]]
{1., 1.41421, 1.73205, 2.}

在此计算中,原始列表将通过WSTP发送给Java,并使用这些值创建一个Java数组。该数组作为参数传递给arrayAbs(), arrayAbs()本身创建并返回另一个数组。然后通过WSTP将这个数组发送回Wolfram语言以创建一个列表,然后这个列表作为arraySqrt()的参数迅速发送回Java。你可以看到,这是一个浪费时间发送数组数据回Wolfram语言——你有一个完美的数组(返回的arrayAbs()方法)生活在Java端,可以传递给arraySqrt(),但你只把其内容送回Wolfram语言有它立即回到Java又作为一个新的数组相同的值!对于本例,成本可以忽略不计,但是如果数组有200,000个元素呢?

需要的是一种方法,让数组数据保留在Java中,并且只返回对数组的引用,而不是实际数据本身。这可以通过ReturnAsJavaObject函数来完成。

ReturnAsJavaObject[arrayAbs[{1., -2., 3., 4.}]]

«JavaObject[[D]»

请注意,JavaObject的类名是"[D",虽然有点晦涩,但它是一维双精度数组的实际Java类名。下面是使用ReturnAsJavaObject进行计算的情况。

arraySqrt[ReturnAsJavaObject[arrayAbs[{1., -2., 3., 4.}]]]

{1., 1.41421, 1.73205, 2.}

在前面,您看到arraySqrt()被调用时带有一个参数,该参数是一个Wolfram语言的实数列表。这里是通过对Java对象的引用来调用它的,Java对象是一个一维双精度数组。所有接受数组的方法和字段都可以通过Wolfram语言列表或适当类型的Java数组的引用从Wolfram语言调用。

字符串是ReturnAsJavaObject有用的另一种类型。像数组一样,字符串有两个属性:(1)它们在Java中被表示为对象,但也有一个有意义的Wolfram语言值;(2)它们可能很大,因此避免不必要地来回传递它们的数据是有用的。例如,假设您的类MyClass有一个静态方法,该方法将从Java控制的外部设备获取的数字追加到字符串。你有一个名为veryLongString的Wolfram语言变量,它拥有一个很长的字符串,你想要在这个字符串上追加100次。这段代码将导致字符串内容在Wolfram语言和Java之间来回100次。

Do[veryLongString = appendString[veryLongString], {100}];

使用ReturnAsJavaObject可以让字符串保留在Java端,因此它实际上不会生成WSTP通信量。

Do[veryLongString = ReturnAsJavaObject[appendString[veryLongString]], {100}];

这个例子有点做作,因为重复地附加到一个不断增长的字符串并不是一种非常有效的编程风格,但是它说明了问题。

当执行Do循环时,非常长的字符串得到的赋值不是Wolfram语言字符串,而是引用驻留在Java中的字符串的JavaObject表达式。这意味着在第一次迭代时使用Wolfram语言字符串调用appendString(),之后使用JavaObject表达式。与数组的情况一样,任何接受字符串的Java方法或字段都可以在Wolfram语言中通过引用一个字符串或引用一个JavaObject表达式调用。非常长的string变量开始持有一个字符串,但在循环的最后它持有一个JavaObject表达式。

veryLongString

«JavaObject["java.lang.String"]»

在某些情况下,您可能需要一个实际的Wolfram语言字符串,而不是这个字符串对象引用。你如何取回价值?稍后引入JavaObjectToExpression函数时,您将再次访问这个示例。

总之,ReturnAsJavaObject函数导致返回通常转换为Wolfram语言值的对象的方法和字段返回引用。它是为了避免在Wolfram语言和Java之间不必要地传递大量数据而进行的优化,因此它主要用于非常大的数组和字符串。与所有优化一样,您不应该关注ReturnAsJavaObject,除非您有一些代码以不可接受的速度运行,或者您提前知道您正在编写的代码将从它获得可观的收益。大多数Java类的对象在Wolfram语言中没有有意义的“按值”表示,它们总是“按引用”返回。在这些情况下,ReturnAsJavaObject不起作用。

最后,请注意,ReturnAsJavaObject对Java程序员将结果手动发送回Wolfram语言的方法没有影响(本用户指南后面将讨论这个主题)。手动返回结果会绕过J/Link中的常规结果处理例程,因此不可能满足ReturnAsJavaObject请求。

JavaObjectToExpression

在上一节中,您看到了如何使用ReturnAsJavaObject函数使通常按值返回到Wolfram语言的对象按引用返回。有必要有一个函数来执行反向——获取引用并将其转换为其值表示。该函数是JavaObjectToExpression

回到前面的appendString示例,您使用了ReturnAsJavaObject来避免在WSTP上来回传递字符串数据的开销。这样做的结果是,非常长的字符串变量现在持有一个JavaObject表达式,而不是一个文字Wolfram语言字符串。可以使用JavaObjectToExpression以Wolfram语言字符串的形式获取该字符串对象的值。

JavaObjectToExpression[veryLongString]

"03711808636264453448949229492898928782279194828408974226912223659285166782970062739405320988762893368"

在Wolfram语言中,大多数Java对象没有有意义的值表示。在Wolfram语言中,这些对象只能表示为JavaObject表达式,对它们使用JavaObjectToExpression不起作用。

ReturnAsJavaObject函数并不是获取通常作为值返回给Wolfram语言的对象的JavaObject表达式的唯一方法。JavaNew函数总是返回一个引用。

JavaNew["java.lang.String", "a string"]

«JavaObject["java.lang.String"]»

JavaObjectToExpression[%]

"a string"

下一节将介绍MakeJavaObject函数,它比使用JavaNew从Wolfram语言字符串和数组构造Java对象更容易。

MakeJavaObject和MakeJavaExpr

序言

除了调用类构造函数的JavaNew之外,J/Link还提供了两个方便的函数,用于从Wolfram语言表达式创建Java对象。这些函数是MakeJavaObjectMakeJavaExpr。不要把它们弄混,尽管它们的名字很相似。MakeJavaObject是一个常用的函数,用于构造一些特殊类型的对象。MakeJavaExpr是一个高级函数,它创建表示任意Wolfram语言表达式的J/Link的Expr类的对象。

MakeJavaObject

MakeJavaObject[val]	construct an object of the appropriate type to represent the Wolfram Language expression val (numbers, strings, lists, and so on)

MakeJavaObject.

当您从Wolfram语言调用一个接受Java字符串对象的Java方法时,您可以使用Wolfram语言字符串调用它。J/Link的内部将构造一个与Wolfram语言字符串具有相同字符的Java字符串,并将该字符串传递给Java方法。但是,有时您希望将字符串传递给接收类型为Object的方法。您不能使用字符串作为参数从Wolfram语言调用这样的方法,因为尽管J/Link能够识别出Wolfram语言字符串对应于Java字符串,但它不能识别出Wolfram语言字符串对应于Java对象。它是故意这样做的,目的是在Java调用中施加尽可能多的类型安全性。对于这个示例,假设类MyClass有一个具有以下签名的方法。

void foo(Object obj);

还假设theObj是这个类的一个对象,是用JavaNew创建的。试着用字符串字面量调用foo。

theObj@foo["this is a string"]
	Java::argxs:The method foo was called with an incorrect number or type of arguments.
$Failed

由于上述原因,它失败了。要调用一个Java方法,该方法的类型是接受一个带有字符串的对象,您必须首先显式地创建一个带有适当值的Java字符串对象。您可以使用JavaNew来做到这一点。

javaStr = JavaNew["java.lang.String", "this is a string"]

«JavaObject["java.lang.String"]»

现在它可以工作了,因为参数是一个JavaObject表达式。

theObj@foo[javaStr]

为了避免必须调用JavaNew来创建Java字符串对象,J/Link提供了MakeJavaObject函数。

javaStr = MakeJavaObject["this is a string"];

在字符串的情况下,MakeJavaObject只是为您调用JavaNew。当然,如果它只能构造字符串对象,那么它就没有多大用处。直接表示Wolfram语言值的其他Java对象也会出现同样的问题。这包括“包装器”类(如java.lang.Integer、java.lang.Boolean等)和数组类。如果您想调用一个以java.lang.Integer作为参数的Java方法,您可以使用一个原始整数从Wolfram语言调用它。但是如果你想传递一个整数给一个方法,这个方法的类型是一个对象,你必须显式地创建一个对象类型为java.lang.Integer——J/Link不会从一个整数参数自动构造一个对象。为此,调用MakeJavaObject比调用JavaNew要简单。

MakeJavaObject[42]

«JavaObject["java.lang.Integer"]»

当给定一个整数参数时,MakeJavaObject总是构造java.lang.Integer,而不是java.lang.Short、 java.lang.Long或其他“整数”Java包装器对象。类似地,如果您使用实数调用MakeJavaObject,它将创建一个java.lang.Double,而不是一个java.lang.Float。如果您需要一个其他类型的对象,您将不得不显式地调用JavaNew

MakeJavaObject也适用于布尔值。

MakeJavaObject[True]

«JavaObject["java.lang.Boolean"]»

如果MakeJavaObject只是调用JavaNew的捷径,那么它就没有那么有用了。但是,在创建数组类的对象时,它是必不可少的。回想一下,在Java中,数组是对象,它们属于一个类。这些类具有神秘的名称,但是如果您知道它们,就可以使用JavaNew创建数组对象。在创建数组对象时,JavaNew使用的第二个参数是一个列表,给出每个维度的长度。这里创建了一个2*3的int数组。

intArray2D = JavaNew["[[I", {2, 3}]
«JavaObject[[[I]»

JavaNew允许创建数组对象,但不允许为数组元素提供初始值。另一方面,MakeJavaObject接受一个Wolfram语言列表,并将其转换为具有相同值的Java数组对象。

intArray2D = MakeJavaObject[{{1, 2, 3}, {4, 5, 6}}]

«JavaObject[[[I]»

因此,对于创建数组对象MakeJavaObject尤其有用,因为它允许您提供数组元素的初始值,它使你不必学习和记忆Java数组类的名称([[I为一个二维的整数数组,[D为double的一维数组,等等)。MakeJavaObject可以创建深度达3维的数组,其中包括整数、双精度数、字符串、布尔值和对象。

JavaObjectToExpression函数将在“JavaObjectToExpression”一节中讨论,您可以将MakeJavaObject看作JavaObjectToExpression的逆函数。MakeJavaObject接受一个具有对应Java对象的Wolfram语言表达式,该Java对象可以表示其值,并创建该对象。它字面意思是“使其成为Java对象”。JavaObjectToExpression函数则相反——它接受一个具有有意义的Wolfram语言表示的Java对象,并将其转换为该表达式。对于MakeJavaObject可以操作的任何x,以下内容始终适用。

JavaObjectToExpression[MakeJavaObject[x]] === x

请记住,MakeJavaObject不是一个常用的函数。您不需要从Wolfram语言字符串、数组等显式地构造Java对象,只需将它们传递给Java方法——J/Link就会自动地为您完成这一工作。但是,尽管在大多数情况下J/Link会根据某些参数自动创建对象,但当参数类型为通用对象时,它就不会这样做了。然后您必须自己创建一个JavaObject,而MakeJavaObject是实现这一点的最简单方法。

在“SetInternetProxy”一节中讨论的SetInternetProxy函数的代码提供了使用MakeJavaObject的具体示例。要指定代理信息(针对防火墙后的用户),需要使用properties类设置一些系统属性。这个类是Hashtable的子类,因此它有一个具有以下签名的方法。

Object put(Object key, Object value);

您应该始终以字符串的形式为属性指定键和值。因此,您可以尝试使用Wolfram语言。

LoadJavaClass["java.lang.System"];
System`getProperties[]@put["proxySet", "true"]
	Java::argx: Method named put defined in class java.util.Properties was called with an incorrect number or type of arguments. The arguments, shown here in a list, were {proxySet,true}.
	
$Failed

要使其工作,您需要使用MakeJavaObject来创建Java字符串对象。

System`getProperties[]@
 put[MakeJavaObject["proxySet"], MakeJavaObject["true"]]

MakeJavaExpr

要理解MakeJavaExpr函数,需要理解J/Link的Expr类的动机,这将在“Expr类的动机”中详细讨论。基本上,Expr是一个Java对象,它可以表示任意的Wolfram语言表达式。它的主要用途是为想要在Java中检查和操作Wolfram语言表达式的Java程序员提供便利。有时,有一种用Wolfram语言而不是Java创建Expr对象的方法是有用的。MakeJavaExpr是满足这一需求的函数。

MakeJavaExpr[expr]	construct an object of J/Link's Expr class that represents the Wolfram Language expression

MakeJavaExpr.

注意,如果您正在调用一个输入类型为Expr的Java方法,那么您不必调用MakeJavaExpr来构造一个Expr对象。与其他自动转换一样,J/Link将自动将您提供的作为参数的任何表达式转换为Expr对象。与MakeJavaObject一样,MakeJavaExpr用于调用采用通用对象(而不是Expr)的方法,因此J/Link不会为您执行任何自动转换。在这种情况下,您需要从一些Wolfram语言表达式显式地创建一个Expr对象。您可能希望这样做的一个原因是在Java中存储一个Wolfram语言表达式,以便稍后检索。

下面是MakeJavaExpr的一个简单示例。本文演示了Expr类中的一些方法,它有许多类似于Wolfram语言的方法,用于检查、修改和提取表达式的部分。当然,这是一个精心设计的示例——如果您想知道表达式的长度,只需调用Wolfram语言的length[]函数。这里演示的Expr方法通常从Java调用,而不是从Wolfram语言调用。

e = MakeJavaExpr[1 + 2 x + x^2]

«JavaObject[com.wolfram.jlink.Expr]»
e@length[]

3
e@part[3]

x^2
e@insert[x^3, -1]

1 + 2 x + x^2 + x^3

请注意,Expr对象,如Wolfram语言表达式,是不可变的。上面对insert()的调用没有修改e;相反,它返回一个新的Expr。

JavaObjectToExpression[e]

1 + 2 x + x^2

如果您在理解为什么要在Wolfram语言程序中使用MakeJavaExpr方面有困难,请不要担心。它是一个高级函数,很少有程序员会使用它。

创建窗口和其他用户界面元素

在Java窗口中绘制和显示Wolfram语言图像

Java控制台窗口

J/Link提供了一种方便的方法来显示Java“控制台”窗口。任何写到标准System.out和System.err流的输出都将被定向到这个窗口。如果您正在调用将诊断信息写入System.out或System.err的Java代码,那么您可以在程序运行时看到此输出。与大多数J/Link特性一样,控制台窗口可以从Wolfram语言或Java程序中轻松使用(在“编写使用Wolfram语言的Java程序”中描述了对Java代码的使用)。要从Wolfram语言使用它,请调用ShowJavaConsole函数。

ShowJavaConsole[]	display the Java console window and begin capturing output written to System.out and System.err
ShowJavaConsole["stream"]	display the Java console window and begin capturing output written to the specified stream, which should be "stdout" for System.out or "stderr" for System.err
ShowJavaConsole[None]	stop all capturing of output

显示控制台窗口。

ShowJavaConsole[]

«JavaObject["com.wolfram.jlink.ui.ConsoleWindow"]»

捕获输出只有当你叫ShowJavaConsole开始,所以,当窗口第一次出现,它将没有任何内容,可能是以前写给System.out或者System.err你也会注意到,J/Link控制台窗口显示版本信息的Java组件和Java运行时本身。在窗口已经打开时调用ShowJavaConsole将使其出现在前台。

为了演示,您可以从Wolfram语言编写一些输出。如果执行前面给出的ShowJavaConsole[],那么您将看到在窗口中打印出“Hello from Java”。

LoadJavaClass["java.lang.System"];
System`out@println["Hello from Java"]

尽管使用像这样的Wolfram语言代码来演示向窗口编写代码很方便,但这通常是从Java代码完成的。实际上,在一种常见的情况下,使用Java控制台窗口对由Wolfram语言代码编写的诊断输出非常有用。在这种情况下,你有一个“非模态”的Java用户界面(如“创建窗口和其他用户界面元素”一节所述),而且你没有使用ShareFrontEnd函数。回想一下,在这种情况下,使用Wolfram语言进行Print的调用的输出将不会出现在笔记本前端。如果你写入System.out,相反,如本例所示,您将始终能够看到输出。您可能希望在其他情况下这样做,只是为了避免调试输出使您的笔记本混乱。

使用JavaBean

托管applet

定期任务

“创建Windows和其他用户界面元素”一节描述了ShareKernel功能,以及它如何允许Java和笔记本前端共享内核的注意力。此功能的一个附带好处是,它可以很容易地提供一种方法,让用户可以安排任意的Wolfram语言程序在会话期间周期性地运行。假设您有一个提供持续更新的财务数据的来源,并且您希望使用Wolfram语言中的一些变量来持续反映当前的价值。你已经写了一个程序,从源代码读取信息,但是你必须手动运行这个程序,在你工作的时候。更好的解决方案是设置定期任务,从源中提取数据并每15秒设置变量。

AddPeriodical[expr,secs]	cause expr to be evaluated every secs seconds while the kernel is idle
RemovePeriodical[id]	stop scheduling of the periodical represented by id
Periodical[id]	return a list {HoldForm[expr],secs} showing the expression and time interval associated with the periodical represented by id
Periodicals[]	return a list of the id numbers of all currently scheduled periodicals
SetPeriodicalInterval[id]	reset the periodical interval for the periodical task represented by id
$ThisPeriodical	holds the id of the currently executing periodical task

控制定期任务。

您可以用AddPeriodical函数设置这样的任务。

id = AddPeriodical[updateFinancialData[], 15];

AddPeriodical返回一个必须用于标识任务的整数ID号——例如,当需要通过调用RemovePeriodical来停止调度任务时。AddPeriodical依赖于内核共享,所以如果还没有调用ShareKernel,它就会调用ShareKernel。周期任务数量不限。

在调度该任务之后,当内核空闲时,updateFinancialData[]将每15秒执行一次。请注意,周期性的任务只在内核不繁忙时运行,它们不会中断其他的计算。如果内核在分配的15秒过后正在执行另一个计算,任务将等待执行,直到计算立即结束。任何这样的延迟的定期任务都保证在内核完成当前计算后立即执行。如果用户在前端或Java中忙于大量计算,则不能无限期地延迟它们。反之亦然——如果用户在前端计算单元格时正在执行周期任务,则在所有周期任务结束之前无法开始计算,但可以保证在此之后立即开始。

若要删除单个定期任务,请使用RemovePeriodical,并提供期刊的ID号作为参数。使用RemovePeriodical[Periodicals[]]删除所有定期任务。如果不带参数调用UnshareKernel[],周期性任务将全部删除,这将关闭所有内核共享。然后,您需要再次使用AddPeriodical来重新建立期刊任务。

您可以通过调用SetPeriodicalInterval(这是J/Link 2.0中的新内容)来重置周期性任务的调度间隔。这行代码使财务数据每10秒定期执行一次,而不是前面显示的每15秒执行一次。

SetPeriodicalInterval[id, 10]

有时,您可能希望更改周期性任务的间隔,或者从任务本身的代码中完全删除它。$ThisPeriodical是一个变量,它保存当前执行的期刊任务的ID。它只有在周期性任务执行期间才有值。你使用在你周期任务的$ThisPeriodical来获取它的ID,这样你就可以调用RemovePeriodical或者SetPeriodicalInterval

周期性任务不一定与Java有任何关系,也不需要使用Java。从技术上讲,Java甚至不需要运行。但是,由于共享内核的内部使用Java来产生CPU,如果Java没有运行,那么设置周期性任务将导致内核使CPU持续忙碌。周期性任务功能包含在J/Link中,因为它是ShareKernel的一个简单扩展,而且它在与Java的关联中确实有一些很好的用途。

关于周期性任务最后要注意的是,它们不会导致在前端出现输出。看看这个尝试。

id = AddPeriodical[Print["hello"], 10];

程序员期望每10秒钟就能在他的笔记本上打印出hello,但什么也没有发生。在执行期刊期间,$ParentLink没有分配给前端(或Java)。结果或副作用,如Print输出、消息或图形,将消失在以太中。

在继续之前,清理您创建的周期性任务。

RemovePeriodical[Periodicals[]];

一些特殊的数字类

用Wolfram语言代码实现Java接口

您已经看到了J/Link如何允许您编写使用现有Java类的程序。您还看到了如何通过MathListener类回调到Wolfram语言来绑定Java用户界面的行为。您可以将这些MathListener类(例如MathActionListener)看作是将其行为“代理”给任意用户定义的Wolfram语言代码的类。这就好像您有一个用Wolfram语言编写其实现的Java类。这个功能非常有用,因为它极大地扩展了纯用Wolfram语言编写的程序集,而无需编写自己的Java类。

ImplementJavaInterface["interfaceName",{"methName"->"mathFunc",\[Ellipsis]}]	\[SpanFromLeft]
	create an instance of a Java class that implements the named Java interface by calling back to the Wolfram Language according to the given mappings of Java methods to Wolfram Language functions

完全使用Wolfram语言实现Java接口。

如果能够接受这种行为并将其一般化就好了,这样您就可以接受任何Java接口并通过对Wolfram语言函数的回调来实现它的方法,并且不需要编写任何Java代码就可以完成所有这些工作。J/Link 2.0中新增的ImplementJavaInterface函数允许您这样做。通过一个具体的示例,这个函数更容易理解。假设您正在编写一个使用J/Link显示带有Swing菜单的Java窗口的Wolfram语言程序,并且希望用Wolfram语言编写菜单行为的脚本。Swing JMenu类向已注册的MenuListener触发事件,因此您需要的是一个通过调用Wolfram语言实现MenuListener的类。快速浏览一下有关MathListeners的部分,就会发现J/Link并没有为您提供MathMenuListener类。您可以选择编写自己的此类实现,实际上这将非常简单,甚至是微不足道的,因为您将使其成为MathListener的子类,并继承所需的几乎所有功能。为了方便讨论,假设您选择不这样做,可能是因为您不了解Java,或者不想处理该解决方案所需的所有额外步骤。相反,您可以使用ImplementJavaInterface用一行Wolfram语言代码创建这样的Java类。

mathMenuListener =
	ImplementJavaInterface["javax.swing.event.MenuListener",
		{"menuSelected" -> "menuSelectedFunc",
		 "menuCanceled" -> "menuCanceledFunc",
		 "menuDeselected" -> "menuDeselectedFunc"}
	];
myMenu@addMenuListener[mathMenuListener];

...

(* Later, define the three Mathematica event-handler functions: *)
menuSelectedFunc[menuEvent_] := ...

menuCanceledFunc[menuEvent_] := ...

menuDeselectedFunc[menuEvent_] := ...

ImplementJavaInterface的第一个参数是要实现的Java接口或接口列表。第二个参数是一组规则,这些规则将来自某个接口的Java方法的名称与要调用以实现该方法的Wolfram语言函数的名称关联起来。将使用与Java方法相同的参数调用Wolfram语言函数。ImplementJavaInterface返回的是新创建的类的Java对象,该类实现了命名接口。使用它就像通过调用JavaNew或通过任何其他方法获得的任何JavaObject一样。这就好像您编写了自己的Java类,通过调用相关的Wolfram语言函数来实现命名接口,然后调用JavaNew来创建这个类的实例。

没有必要将接口中的每个方法与Wolfram语言函数关联起来。映射列表之外的任何Java方法都将得到一个返回null的默认Java实现。如果这不是一个合适的方法返回值(例如,如果该方法返回一个int),并且该方法在某个时候被调用,将抛出异常。通常,此异常将传播到Java调用堆栈的顶部并被忽略,但建议您实现Java接口中的所有方法。

ImplementJavaInterface函数利用了Java 1.3中引入的“动态代理”功能。它不能在1.3之前的Java版本中工作。所有与Mathematica 4.2及更高版本绑定的Java运行时都在1.3或更高版本。如果您有Mathematica 4.0或4.1,ImplementJavaInterface函数是确保您的系统有最新的Java运行时的另一个原因。

乍一看,ImplementJavaInterface函数似乎给了我们用Wolfram语言编写任意Java类的能力,在某种程度上这是正确的。您不能做的一件重要的事情是扩展或子类现有的Java类。您也不能添加您正在实现的接口中不存在的方法。事件处理程序类是使用此功能的类类型的一个很好的例子。您可能认为ImplementJavaInterface使MathListener类过时了,而且它们的功能确实可以用它复制。MathListener类对于1.3之前的Java版本仍然很有用,但最重要的是,它们对于编写调用Wolfram语言的纯Java程序很有用。在调用Wolfram语言的Java程序中使用通过ImplementJavaInterface用Wolfram语言实现的类是可能的,但相当麻烦。如果您想要一个双重用途的类,并且从Wolfram语言和Java语言中使用都一样容易,那么您应该编写自己的MathListener子类。选择使用ImplementJavaInterface而不是编写自定义Java类的一个不太好的原因是,您担心会使应用程序复杂化,因为除了Wolfram语言代码外,还要求它包含自己的Java类。正如在“部署使用J/Link的应用程序”中解释的那样,在应用程序中包含支持Java类非常容易。您的用户不需要任何额外的安装步骤,也不需要修改Java类路径。

编写自己的可安装Java类

序言

前面的部分展示了如何加载和使用现有的Java类。这使Wolfram语言程序员可以立即访问整个Java类领域。但是,有时现有的Java类是不够的,您需要编写自己的Java类。

J/Link基本上消除了Java和Wolfram语言之间的边界,允许您来回传递任何类型的表达式,并以有意义的方式在Wolfram语言中使用Java对象。这意味着,在编写自己的Java类从Wolfram语言调用时,通常不需要做任何特殊的事情。编写代码的方式与只从Java使用类的方式完全相同。(这条规则的一个重要例外是,因为从Wolfram语言调用Java相对比较慢,所以您可能需要在设计类时不需要从Wolfram语言调用过多的方法来完成工作。这个问题在“Java调用的开销”中有详细的讨论。)

在某些情况下,您可能希望对与Wolfram语言的交互施加更直接的控制。例如,您可能希望一个方法返回与Wolfram语言不同的内容,而不是方法本身返回的内容。或者,您可能希望该方法不仅返回一些东西,而且还在Wolfram语言中触发副作用——例如,在特定条件下打印一些东西或显示一条消息。您甚至可以在方法返回之前与Wolfram语言建立一个扩展的“对话框”,也许可以调用Wolfram语言中的多个计算并读取它们的结果。您可能还想编写一个MathListener类型的类,它在Java中触发某个事件时调用Wolfram语言。

如果您不想做这些事情,那么您可以忽略此部分。J/Link的全部意义在于,不需要关心通过WSTP与Wolfram语言的交互。大多数想要编写用Wolfram语言使用的Java类的程序员只编写Java类,没有考虑Wolfram语言或J/Link。那些想要更多控制权,或者想了解更多关于J/Link的可能性的程序员,请继续阅读。

本节讨论的问题需要了解WSTP编程(或者更准确地说,使用使用WSTP的Java方法的J/Link编程),这在“编写使用Wolfram语言的Java程序”中详细讨论。你满足这些方法和问题的结果是错误的但是有用的二分法,之间的“介绍”,指出使用WSTP写“安装”功能从Wolfram语言调用和使用WSTP Wolfram语言编写前端。WSTP始终以相同的方式使用,只是在可安装的情况下,实际上为您处理了所有WSTP。本节介绍如何超越这个默认行为,因此您将直接调用J/Link来读取和写入链接。因此,您将在本节中遇到直到“编写使用Wolfram语言的Java程序”才会解释的概念、类和方法。

本节中的一些讨论将比较和对比用C编写可安装程序的过程。这是为了帮助有经验的WSTP程序员理解J/Link是如何工作的,也为了让您相信J/Link是使用C、c++或FORTRAN更好的解决方案。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值