-
2007-06-30
Javac背后的故事——创建对象
要让Java这个面向“对象”的世界正常运作,创建对象就是一项不可或缺的操作。
public class NewMain {
public static void main(String[] args) {
new Object();
}
}
用javap反编译上面的代码,我们可以得到下面的指令,这里省去了javac暗中创建的构造函数。
public class NewMain extends java.lang.Object{
...
public static void main(java.lang.String[]);
Code:
0: new #3; //class java/lang/Object
3: invokespecial #8; //Method java/lang/Object."<init>":()V
6: return
}
从这段代码中,我们可以清晰的看出创建对象(new)和调用构造函数(invokespecial)两个过程。关于这个问题,我在《对象的生命》中曾经进行过讨论。
既然javac将一个new的动作被解释为两条指令,那在JVM的层面上,我们当然就可以将它们分开。下面是一段没什么实际用途的代码,只是证明这个观点可行性。
public class NewGenerator {
public static void main(String[] args) throws Exception {
String className = "New";
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_2, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
Method m = Method.getMethod("void main (String[])");
GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, m, null, null, cw);
mg.newInstance(Type.getType(Object.class));
Label label = mg.newLabel();
mg.ifNonNull(label);
mg.mark(label);
mg.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
mg.push("new object is not null");
mg.invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println(java.lang.String)"));
mg.pop();
mg.returnValue();
mg.endMethod();
cw.visitEnd();
OutputStream os = null;
try {
os = new FileOutputStream(className + ".class");
os.write(cw.toByteArray());
} finally {
if (os != null) {
os.close();
}
}
}
}
这段代码生成的类是能够运行的,有兴趣的可以自己试一下。这段代码的作用是new出一个对象之后,如果这个对象非空的话,就会产生一个输出:
new object is not null
当然,如果尝试用这个对象做一些其它的操作,会有错误等待着我们,因为这个对象并不是一个完整的对象。在JVM规范中有相关的解释:new指令并不能完整创建出一个新的对象,直到对未初始化的对象调用了实例初始化方法才会完成实例的创建。这段代码也正好符合《对象的生命》中的解释,它只是负责做出申请内存。当然,在JVM中,它的实际工作要略多一些,如果这个对象的类没有加载,就会加载相应的类。
-
2007-06-18
Javac背后的故事——空类
程序员最熟悉的是源代码,但是要让程序真正的发挥功效,少不了编译器的帮助。javac的作用就是将Java代码编译为JVM指令。由于Java语言和JVM同出一门,所以,稍微熟悉一下,我们便不难发现,二者几乎是直接对应的。当然,为了简化代码的编写,javac除了直接翻译之外,还暗地里帮我们做了不少工作,我们从最简单的情况看起。
public class Test {
}
我们用javac编译这段代码,javap可以帮助我们反编译生成的类文件。
javap -c Test
下面就是反编译的结果。
public class Test extends java.lang.Object{
public Test();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
}
抛开指令具体的内容,上面反编译的结果清清楚楚的告诉我们,我们编写的这个空类一点都不空,因为其中还有一个构造函数。这就是javac替我们做的工作。没错,这是javac做的,但未必是JVM要求的。其实,JVM上运行的类,完全可以没有构造函数。不过,前面的例子已经明明白白的告诉我们,因为javac的作用,直接用Java语言是无法构造出真正的空类。那我们就不妨直接从字节码入手,借助ObjectWeb ASM构造真正的“空“类。
public class NoCtorGenerator {
public static void main(String[] args) throws Exception {
String className = "NoCtor";
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_2, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
cw.visitEnd();
try {
os = new FileOutputStream(className + ".class");
os.write(cw.toByteArray());
} finally {
if (os != null) {
os.close();
}
}
}
}
借助javap,我们可以看到生成的结果,确实没有构造函数。
public class NoCtor extends java.lang.Object{
}
不过,因为没有构造函数存在,我们并不能用这个类创建对象,但是,下面的代码证明了这个类生成的类确实可用。
public class NoCtorMain {
public static void main(String[] args) {
System.out.println(NoCtor.class);
}
}
运行这段代码,我们可以得到下面的输出:
class NoCtor
关于Java虚拟机的指令,可以参考《深入Java虚拟机》,而ObjectWeb ASM的入门,可以参考我的《Hello, ASM——代码生成》。 -
2006-12-12
Hello, Java Compiler API
Java 6发布了,想必很多人还不没有来得及玩熟Java 5,很多人还在与1.4打交道,稳定归稳定,发展是不能停止的。Java 6并不像Java 5那样在语法上下了很大的功夫,更多的力气用在丰富API上。其中有一项是我比较感兴趣的,就是Compiler API,也就是JDK为我们提供的访问Java编译器的接口。下面便是一个使用Compiler API编译Java程序的例子。
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;public class Main {
public static void main(String[] args) throws Exception {
String sourceFile = "Hello.java";
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager =
compiler.getStandardFileManager(null, null, null);
Iterable compilationUnits = fileManager.getJavaFileObjects(sourceFile);
JavaCompiler.CompilationTask task =
compiler.getTask(null, fileManager, null, null, null, compilationUnits);
task.call();
fileManager.close();
}
}作为一个最简单的例子,代码本身没什么值得太多解释的地方。它实际上完成的就是调用系统的Java编译器去实现完成编译的工作,因此,基本上等价于
javac Hello.java之前,刚刚在blog中提到ASM,里面的代码生成工作是通过直接写bytecode完成的。现在有了Compiler API,可以考虑生成代码以Java源码的形式完成,然后,通过调用Compiler API对源码进行动态编译,这样,可以达到同直接写bytecode类似的作用。使用Compiler API,肯定不如直接生成bytecode来得高效,但对于不了解JVM指令的人来说也许是一种解决方案。
有了Compiler API,就可以在运行时直接编译Java程序,相当于把javac和java两个过程合二为一,这让Java代码拥有了类似于动态语言的特征。事实上,为了了解Compiler API,我确实做了一个这样的例子,不过,这样就要动不少手脚,不像这里展示的代码那样清晰简单。
-
2006-11-14
开源Java
OpenJDK
https://openjdk.dev.java.net/Mobile and Embedded Community
http://community.java.net/mobileandembedded/Open Source Java
http://www.sun.com/software/opensource/java/Sun Opens Java
http://www.sun.com/2006-1113/feature/index.jspJava Story
http://www.sun.com/2006-1113/feature/story.jspJames Gosling's Letter to the Java Community
http://www.sun.com/software/opensource/java/gosling_letter.jspCSDN报道
http://blog.csdn.net/programmer_editor/archive/2006/11/14/1383027.aspx喊了好长时间的口号,Java终于开源了。这次主要是SE和ME,加上之前的EE,算是凑足了一条完整的线。
和很多朋友一样,我有一个疑问。JDK的源码很早就可以得到了,开源的差别在哪呢?想来想去,根源可能在于我们对于开源的理解。我们一直认为有源码就叫开源,其实,开源的意义并不只是在代码开放,还有一个参与反馈的过程。原来的Java,我们可以从中学习,但并不能把自己的想法回馈到Java中,所以,算不得真正的开源。如果想参与,现在有机会了,因为Java开源了。
不过,有一点需要清楚,实际上这里的开源只是开放SUN的Java实现,而并非Java语言,平台API和规范。这些东西依然控制伟大的JCP手中,这么多年了,JCP的办事效率大家都很清楚。所以,这种开源方式对Java而言,不会伤筋动骨,但是,想大踏步的前进也是不可能的。
SUN对Java的开源采用了GPL 2,也就是说,可以发布自己的Java版本,但所做的改动要反馈回这个项目之中,SUN希望借此保持Java的纯正。谈及这次开源的意义,SUN把GNU/Linux也列了出来,至少以后发布的Linux发行版中加入Java可以名正言顺了,Richard Stallman可是最在乎名义的。
SUN有做好事的传统,Solaris、NetBeans、OpenOffice等等全都送到了开源阵营中,这次是Java。SUN是商人和技术人矛盾的组合,它做出的东西都很不错,只是无法给自己带来足够的价值。SUN不傻,能赚钱的东西,它是不会开源的,送到开源社区也是无奈之举。
顺便说一下,Java那个可爱吉祥物Duke也开源了。
https://duke.dev.java.net/







