储春生 (chuchuns@cn.ibm.com)
IBM中国研究中心
2005 年 1 月
本文介绍了如何将程序代码本身打印出来的方法。
第一节 打印自身的程序
听说过这样的编程题目吗:一个程序执行后的输出自身的源代码。当然有些条件限制,这里所说的一个程序就是指的写在一个源程序文件里的程序,输出就是输出到控制台,只使用最基本的输出函数,如c的printf()函数,java的System.out.println()函数。当然也不能从文件系统中把源代码文件读出来后输出,那样就有点赖皮了。总之,就是一个程序完全靠自身的力量用基本的输出函数把自己的源代码输出。还有一个不是限制的限制,程序总是要用语言写的,就那么几种选择,我们就用java吧!
对这个问题的直观反映有时候会有2种极端,一种认为太简单,另一种很快的思考后会得出困难很大的结论。还会有一种人会指出这个问题与计算理论的递归函数的理论有关(致敬!),计算理论、图灵机那是遥远的事情,仿佛可以不管。但这个问题还真是一个有趣的问题值得我们来无聊一下。
尝试一下
不管怎么样,用java写的程序总的有 一个类,里面有个main()函数吧,恩,先看看。
清单 1. 最简单的java程序
Public class PrintOwn{
Public static void main(){
//
}
}
|
嗯,写个函数打印出来看看:
Public class PrintOwn{
Public static void main(){
P1();
}
void P1(){
System.out.println("Public class PrintOwn{");
System.out.println("Public void static main()");
System.out.println(" P1();");
System.out.println(" }");
}
}
|
麻烦来了,我们用P1( )打印了程序主体,可是现在源代码已经变了,函数P1( )的内容又由谁来打印呢? 当然可以写个P2( ),它把P1( ) 打印出来,但是麻烦并没有被摆脱,只是问题变成 P2( ) 由谁来打印的问题。要是有个"别人"来帮我把这个P2( ) 函数内容打印一下就好了,那样问题就解决了可以回家吃饭了。但是现在"别人" 还没有来,问题还得自己解决。
每天早上起床后洗漱,照镜子梳头穿衣服,如果足够悠闲并有充分的"科学头脑"的话,你是否意识到,你观察到镜子里面的影像并不是"此刻"你真实的影像,你观察镜子里面的影像、镜子把这个"动作"反馈到你的瞳孔里面、这个结果又会被镜子捕获….这个过程以光速传递。当然由于太小太快,我们的眼睛看不到这样的过程,小时候玩过将2面小镜子面对面放着,然后观察"镜子"在镜子里面的无限延伸吗?就是这样的,我们试图观察对象的行为改变了被观察对象。如果一定要足够准确的观察自己怎么办呢?这也好办,拍一张自己的照片,然后来看照片就可以了。嗯!照片?!问题有点眉目了。
按照这个思路,定义一个用来保存程序的字符串数组以及函数p1( ),保存的过程中顺便把它输出到控制台。
static String[] buff = new String[100];
static int flag = 0;
static void p1(String s) {
buff[flag++] = s;
System.out.println(s);
}
|
在main()函数中通过p1( )把源程序保存(照相)并输出,现在我们的程序是这样的,如清单2。
清单 2. 中间程序
package printOwn;
public class Mid {
static String[] buff = new String[100];
static int flag = 0;
static void p1(String s) {
buff[flag++] = s;
System.out.println(s);
}
public static void main(String[] args) {
p1("package printOwn;");
p1("public class Mid {");
p1(" static String[] buff = new String[100];");
p1(" static int flag = 0;");
p1("");
p1(" static void p1(String s) {");
p1(" buff[flag++] = s;");
p1(" System.out.println(s);");
p1(" }");
p1("");
p1(" public static void main(String[] args) {");
}
}
|
清单 2的中间程序的 输出结果
Package printOwn;
public class Mid {
static String[] buff = new String[100];
static int flag = 0;
static void p1(String s) {
buff[flag++] = s;
System.out.println(s);
}
public static void main(String[] args) {
|
嗯,看起来很不错,现在就差一堆p1( )调用没有输出了,由于我们已经把需要输出的源代码保存起来了(注意这是解决问题的关键),下面就好办了,写一个p2 ()函数,简单的把保存的东西取出来,前面拼上" p1(" ",后面拼上" ");"后输出就ok了。这里有个需要注意的小问题,如果有个字符串
实际上该字符串只有2个字节,第1第3个"/"都是转义字符。要想在控制台上输出四个字符的"///"",就得进行转换处理,好在这个并不难,下面就是convert()函数。
convert()函数
static String convert(String t) {
char[] out = new char[t.length() * 3];
int j = 0;
for (int i = 0; i < t.length(); i++) {
if (t.charAt(i) == '//') {out[j] = '//';out[j + 1] = '//';j += 2;}
else if (t.charAt(i) == '/"'){out[j]='//';out[j+1]='/"';j+=2;}
else {out[j] = t.charAt(i);j++;}
}
return new String(out, 0, j);
}
|
现在就可以写p2( )函数输出那一长串p1( )了。
p2()函数
static void p2() {
for (int i = 0; i < flag; i++) {
System.out.println(" p1(/"" + convert(buff[i]) + "/");");
}
}
|
在一长串p1("×××××") 后马上执行p2( )函数。
p1(" public static void main(String[] args) {");
p2();
}
}
|
执行一下,哈,结果不错(限于篇幅,就不在这里写出来了),就剩下最后这3句话没有输出了。
那么放在那里输出它好呢,p2( )函数无疑是最理想了。
最后的 p2()函数
static void p2() {
for (int i = 0; i < flag; i++) {
System.out.println(" p1(/"" + convert(buff[i]) + "/");");
}
System.out.println(" p2();");
System.out.println(" }");
System.out.println("}");
}
|
相应的,我们在main( ) 函数中加入相应的p1( )执行语句。
好啦,我们完成我们的工作了,源程序执行后完整的在控制台输出了自己,完全可以在源代码编辑器删除全部源代码,然后再从控制台全部拷贝过来再执行看看。我就这么干的,来检查代码是否按照预期的那样完完全全打印自己,如果哪里出了问题,编辑器会敏锐的检查出来。不用担心覆盖了最初的工作,WSAD5.0提供了非常好的从"本地历史记录"恢复源代码的机制。
第二节 启动第二个虚拟机一起来运行程序
还记得在解决上一个问题开始时候在循环的圈子中转不出来时的期望吗--要是"别人"能帮我一下就好了。在经过上一节的编程娱乐后,这一节将讨论一个有用的编程模型:程序运行中启动第二个虚拟机来运行程序的一部分,二个虚拟机之间相互通讯协调最后完成任务,同时,我们也将讨论这样一种编程模型的适用范围及优缺点。
什么时候我们需要第二虚拟机?
通过Runtime.exec( )可以启动第二个虚拟机运行java程序,那么什么时候需要这样的编程模型而不是直接采用线程来完成呢,一个显然的用例是在主程序作为一个容器来运行其他的java程序的时候,这个java程序在自己看来就在一个独立的虚拟机中运行,拥有一个独立的进程,比如测试驱动。
进程 VS 线程
有关进程和线程的讨论非常多,本文不打算重复讨论这个问题,但要注意的是,线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间,这使得线程间的通信远较进程简单。除非有特别的理由,应该将程序的任务交给线程去完成,而不是重新开启一个进程来完成。
双虚拟机模型
双虚拟机模型是这样运行的,如下图所示,在主类printOwn.OtherVMmethod中调用Runtime.exec()来启动第二个虚拟机,来执行printOwn.VMexecuter类,双方通过Socket来通信,主类调用SendMessage发送消息给VMexecuter, VMexecuter将结果反馈给主类。从而实现双方协同工作。
![](http://www-900.ibm.com/developerWorks/cn/java/j-prt/images/image001.gif)
双虚拟机模型UML类图
![](http://www-900.ibm.com/developerWorks/cn/java/j-prt/images/image003.gif)
OtherVMmethod 主执行类,创建Socket的server端,并提供向VMexecuter发送消息的接口,最后,通过发送一个特定消息指示第二个虚拟机中执行的Socket中止。这里是发送"OVER"消息。
VMexecuter 放在第二个虚拟机中执行的类,除了本身的执行工作外,还有打开,关闭Socket,读、写Socket的工作要作。
readThread 类的设计是由于主类需要监听来自VMexecuter的消息,这个动作是阻塞的,所以需要放在线程中执行监听。
StreamOut 类设计用来接收第二个虚拟机中运行的程序的控制台输出以及异常信息,给客户端一个统一透明的界面。程序很简单,实现Runnable接口从而在线程中读输入流并输出到本地控制台。
StreamOut类清单
class StreamOut implements Runnable {
String name;
InputStream is;
Thread thread;
public StreamOut (String name, InputStream is) {
this.name = name;
this.is = is;
}
public void start () {
thread = new Thread (this);
thread.start ();
}
public void run () {
try {
InputStreamReader isr = new InputStreamReader (is);
BufferedReader br = new BufferedReader (isr);
while (true) {
String s = br.readLine ();
if (s == null) break;
System.out.println ("[" + name + "] " + s);
}
is.close ();
} catch (Exception ex) {
System.out.println ("Problem reading stream " + name + "... :" + ex);
ex.printStackTrace ();
}
}
}
|
主控制类 OtherVMmethod 清单
static final String MAIN_CLASS = "printOwn.VMexecute";
static int port ;
static PrintWriter writer;
static BufferedReader reader;
// 启动一个服务端socket并初始化输出流
void connect() {
try {
ServerSocket server;
server= new ServerSocket(port);
Socket socket= server.accept();
writer = new PrintWriter(socket.getOutputStream(), true);
new readThread(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
//向第二虚拟机中执行的程序发送消息
static void sendMessage(String s){
writer.println(s);
}
//用于读第二虚拟机中的标准控制台(System.out.println())以及异常输出。
class readThread extends Thread{
private Socket socket;
public readThread(Socket socket){
this.socket = socket;
this.start();
}
public void run(){
try{
readMessage();
}catch(Exception e){
e.printStackTrace();
}
}
void readMessage() throws IOException {
reader= new BufferedReader(
new InputStreamReader(socket.getInputStream()));
try {
String line= null;
while ((line= reader.readLine()) != null) {
System.out.println(line);
}
} finally {
reader.close();
}
}
}
|
在第二个虚拟机中执行的类VMexecuter清单
class VMexecuter {
private int port;
private int flag = 0;
private Socket socket;
private PrintWriter writer;
private BufferedReader reader;
public static void main(String[] args) {
new VMexecuter().run(args);
}
//程序的执行体
private void run(String[] args) {
port = Integer.parseInt(args[0]);
openClientSocket();
try {
String line= null;
while (!(line= reader.readLine()).equalsIgnoreCase("OVER")) {
String out = (" p1(/"" + convert(line) + "/");");
writer.println(out);
}
}catch(IOException e){
e.printStackTrace();
} finally {
closeClientSocket();
}
}
//打开socket client,连接第一个虚拟机中启动的server端
private void openClientSocket() {
try {
socket = new Socket("localhost", port);
writer = new PrintWriter(socket.getOutputStream(), true);
reader= new BufferedReader(new InputStreamReader(socket.getInputStream()));
return;
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//关闭socket
private void closeClientSocket() {
try {
writer.close();
reader.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
当这些都准备好后,我们可以主类printOwn.OtherVMmethod的p2( )函数中把这些工作都串起来。
static void p2()throws Exception {
port = 6500;
Runtime rt = Runtime.getRuntime();
Process p = rt.exec("java printOwn.VMexecuter "+port );
OtherVMmethod vm = new OtherVMmethod();
vm.connect();
for(int i=0;i <flag; i++){
sendMessage(buff[i]);
}
//启动2个线程分别接收第二个虚拟机的控制台输出和错误输出。
StreamOut s1 = new StreamGobbler ("stdin", p.getInputStream ());
StreamOut s2 = new StreamGobbler ("stderr", p.getErrorStream ());
s1.start ();
s2.start ();
sendMessage("OVER");
p.waitFor();
}
|
我们还是以实现打印自身源代码的任务来串起来整个编程模型,虽然有点牛刀杀鸡而且不是一个非常合适的任务。还有一点是,为了不违背当初的限制条件,这几个类不得不放在一个文件里面,这使得程序看起来有点长。最后,由于程序较长,用ctrl+c 和 ctrl+v 的方式来写p1("" ) 有点低效。有个原则是,凡是单纯的"体力"劳动计算机来作就非常高效,而需要"脑力"劳动的计算机往往难以很应对,比如下围棋。我写了这样一个小程序,输出p1(" ×××××")到result .txt文件中去然后拷出来,在你的机器上可能要相应更改一下文件的目录。
#Tool.java 程序清单
package printOwn;
import java.io.*;
public class Tool {
public static void main(String[] args) {
try{
BufferedReader is =new BufferedReader(new InputStreamReader(
new FileInputStream("f://IBM//printOwn//printOwn//OtherVMmethod.java")));
PrintWriter pw =new PrintWriter( new FileOutputStream("f://eclipse-workspace//printOwn//printOwn//result.txt"));
String line = null;
while ((line = is.readLine())!= null){
String s = " p1(/"" + VMexecuter.convert(line) + "/");" ;
pw.println(s);
}
is.close();
pw.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
|
结束语
编程是许多IT人日常的工作,与大家一起分享编程的乐趣、快乐工作是我写出这些文字的主要原因。本文讨论了一个有趣的编程题目"打印自身程序"以及双虚拟机模型,但要注意的是,重开一个进程相比开一个线程代价大很多,使用前要充分考虑的使用的理由。
参考资料
- 本文的源代码。
- 参考 Java 2 平台上的 API 规范说明书(1.4 版标准):Java 2 API 文档.
- 2004年5月《csdn开发高手》"打印自身的程序"杂谈。
- Michael C. Daconta 在文章"When Runtime.exec() won't"中讨论了java.lang的Runtime.exec()的缺陷及应对。 可以在Javaworld网站上找到它。
- Erich Gamma, Kent Beck, "Contributing to Eclipse: Principles, Patterns, and Plug-Ins" 这本书非常棒。网上可以找到该书的电子版本。
关于作者 储春生,2004.6研究生毕业加入位于北京上地的IBM中国研究中心,在研究生期间,主要从事卫星可靠组播通讯协议(Reliable Multicast Protocol over Satellite)的研究,并在该领域申请了一项专利,发表了2篇学术论文。可以通过chuchuns@cn.ibm.com和他联系。 |