三、Java RMI使用
RMI 应用通常有两个分开的程序组成,一个服务端程序和一个客户端程序。一个典型的服务端程序创建一些远程对象,使得对这些远程对象的引用可以被访问,等待客户端调用这些远程对象提供的方法。一个典型的客户端程序获取远程引用,指向一个或者多个服务端上的远程对象,然后调用这些远程对象所提供的方法。通常我们称这为分布式对象应用程序。
3.1 RMI的工作方式
分布式对象应用程序需要做的事情:
l 查找(定位)远程对象。 应用程序可以使用各种不同的机制取得远程对象的引用。比如应用程序可以通过 RMI 提供的简单的命名工具, RMI 注册。或者应用程序可以传递和返回远程对象作为远程方法调用的一部分。
l 和远程对象通信。 远程对象之间通信的细节由 RMI 处理,对程序员来说远程对象的通信和通常的 Java 方法调用没有区别。
l 加载传递过来的远程对象的类定义。 因为 RMI 允许对象双向传递,因此它提供了加载对象类定义和传递对象数据的机制。
下图(图3.1)描述了一个使用 RMI 注册机制取得远程对象引用的RMI分布式应用。 Server调用注册机制将一个名字和远程对象关联(或者叫绑定)。Client 根据名字在Server的注册机制里面查找远程对象,获得远程对象的引用,并调用远程对象的方法。下图还描述了这样的情况,RMI 系统使用Web Server,从Server 到Client或者从Client到Server,加载所需对象的类定义。
图3.1 RMI工作方式
3.1.1 动态类加载
RMI的一个独特的核心优势就是能够加载一个未在接收端的Java虚拟机( JVM )中定义的类。在一个Java虚拟机中定义的一个对象所有的类型和行为,能够从这个Java虚拟机传递到另外一个Java虚拟机,甚至可以是远程的Java虚拟机。RMI根据对象的真实类型传递对象,所以当对象被传递到另一个Java虚拟机时对象的行为不会改变。这个特性使得新的对象类型和行为能够被引入到一个远程的Java虚拟机当中,也就是说动态扩展了应用程序的行为。下面的compute engine例子就是使用这种能力往分布式程序中引入新的行为。
3.1.2 远程对象
正如其他很多Java应用程序一样,一个构建在RMI之上的分布式应用也是由接口和类组成的。接口声明方法,类实现在接口中定义的方法,也许还会声明额外的方法。在分布式应用中,一些方法可能存在于某些Java虚拟机中但是却不在另一个Java虚拟机中。如果一个对象的方法能够在不同的Java虚拟机之间被调用,那么此对象被称作远程对象(remote objects)。
一个普通对象可通过实现远程接口( java.rmi.Remote )变成远程对象,这个远程接口有如下特征。
l 一个远程接口扩展 java.rmi.Remote 接口
l 每个远程接口里声明的方法除了声明抛出本身应用特定的异常之外,都要声明抛出 java.rmi.RemoteException 异常
当对象从一个Java虚拟机传递到另一个Java虚拟机时,RMI区别对待远程对象和非远程对象。当RMI传递一个远程对象到另一个JVM时,它实际上传递的是此远程对象对应的存根(stub)对象,而不是传递这个对象的拷贝。这个存根对象担当远程对象的代表或者代理的角色,为client提供到远程对象的引用。Client调用所获得的stub的方法,而这个stub则负责执行远程对象里这个方法的调用。
一个远程对象的stub(存根)实现了与这个远程对象所实现的远程接口的相同方法集合。这个特性使得stub能够被转型为远程对象实现的远程接口。然而,也只有那些在远程接口里声明的方法才能被接收端的JVM调用。
在客户端进行远程方法调用时,RMI框架会把遇到的网络通信失败转换为RemoteException,客户端可以捕获这种异常,并进行相应的处理。
3.1.3 远程方法中的参数与返回值传递
RMI规范对参数及返回值的传递作出了以下规定:
l 只有基本数据类型、远程对象及可序列化的对象才能作为参数或者返回值进行传递。
l 如果参数或返回值是一个远程对象,那么把它的存根对象传递到接收方。也就是接收方得到的是远程对象的存根对象。
l 如果参数或返回值是可序列化的对象,那么直接传递该对象的序列化数据。也就是说,接收方得到的是发送方的可序列化的对象的复制品。
l 如果参数或返回值是基本数据类型,那么直接传递该数据的序列化数据。也就是说,接收方得到的是发送方的基本数据类型的复制品。
3.1.4 远程对象的equals()、hashCode()和clone()方法
在Object对象中定义了equals()、hashCode()和clone()方法,这些方法没有声明抛出RemoteException。Java语言规定了当子类覆盖父类方法时,子类方法不能声明抛出比父类方法更多的异常。而RMI规范要求远程接口中的方法必须声明抛出RemoteException异常,因此无法在远程接口中定义equals()、hashCode()和clone()方法。这意味着一个远程对象的这些方法永远只能作为本地方法,被本地Java虚拟机内的其它对象调用,而不能作为远程方法,被客户端远程调用。
3.1.5 分布式垃圾收集
在Java虚拟机中,对于一个本地对象,只要不被本地Java虚拟机中的任何变量引用,它就会结束生命周期,可以被垃圾回收器回收。而对与一个远程对象,不仅会被本地Java虚拟机中的变量引用还会被远程引用。如将远程对象注册到rmiregistry注册表时,rmiregistry注册表则持有它的远程引用。
RMI框架采用分布式垃圾收集机制(DGC,Distributed Garbage Collection)来管理远程对象的生命周期。DGC的主要规则是,只有当一个远程对象不受任何本地引用和远程引用,这个远程对象才会结束生命周期。
当客户端获得了一个服务器端的远程对象存根时,就会向服务器发送一条租约通知,告诉服务器自己持有这个远程对象的引用了。此租约有一个租约期限,租约期限可通过系统属性java.rmi.dgc.leaseValue来设置,以毫秒为单位,其默认值为600 000毫秒。当到达了租约期限的一半时间,客户端如果还持有远程引用,就会再次向服务器发送租约通知。如果租约到期后服务器端没有继续收到客户端的新的租约通知,服务器端就会认为这个客户已经不再持有远程对象的引用。
有时,远程对象希望在不再受到任何远程引用时执行一些操作,如释放所占用的资源,以便安全的结束生命周期。这样的远程对象需要实现java.rmi.server.Unreferenced接口,该接口有一个unreferenced()方法,远程对象可以在这个方法中执行释放占用的相关资源的操作。当RMI框架监测到一个远程对象不再受到任何远程引用时,就会调用的这个对象的unreferenced()方法。
3.2 通过RMI创建分布式应用
通过使用RMI开发一个分布式应用遵循下面几个步骤:[3]
1. 设计实现分布式应用的组件
2. 编译源代码
3. 使得你的类在网络上可访问
4. 启动应用程序
3.2.1 设计实现应用程序组件
首先决定应用程序的体系结构,包括哪个组件是本地对象,哪个组件远程可访问。这一步骤包括:
l 定义远程接口. 一个远程接口指定了哪些方法能够被client远程调用。Client程序针对远程接口编程,而不是针对实现了这些远程接口的类。这些接口的设计包括了如何声明远程方法所需参数的对象类型,以及远程方法返回值类型。
l 实现远程对象. 远程对象需要实现至少一个远程接口。远程对象也可以实现其它的接口,但这个接口里声明的方法只在本地JVM可用。如果任何一个本地类要被用作为这些(远程)方法的参数或者是返回值,这些类也需要被实现。
l 实现客户端. 在远程接口定义好之后,使用远程对象的客户端可以在任何时候被实现,任何时候的意思包括在远程对象部署之后。
3.2.1.1 实现服务器端
ComputeEngine是一个服务器上的远程对象,从客户端接受任务,执行任务之后返回结果。这些任务是在服务端运行的机器上执行的。这种类型的分布式应用程序使得许多客户端使用性能强劲的机器或者是拥有特殊硬件资源的机器。
ComputeEngine的奇特之处在于它运行的任务不需要在它写代码或者运行的时候定义。新的任务可以随时被创建然后交由其执行。一个任务唯一的要求就是任务类必须实现一个特定的接口。需要完成的任务的代码能够被RMI系统下载到ComputeEngine。然后ComputeEngine运行这个任务,使用ComputeEngine所运行的机器上的资源。
执行任意任务的能力是由Java平台的动态特性保证的,这个动态特性又被RMI扩展到网络世界。RMI动态装载任务代码到ComputeEngine所在的JVM,然后运行这个任务,而不需要预先知道实现这个任务的类。这样一个有动态加载代码能力的应用通常被称为 behavior-based application 。这种应用程序通常要求允许代理的基础结构。有了RMI,这种应用构成了Java平台上分布式计算的基本的机制。
l 设计远程接口
compute.Compute接口定义了远程可访问的部分,下面是Compute接口的源代码。通过继承java.rmi.Remote接口,Compute接口表明自己是接口方法可以被另一个JVM调用的接口。所以实现该接口的对象就是远程对象。
package compute;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Compute extends Remote {
<T> T executeTask(Task<T> t) throws RemoteException;
}
作为远程接口的一员,executeTask方法是一个远程方法。因此该方法需要定义为能抛出java.rmi.RemoteException异常。这个异常由RMI系统调用一个远程方法时抛出,表示通讯失败或者是协议错误发生。RemoteException是一个受检查的异常,所以任何调用远程方法的代码需要处理这个异常,要么捕获该异常,要么声明抛出子句。
ComputeEngine需要的第二个接口是Task接口,这个接口也是executeTask的类型参数。Compute.Task接口定义了ComputeEngine和它要执行的工作直接的接口,提供了开始这个工作的方法。下面是Task接口的源代码:
package compute;
public interface Task<T> {
T execute();
}
Task接口就定义了一个方法,execute,无参数,也没有异常。因为这个接口没有继承Remote接口,所以方法也无需声明抛出java.rmi.RemoteException子句。
RMI使用Java对象序列化机制在JVM之间以值传递方式传输对象。对象要能被序列化就需要实现java.io.Serializable这个标识接口,因此实现Task接口的类也要实现java.io.Serializable接口,作为Task执行结果的对象的类也必须要实现这个接口。
不同种类的任务都能被一个Compute对象执行,只要这些任务都实现了Task接口类型。实现这个接口的类可以包含任何执行计算所需要的数据和其他执行计算所需要的方法。
RMI假定Task对象由Java语言程序编写,ComputeEngine先前不知道的Task对象的实现,在需要时由RMI下载到ComputeEngine所在的JVM。这个能力使得ComputeEngine的Client能够定义新的将要在Server上运行的任务,而不需要代码显式的被安装在Server机器上。
l 实现ComputeEngine类 (服务类)
概括的说,一个实现远程接口的类至少需要做以下步骤:
Ø 声明要实现的remote interface
Ø 为每个remote对象的定义构造函数
Ø 实现remote interface里的远程调用方法
一个RMI 的server端程序需要创建初始的远程对象并把他们发布到RMI的环境,使其能够接受远程调用。这一步骤可以包括在远程对象某个方法中,也可以在其他类的实体对象中。这一步骤要做如下步骤:
Ø 创建并安装一个security manager
Ø 创建并发布一个或者多个远程对象
Ø 使用RMI registry注册至少一个远程对象
实现安全管理器的目的是RMI框架利用Java安全管理器来确保远程方法调用的安全性。
下面是ComputeEngine的全部实现。这个类实现了远程调用接口(remote interface)Compute,还有main方法,用来安装compute engine。下面是这个类的源代码:
package engine;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import compute.Compute;
import compute.Task;
public class ComputeEngine implements Compute {
public ComputeEngine() {
super();
}
public <T> T executeTask(Task<T> t) {
return t.execute();
}
public static void main(String[] args) {
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try {
String name = "Compute";
Compute engine = new ComputeEngine();
//使用匿名端口导出远程对象,以便能够接收传入的调用。
//返回远程对象的stub
Compute stub =
(Compute) UnicastRemoteObject.exportObject(engine);
Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);
System.out.println("ComputeEngine bound");
} catch (Exception e) {
System.err.println("ComputeEngine exception:");
e.printStackTrace();
}
}
}
远程方法需要的参数或者是返回值几乎可以是任何对象,包括本地对象,远程对象,元数据类型。更确切的说,任何实体对象只要是符合如下类型的实例都能作为远程对象可用的参数或者返回值,这些类型包括元数据类型,远程对象或者是一个可序列化对象(实现了java.io.Serializable的对象)。
有些对象类型不能满足上面的要求,因此不能传递给远程方法作为参数,或作为远程方法的返回值。这些对象如线程或者是文件描述符,它们只在单个地址空间内是有意义的,许多核心的类都实现了Serializable接口。
如何传递参数和获得返回值遵循如下的约定:
Ø 远程对象本质上通过引用传递。一个远程对象的引用包含在此对象所对应的存根对象中,在向注册处注册时传递就是这个存根对象,而客户端通过名字在注册出查找获得的也是这个存根对象。这个存根可作为一个实现了远程接口的客户端的代理。
Ø 本地对象通过值拷贝传递,其中使用对象序列化的技术。默认的拷贝方法是,拷贝除了被标识为static和transient的所有的域。默认的序列化行为可以被重载。
通过引用传递远程对象意味着所有对这个对象状态的修改都会影响到原来的远程对象。当传递远程对象引用的时候,只有远程接口提供的方法才能被接收者使用。任何在实现类中定义的方法或者类实现的非远程接口中定义的方法,接收端是不可用的。
在远程方法调用的参数返回值中,非远程对象的对象通过值拷贝传递。因此,在接受端的JVM中一个对象的拷贝被创建。任何对对象状态的更改只会影响到拷贝的对象,而不会影响发送端的原始对象实例。任何由发送端对对象状态的更改,只会影响发送端原始对象的实例,而不会影响接收端该对象的拷贝对象。
l 实现服务器 (由服务类中的main方法完成)
ComputeEngine中最复杂的代码就是这个main方法。Main方法用来启动ComputeEngien因此需要做必要的初始化,为server接受client的请求做准备。这个方法不是远程方法,就是说它不能被另一个JVM调用。因为main方法声明成静态的,这个方法不会和一个对象关联,只和ComputeEngine类关联。
1. 创建安装一个Security Manager
main方法的第一个任务是创建安装一个安全管理器,保护来自未被信任的下载到的代码对系统资源的访问。安全管理器决定了下载到的代码,是否有权限访问本地文件系统,或者执行有特殊权限的操作。
如果RMI程序没有安装安全管理器,RMI将不会为作为参数接受的对象或者作为远程方法调用返回值的对象下载其对应的类。这个限制使得由下载代码执行的操作必须符合安全策略。
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
2. 使得远程对象对Clients可用
下一步,main方法创建ComputeEngine的实例,然后使用下面的语句将实例导出为能为远程客户提供服务的远程对象。
Compute engine = new ComputeEngine();
Compute stub =
(Compute) UnicastRemoteObject.exportObject(engine);
UnicastRemoteObject 类用于导出带 JRMP 的远程对象和获得与该远程对象通信的 stub,即使远程对象具有相应的存根,并使它能够远程客户的方法调用请求。一般有两种方式进行导出:
1) 使远程类继承java.rmi.server.UnicastRemoteObject类,并且远程类的构造方法必须声明抛出RemoteException,因为UnicastRemoteObject类的构造函数会调用自身的静态方法exportObject(Remote obj, int port),而该方法会抛出RemoteException。此方式最为常用。
2) 如果一个远程类已经继承了其它类,无法再继承UnicastRemoteObject类,那么可以在其构造函数中调用静态方法UnicastRemoteObject.exportObject(Remote obj, int port),同样,远程类的构造方法也必须声明抛出RemoteException异常。或者在服务程序中直接调用静态方法UnicastRemoteObject.exportObject也行。上述程序即在采用的直接调用的方法。
静态方法UnicastRemoteObject.exportObject有三种重载形式:
l public static RemoteStub exportObject(Remote obj) throws RemoteException
l public static Remote exportObject(Remote obj, int port) throws RemoteException
l public static Remote exportObject(Remote obj, int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException
对于这些静态 exportObject 方法,正在导出的远程对象的 stub 按以下方式获得:
l 如果使用 UnicastRemoteObject.exportObject(Remote) 方法导出该远程对象,则加载 stub 类(通常使用 rmic 工具从远程对象的类预生成),并按以下方式构造 stub 类的实例:
n “根类”按以下情形确定:如果远程对象的类直接实现扩展 Remote 的接口,则远程对象的类为根类;否则,根类为直接实现扩展 Remote 接口的远程对象类的最具派生能力的超类。
n 要加载的 stub 类的名称通过连接带有后缀 "_Stub" 的根类的二进制名称确定。
n 按使用根类的类加载器的名称加载 stub 类。该 stub 类必须扩展 RemoteStub 并且必须有公共构造方法,该构造方法有一个属于类型 RemoteRef 的参数。
n 最后,用 RemoteRef 构造 stub 类的实例。
n 如果无法找到适当的 stub 类,或无法加载 stub 类,或创建 stub 实例时出现问题,则抛出 StubNotFoundException。
l 对于所有其他导出方式,如果无法加载远程对象的 stub 类(如上所述)或将系统属性 java.rmi.server.ignoreStubClasses 设置为 "true"(不分大小写),则用以下属性构造 Proxy 实例:
n 代理的实例由远程对象类的类加载器定义。
n 该代理实现由远程对象类实现的所有远程接口。
n 代理的调用处理程序是用 RemoteRef 构造的 RemoteObjectInvocationHandler 实例。
n 如果无法创建代理,则抛出 StubNotFoundException。
静态方法UnicastRemoteObject.exportObject返回的是RemoteStub类型的对象,它是远程对象对应的存根对象。注意stub的类型必须是接口Compute,而不是ComputeEngine,因为远程对象的stub仅仅实现了导出的远程对象实现的远程接口。也可以指定一个TCP端口监听远程调用请求,通常用0,指的是使用匿名的端口,RMI的默认端口为1099。一旦exportObject方法调用成功返回,ComputeEngine远程对象就准备好处理远程调用了。
exportObject方法声明抛出RemoteException,这是一个受检查的异常类型。main方法在try/catch块中处理这个异常。如何这个异常没有这样处理,那么就必须在main方法的声明中抛出这个异常子句。在必要的通讯资源不可用,例如需要的端口被占用,尝试导出远程对象这个操作就会抛出这个异常。
RMI注册
RMI系统为远程对象提供了一个特殊的机制,RMI注册。其目的是为了获得其它远程对象的引用。RMI注册机制是一个简单的远程对象命名服务,使得client能够通过远程对象的名字获得远程对象的引用( 其实获得的是远程对象对应的存根对象 )。
java.rmi.registry.Registry远程接口是用来在注册处绑定(或者注册)和查找远程对象的API。java.rmi.registry.LocateRegistry提供了在当前JVM中创建新注册的方法。一旦一个远程对象使用RMI在本地主机注册了,任何其他主机上的clients能够通过远程对象的名字查找远程对象,获得远程对象的引用,然后调用远程方法。这个注册表能够被所以运行在一个主机上的所以server共享,或者是每个server可以创建使用自己的注册表。
Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);
rebind调用产生一个对本机RMI注册表的远程调用。像任何远程调用一样,这个调用可能抛出RemoteException,这个异常将被main方法的catch语句块处理。
关于rebind调用要注意如下几点:
Ø 无参数的 LocateRegistry.getRegistry 方法返回本地主机在默认注册表端口 1099 上对远程对象 Registry 的引用。如何注册表不是在默认端口上被创建的,你需要使用带一个整形参数的重载方法。
Ø 当RMI远程对象注册表上远程调用产生的时候,远程对象的stub被传递,而不是远程对象本身的拷贝。远程对象的实现,诸如ComputeEngine的实例,永远不会离开它被创建的那个JVM。因此,当client在server的远程对象注册表中执行查找的时候,stub的拷贝被返回。远程对象在这样的情况下是通过传递引用高效运作的,而不是值传递。
Ø 由于安全原因,一个应用程序只能在运行于同一个主机上的注册表上bind,unbind或者rebind远程对象的引用。这个限制阻止了远程client删除或者重写server远程对象注册表中的实体。然而我们能够在任何主机上做出这个lookup请求,本地或者远程都可以。
Server 一向本地RMI注册表注册,它就会打印一条信息,表明它准备好开始处理远程调用了。然后main方法完成了。没有必要用一个线程让server一直处于活跃状态。只要另外一个JVM中还有对ComputeEngine对象的引用,不管是本地的还是远程的,ComputeEngine对象就不会被关闭或者被垃圾回收。因为程序在注册表里绑定了ComputeEngine对象的引用,远程的client和注册表都是可以访问这个对象引用的。RMI系统让ComputeEngine的进程处于运行状态。ComputeEngine一直是可用的不会被回收,直到它的绑定被从注册表中删除,并且没有远程client持有ComputeEngine对象的引用。
3.2.1.2 实现客户端
ComputeEngine是一个相对简单的程序:它运行交给它的任务。ComputeEngine的client程序更复杂一点。一个client需要访问ComputeEngine,还需要定义ComputeEngine要执行的任务。
例子中的client程序由两个单独的类组成。第一个类,ComputePi,查找调用一个Compute对象。第二个类,Pi,实现了Task接口,定义了compute engine的任务。Pi这个类的任务就是计算pi值到若干小数位数。
下面是client.ComputePi的源代码,即主要的client类:
像ComputeEngine服务端程序一样,客户端程序从安装安全管理器开始。这一步是必要的,因为接收server远程对象stub的过程需要从server下载类定义。为了RMI能够下载类,安全管理器必须是有效的。
安装完RMI安全管理器之后,client创建一个用来查找Compute远程对象的名字,这个名字也就是服务端用来绑定ComputeEngine远程对象的名字。客户端也是用LocateRegistry.getRegistry API来获得server主机远程对象注册表的引用的。命令行参数的第一个值是运行Compute对象的远程主机的名字。client然后调用registry的lookup方法在server的远程对象注册表中查找指定名称的远程对象。LocateRegistry.getRegistry方法的特殊重载方法这里被用到了,这个方法接受一个字符串参数,返回指定主机上默认端口1099上远程对象注册表对象的引用。如何远程对象注册表不是在默认端口1099上被创建的,你需要使用带一个整形参数的重载方法。
package client;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import
java.math.BigDecimal;
import
compute.Compute;
public
class
ComputePi {
public
static
void
main(String args[]) {
if
(System.getSecurityManager() == null
) {
System.setSecurityManager(new
SecurityManager());
}
try
{
String name = "Compute";
Registry registry = LocateRegistry.getRegistry(args[0]);
Compute comp = (Compute) registry.lookup(name);
Pi task = new
Pi(Integer.parseInt(args[1]));
BigDecimal pi = comp.executeTask(task);
System.out.println(pi);
} catch
(Exception e) {
System.err.println("ComputePi exception:");
e.printStackTrace();
}
}
}
下一步client创建一个新的Pi对象,将命令行参数的第二个值作为整形值传递给这个类的构造函数。这个参数指出了计算的精确度数。然后client调用Compute远程对象的executeTask方法返回一个BigDecimal对象,程序中用变量result存储这个值。最后程序打印出这个值。
下面是实现了Task接口的client.Pi类的源代码:
package client;
import compute.Task;
import java.io.Serializable;
import java.math.BigDecimal;
public class Pi implements Task<BigDecimal>, Serializable {
private static final long serialVersionUID = 227L;
/** constants used in pi computation */
private static final BigDecimal FOUR =
BigDecimal.valueOf(4);
/** rounding mode to use during pi computation */
private static final int roundingMode =
BigDecimal.ROUND_HALF_EVEN;
/** digits of precision after the decimal point */
private final int digits;
/**
* Construct a task to calculate pi to the specified
* precision.
*/
public Pi(int digits) {
this.digits = digits;
}
/**
* Calculate pi.
*/
public BigDecimal execute() {
return computePi(digits);
}
/**
* Compute the value of pi to the specified number of
* digits after the decimal point. The value is
* computed using Machin's formula:
*
* pi/4 = 4*arctan(1/5) - arctan(1/239)
*
* and a power series expansion of arctan(x) to
* sufficient precision.
*/
public static BigDecimal computePi(int digits) {
int scale = digits + 5;
BigDecimal arctan1_5 = arctan(5, scale);
BigDecimal arctan1_239 = arctan(239, scale);
BigDecimal pi = arctan1_5.multiply(FOUR).subtract(
arctan1_239).multiply(FOUR);
return pi.setScale(digits,
BigDecimal.ROUND_HALF_UP);
}
/**
* Compute the value, in radians, of the arctangent of
* the inverse of the supplied integer to the specified
* number of digits after the decimal point. The value
* is computed using the power series expansion for the
* arc tangent:
*
* arctan(x) = x - (x^3)/3 + (x^5)/5 - (x^7)/7 +
* (x^9)/9 ...
*/
public static BigDecimal arctan(int inverseX,
int scale)
{
BigDecimal result, numer, term;
BigDecimal invX = BigDecimal.valueOf(inverseX);
BigDecimal invX2 =
BigDecimal.valueOf(inverseX * inverseX);
numer = BigDecimal.ONE.divide(invX,
scale, roundingMode);
result = numer;
int i = 1;
do {
numer =
numer.divide(invX2, scale, roundingMode);
int denom = 2 * i + 1;
term =
numer.divide(BigDecimal.valueOf(denom),
scale, roundingMode);
if ((i % 2) != 0) {
result = result.subtract(term);
} else {
result = result.add(term);
}
i++;
} while (term.compareTo(BigDecimal.ZERO) != 0);
return result;
}
}
注意所有的可序列化的类,不管他们直接或者间接实现了Serializable接口,都必须声明一个private staitic final的叫做serialVersionUID的域来保证不同版本之间序列化的兼容性。如果这个类之前没有发布过版本,那么这个域的值可以是任何长整行的值,就像程序里Pi类使用的227L,只要将来的版本中也使用这个值就可以了。如果这个类的之前版本没有显式声明serialVersionUID,但是于那个版本的兼容性非常重要,那么新版本的这个类必须显式的使用那个先前版本默认的隐式的serialVersionUID的值。Serialver工具能够在先前的版本上运行以得出默认的隐式的serialVersionUID值。
这个例子最有意思的的特性是Compute类的实现对象不需要Pi的类定义,直到Pi对象作为一个参数传递到Compute对象的executeTask方法。那个时候,Pi的类定义才会被RMI装载到Compute对象所在的JVM,然后excute方法被调用,任务代码被执行。Pi任务的计算结果BigDecimal对象,被传回调用Compute对象的client,然后被用来作为结果打印出来。
Author: orangelizq
email: orangelizq@163.com
posted on 2007-12-01 20:55
桔子汁 阅读(3247)
评论(1) 编辑 收藏 所属分类:
J2SE