这篇文章是编写评估命令的第四篇,它演示了如何用另一种语言(如C、C 或Java)编写的代码插入Stata。这种技术被称为编写插件,或者为Stata编写一个动态链接库(DLL)。
在这篇文章中,我在Java中编写了一个插件,它实现了由mymean11.ado中mymean_work()所执行的计算,这个内容在之前的文章中有详细的讲解。
这篇文章类似于《在Stata中编写估计命令:编写C语言插件》和《在Stata中编写评估命令:编写C 插件》,不同之处在于插件代码是在Java而不是C或C 。
编写hello-world JAVA插件
在进行任何计算之前,我都会演示如何编写和编译一个与Stata通信的Java插件。Code block 1包含myhellojava.ado的代码,调用Java插件在Stata中显示Hello from Java。
第2行从sfi-api.jar导入Stata函数接口(SFI),我从Stata分发的Stata/utilities/jar目录中复制到当前目录。您应该将安装在Stata的版本复制到Java编译器需要的目录中。
第3行定义了HelloFromJava的公共类别,在myhellojava.ado的第6行中指定。
第4行定义helloJavaWork()方法,它是这个插件的入口点。入口方法的签名必须是这种形式。该方法是public static。这个方法可以返回一个int,并且它能接受字符串数组。
Stata将返回的int视为返回码,0意味着一切都很顺利而不是识别出一个错误条件。如果返回的int不是0,那么Stata将退出返回int中指定的错误。字符串数组包含javacall传递给插件的参数。
第5行使用SFI方法SFIToolkit.displayln()来显示来自Hello from Java的字符串,并附加一行返回。
第6行返回0到Stata,因此Stata不会以错误代码退出。
接下来将讨论如何从HelloFromJava.java创建JAR文件hellojavawork.jar。为了便于讨论,我使用了Java命令行工具。
在包含myhellojava.ado和HelloFromJava.java的目录中,还有从Stata/utilities/jar目录中复制的sfi-api.jar。在我的OS X Mac上安装了命令行开发者工具,我通过输入javac –release 8 -classpath sfi-api.jar HelloFromJava.java,从HelloFromJava.java和sfi-api.jar中使用javac创建HelloFromJava.class。
在本文中,Stata与Java 8一起工作,尽管Java 9已经发布初始版本。我必须指定-release 8,因为我的机器上的命令行工具默认使用Java 9。如果javac在您的机器上默认为Java 8,您可以省略这个选项。
要从HelloFromJava.class创建JAR文件hellojavawork.jar,我输入了
jar cf hellojavawork.jar HelloFromJava.class
javac和jar的这些命令可以在所有平台上工作,您可以将一个平台上的jar文件分发到其他平台上。这种跨平台的兼容性是Java的一大优势。为了确保Stata命令discard删除当前装载到Stata的所有Java类,我也删除了javac编译的.class,然后再使用Java类运行ado-command。
在OS X MAC操作系统上,我输入
rm HelloFromJava.class
已经创建hellojavawork.jar并且删除了HelloFromJava.class。然后可以在Stata中执行myhellojava.ado
示例1:myhellocjava
如果我更改HelloFromJava.java,重新编译、重制JAR文件、删除.class文件,在运行myhellojava之前在Stata中输入dicard,那么Stata将找到Java类的新版本。discard可以工作是因为Stata的Java插件接口使用自定义类加载器而不是Java system类加载器来加载插件的JAR文件。当您在Stata的当前工作目录中留下您的.class文件时,就会出现问题,因为java-system类加载器会在Stata的自定义加载器运行之前找到并加载.class文件。这个问题避免了Stata的discard命令,这意味着您必须重启Stata来卸载旧的类定义并加载新的版本。为了防止这个问题,在调用Java插件之前删除.class文件(或者,您可以在Stata的当前工作目录之外使用Java代码,但是我更喜欢删除.class文件,因为一旦我有了JAR文件,它们就是多余的)。
为简单起见,我将sfi-api.jar,HelloFromjava.java, myhellojava.ado,和hellojavawork.jar放在同一个目录中。对于大型项目,我将把.ado和.jar文件放在Stata的ADOPATH目录中,并使用我的IDE来管理我放置sfi-api.jar和Java源文件的位置。对于本文中的示例,我使用了sfi-api.jar、所有的.ado文件、所有的Java源文件以及创建的.jar文件都放在一个目录中。
在插件中访问Stata数据
helloJavaWork()使Stata显示插件中创建的内容。下一步是让插件访问Stata中的数据。为了说明这个过程,我将讨论mylistjava.ado使用插件来列出对指定变量的观测值。
让我们先看看ado-code。
第6行,syntax创建三个本地宏。它将用户指定的变量放入本地宏varlist中,将用户指定的任何if条件放入本地宏if中,将用户指定的任何in范围放入到本地宏in中。我将max=3指定到syntax,将变量的数量限制为3。这个限制是愚蠢的,我不需要它作为Stata/Mata程序的例子,但是它可以简化Java插件的示例。
第7行中,marksample创建了样本包容变量,并将样本包含变量的名称放到本地宏touse中。每个被排除观测值的样本包含变量为0,而每个非排除观测值的包含变量为1。marksample使用本地宏varlist中的变量、本地宏if中的if条件以及本地宏中in范围中in来创建样本包含变量(所有三个本地宏都是由syntax创建的)。如果本地宏varlist中的任何变量都包含一个缺失值,被本地宏if的条件排除,或者被本地宏in的范围排除在外,则会排除一个观测结果。如果没有被排除的话,那么样本包含变量就是1。
第9行,我进一步通过显示变量的名称来简化Jave插件,这些变量的值是由方法插件列出的。
第10行,javacall调用了这个插件。入口点是在MyListJava类中的myListJW()方法,由JAR文件myListJW.JAR定义。因为“varlist”是指定的,SFI方法将能够访问本地宏varlist中包含的变量。因为if “touse”是指定的,如果touse中的样本包含变量为0,那么SFI方法Data.isParsedIFTrue()将返回0,如果样本包含变量为1,那么会返回1。因为“in”被指定,SFI方法Data.getObsParsedIn1()和Data.getObsParsedIn2()分别返回在范围内指定的任何用户的第一个和最后一个观测值。
指定“in”不是识别用户指定的样本所必需的,因为if “touse”已经指定了这个样本包含的信息。然而,指定“in”可以极大地减少循环中对数据的观测范围,从而加快代码的速度。
Code block 4中的MyListJava的代码。在包含MyListJava.java和sfi-api.jar的目录中,我创建了mylistjw.jar,在我的Mac上输入以下三行代码:
javac –release 8 -classpath sfi-api.jar MyListJava.java
jar cf mylistjw.jar MyListJava.class
rm MyListJava.class
解释了MyListJava.java如何说明Stata的Java插件的结构,并且讨论了代码中使用的SFI方法。
如果Stata运行顺利,myListJW.java会返回0,如果出错,它会返回一个非零错误代码。
因为所调用的方法都不能失败,唯一的错误条件是遇到缺失值,这些值在第30-34行中处理。在出现错误的情况下,第32行使用SFIToolkit.errorln()来确保Stata显示错误消息,并以红色显示。SFIToolkit.display()是代码中其他地方使用的标准显示方法。
Java插件使用SFI中定义的方法从或写入Stata对象。myListJW()不会返回任何结果,所以它有一个简单的结构。
� 使用SFI方法从Stata中指定的数据样本中读取数据。
� 使用标准的Java和SFI方法,使Stata显示对指定样本的变量的观测,并保留了在指定样本中有多少观测值的计数器。
� 使用标准的Java和SFI方法来显示样本中的第一个观测值、样本中最后的观测值,以及在指定的样本中有多少观测值。
现在我们来讨论MyListJava.java部分。
第10、12和14行使用SFI Data类方法。Data.getParsedVarCount()将varlist中指定的变量数量放入nVariables中。Data.getObsParsedIn1()将in范围指定的第一个观测值放入firstObs。Data.getObsParsedIn2()将in范围指定最后一个观测值放入lastObs。如果in范围没有指定到javacall,那么firstObs将包含1,而lastObs将包含数据集中观测值数量。
firstObs、lastObs和所有Stata观测数量的Java变量都是long类型的,因为比适合int类型的Java变量,Stata数据集可以包含更多的观测数量。
第20-22行确保我们跳过了被mylistjava.ado第10行指定的javacall的if限制排除的观测结果。为了说明一些细节,请看示例2。
示例2:mylistjava
在第20行中,当指定给javacall的if限制的obs观测值是1时,Data.isParsedIfTrue(obs)返回1,否则返回0。在mylistjava.ado的第10行中,我们看到传递给javacall的if限制是if“touse”。正如上面所讨论的,对于排除变量,本地宏touse的样本包含变量是0,对于非排除变量是1。
包含了mylistjava.ado第10行in范围,因此,MyListJava.java第19行的观测值循环,从in范围内指定的开头到结尾。在示例2中,不是循环auto数据集中所有74个观测值,而是循环MyListJava.java第19行的第2个观测值到第10个观测值。
在示例2中,6个观测值的样本包含变量为1,而其他68个观测值的变量为0。in 2/10的范围不包括观测值1和11-74的观测值。在前10个观测值中,2被排除在外,因为rep78缺失。排除一个观测值,因为trunk是21。
为了进行比较,在示例3中列出了2到10之间的9个观测值。
示例3:list
回到MyListJava,我们可以看到第28-29行说明了如何将Stata数值变量的值放入Java变量中。请注意,Data.getNum()返回所有Stata数值变量类型double。在示例2中,mpg、trunk和rep78都是Stata中的int类型。
如果一个变量中任何观测值包含一个缺失值,那么第30-34行会导致myListJW()退出,错误信息为416。这些行是多余的,因为指定给javacall的touse样本包含变量,排除包含缺失值的观测值。我包含了这些行来说明如何安全地从插件内部排除缺失值,并重申Java代码必须小心处理缺失值。Stata缺失的值是Java中是有效的双精度数。如果在计算中包含Stata缺失值,就会得到错误的结果。
估算Java插件中的平均值
现在,我将讨论ado命令mymeanjava,在MyCalcs类中使用myWork()方法来实现mymean11.ado中执行mymeanwork()的计算。
在code block 5中,Mymeanjava的代码来自于mymeanjava.ado。
这个程序的大概结构跟mymean10.ado和mymean11一样,这个内容在《在Stata中编写估计命令:编写插件》讨论过。整体来看,mymeancpp.ado可以:
� 解析用户输入
� 创建样本包含变量
� 为保存结果的对象创建临时名称
� 调用工作程序来进行计算
� 保存工作程序返回的结果到e()中,并
� 显示结果
Mymeanjava.ado和mymean11.ado的主要区别在于,工作程序是一个Java插件,而不是一个Mata函数。
第6行和第7行与mylistjava.ado的代码相同。想要了解这些行是如何创建本地宏varlist的,在本地宏touse中样本包含变量,本地宏in包含任何用户指定的范围,可以参看Getting access to the Stata data in your plugin中对mylistjava.ado的说明。
第8行将临时名称放入本地宏b、V和N中。我们可以使用这些名称来计算由Java插件计算的结果,并且知道我们不会覆盖用户保存在全局Stata内存中的任何结果(回想Stata矩阵和标量是Stata中的全局对象)。请参阅Programming an estimation command in Stata: A first ado-command)。此外,Stata还会在mymeanjava终止时,将tempname创建的临时名称中的对象删除。
mymeanjava中的第10行类似于mylistjava.ado中的第10行。在这种情况下,myWork()是在类MyCalcs中定义的入口方法,位于JAR文件mycalcs.jar中。上面已经讨论了varlist, if,’touse’和’in’的详细情况。新的情况是,我们使用args(b,V,N)将临时名称传递到myWor()中。
myWork()可以:
� 执行计算
� 将估计的方法放入一个新的Stata矩阵中,该矩阵的名称在本地的宏b中
� 将估计方差(协方差)的估计量(VCE)放入一个新的Stata矩阵中,该矩阵的名称在本地宏V中
� 将样本内的观测数量放入Stata标量中,该标量的名称位于本地宏N中
第13-15行将变量名称放在估计方法向量的列条纹上和VCE矩阵的行和列条纹上。第16-18行将结果保存在e()中,第19行显示结果。
在讨论myWork()的细节之前,我们先创建插件并运行一个示例。
在一个包含MyCalcs.java,MyCalcsW.java,MyMatrix.java,MyLong.java和sfi-api.jar的目录中在Mac电脑中输入以下内容创建了mycalcs.jar。
javac --release 8 -classpath MyCalcs.java MyCalcsW.java MyMatrix.java MyLong.java sfi-api.jar
jar cf mycalcs.jar MyCalcs.class MyCalcsW.class MyMatrix.class MyLong.class
rm MyCalcs.class MyCalcsW.class MyMatrix.class MyLong.class
创建完mycalcs.jar,运行示例3。
示例4:mymeanjava
现在讨论Java代码的一些方面,从code block 6中的MyCalcs.java开始。
MyCalcs.java只包含入口方法myWork()。综上所述,myWork()可以执行以下任务:
1把名称作为参数传递给Java String对象的实例中,这些对象可以被传递到SFI方法中。
2将指定Stata变量的数量放入用于循环变量的Java变量中。
3将样本观测的范围放入Java变量中,用于循环观测值。
4创建MyMatrix类的bmat和vmat实例来保存样本平均值和VCE。
5创建了MyLong类的nObs实例,它将保存样本观测值的数量。
6使用MyCalcsWmyAv()和MyCalcsWmyV()方法来计算结果并保到在bmat、vmat和nObs中。
7使用MyMatrix类的CopyCtoStataMatrix()方法来将bmat和vmat的结果复制到新的Stata矩阵。新的Stata矩阵的名称是传递给myWork()的第一个和第二个参数。
8使用SFI方法Scalar.setValue()将结果从nObs复制到新的Stata标量中,该标量的名称是传递给myWork()的第三个参数。
MyCalcs.java很容易读懂,因为我把所有的细节都放到了MyMatrix、MyCalcsW和MyLong类中,下面我将对此进行讨论。
如同Stata的所有Java插件一样,myWork()使用返回码rc来处理错误条件。如果一切顺利,每个调用的方法都返回0,如果它不能执行所请求的工作,它会返回一个非零错误代码。如果返回的代码不是0,myWork()会立即返回到Stata中。与错误条件相关联的错误消息由这些方法来显示。
在(3)中,我注意到bmat和vmat是MyMatrix类的实例。样本平均值和VCE最好保存在矩阵中。为了事情简单和自包含,我定义了一个简单的矩阵类MyMatrix,它使用row-major保存,并且只使用我需要的方法。除了copyJavatoStataMatrix()方法之外,MyMatrix的代码是标准Java,可以在code block 7中看到。
第33-58行包含copyJavatoStataMatrix()的代码。第40和49行使用了我还没有讨论过的SFI方法。Matrix.createMatrix(字符串sname、int rows、int cols、double val)使用rows行和cols列创建了一个新的Stata矩阵。这个矩阵的每一个元素都被初始化为值val。sname包含这个Stata矩阵的名称。
Matrix.storeMatrixAt(字符串sname,int i,int j,double val)将值val保存到Stata矩阵行i和列j中,该矩阵的名称包含在sname中。基于零的索引给出了行i和列j。
在(4)里,我注意到,我使用了MyLong类的一个实例来保存样本观测值的数量。Java中的原始类型不能通过引用传递,标准的包装类型是不可变的,所以我创建了long counter,nObs传递到MycalcsW.myAve()中。当MyCalcsW.myAve()完成时,nObs包含样本观测值的数量。MyLong的代码是标准Java,由code block 8中给出。
在(5)中,我注意到MyCalcsW.myAve()和MyCalcsW.myV方法计算了样本平均值和VCE。这些方法在类MyCalcsW中,它的代码由code block 9给出。
MyCalsW.myAve()是Mata函数MyAve()在Java中的实现,《在Stata中编写估计命令:编写插件》这篇文章中有过讲解。它将样本平均值放入MyMatrix类的bmat实例中,并将样本中观测的数量放入nObs中。这个方法的大部分代码是标准Java,或者使用我已经讨论过的SFI方法。第18、34和38行值得讨论。
第18行MyCalcsW.java使用MyLong的方法incrementValue()来增加保存在nObs中的观测数量。它使nObs的当前值递增1。
第34行使用MyMatrix方法incrementByValue()。当计算样本均值并将其保存在向量名称为b的jth元素中时,您需要保存b[j] value到b[j]中。换句话说,通过value,在b中增加jth元素的数量。通过value,在bmat中bmat.incrementByValue(0,var-1, value)增加元素var-1。
第38行使用MyMatrix的divideByScalar()方法。bmat.divideByScalar(z)将bmat的每个元素替换为可以除以z的元素。
MyCalsW.myV()是Mata函数MyV()在Java中的实现,《在Stata中编写估计命令:编写插件》这篇文章中有过讲解。它将VCE放入MyMatrix类的vmat实例中。这个方法的大部分代码是标准Java,或者使用我已经讨论过的方法。第72、77和85行使用MyMatrix方法storevalue()和getValue()。vmat.storeValue(i,j,z)将z值保存在MyMatrix的vmat实例中(i,j)。vmat.getValue(i,j)返回保存在MyMatrix中vmat实例中的元素(i,j)中的值。
完成和撤销
这篇文章展示了如何实现一个Java插件,它可以在mymean10.ado和mymean11.ado中执行Mata工作函数的计算。这个内容在《在Stata中编写估计命令:编写插件》中讨论过。