java开发C编译器:把struct编译成class
2017-08-15 17:27
567 查看
更详细的讲解和代码调试演示过程,请参看视频
用java开发C语言编译器
C语言是一种面向过程的语言,由于不像java那样具备面向对象的特性,所以在C语言中不存在类这样的对象,但C语言中的struct结构体跟java的类具有很多相通之处,struct本质上等价于一个没有方法只有数据,并且数据属性全是public的类。
本节我们要实现的目标是将包含struct定义的C源程序编译成java字节码,我们将会把struct编译成对应的java类,当完成本节代码后,我们的编译器能将下面C代码编译成java字节码并在jvm上正确运行:
我们先了解jvm用于创建和操作类对象的相关指令。当虚拟机创建一个具体类的实例之时,它需要指令new, 假设有个类,其名为ClassName,那么在虚拟机上创建一个它的实例对应的指令就是:
执行上面语句后,在虚拟机的堆栈顶部就会有一个对象实例,但代码还不能直接使用这个实例,该实例的使用必须要先初始化。我们知道,每个类必然都有自己的构造函数,例如下面这个类:
该类有两个构造函数,一个不带参数,一个带有一个String类型的参数。在初始化一个该类的实例时,这两个构造函数中,必有一个会被调用。从代码上看,每个类的构造函数都是跟类的名字是一样的,但在虚拟机内部,所有类的构造函数名一律转换为init,所以上面类的构造函数在虚拟机内部是这样的:
第一个init对应的是类定义里不带参数的构造函数,第二个init对应的是带String类型参数的构造函数。假设虚拟机通过new 指令在堆栈上构建了一个ClassName的实例对象,那么接下来它要调用不带输入参数的构造函数来初始化实例对象时,它会这么做:
上面指令中, new ClassName现在堆栈顶部创建一个类的实例,执行后堆栈情况如下:
stack:
ClassName
接着dup指令的作用是,把堆栈顶部的对象复制一份后再次压入栈顶,执行这条指令后,堆栈情况如下:
stack:
ClassName
ClassName
invokespecial 是调用指定某个类实例中成员函数的指令,如果我们想调用某个类的相关接口,那么需要把该类的实例压入堆栈顶部,然后执行指令invokespecial, 该指令后面跟着的是要调用的类的接口名称,它的格式如下:
类名/接口名
因为我们要调用ClassName实例对象的无参数构造函数,根据上面原理,虚拟机就需要使用invokespecial指令.指令执行后,压入堆栈的类实例就会从堆栈顶部移除,所以调用完构造函数后,堆栈顶部就只剩下一个类的实例.
stack:
ClassName
接下来,我们看看java一个类的定义是如何在虚拟机里定义的,假设我们有一个类定义如下:
这个类的定义很简单,它只含有两个公开成员变量,同时有一个不带输入参数的构造函数,那么上面代码转换成java汇编代码时,情况如下:
public class CTag
这句类声明会被转换成如下代码:
.class是java汇编语言的专有指令,它用来声明一个类,.super也是专有指令,用来表示一个类的父类,在java中,Object类是所以其他类的父类,所以上面代码转换成java汇编后会带有.super对应的语句,用来声明该类的父类。
接下来就是对类的成员变量进行声明,声明类成员变量的指令是.field 于是两个公开类型的成员变量在java汇编中会变成如下形式:
跟着就是要将构造函数转换成Java汇编了,我们前面讲解过,当某个函数被调用的时候,相关输入参数会存放到局部变量队列。当类的成员函数被调用时,有点特别,那就是类实例本身会被当做参数存放到局部变量队列的第0个位置,这其实就相当于this指针。
完成了对成员变量的声明后,接下来就是构造函数的实现,首先是构造函数的接口声明:
了解面向对象编程原理的话,我们就知道子类在初始化自己时,必须先调用父类的构造函数,所以当初始化构造函数init执行时,必须先执行父类构造函数,代码如下:
前面我们说过,当类的成员函数被调用时,类的实例对象会被存储在局部变量队列的第0个位置,所以指令aload 0 作用是把类的实例对象先压入栈顶,
invokespecial java/lang/Object/() V
的作用就是调用父类Object类的构造函数,完成这个步骤后,代码就要将两个成员变量赋初值为0.
要想改变一个类成员变量的值,jvm需要执行三个步骤,首先是把类的实例加载到堆栈顶部,然后把要赋值的内容压入堆栈,最后使用putfield指令把数值存入类的成员变量,所以对于与代码this.c = 0; 它转换成java汇编后,代码如下:
同理可得,this.x = 0;这条语句对应的java汇编代码为:
上面代码中putfield指令最后的C和I对应的是成员变量的数据类型,x是整形,所以它对应I, c是字符,所以它对应的类型就是C.终上所述,整个构造函数的java汇编实现如下:
最后,整个类对应的java汇编代码如下:
到这里你可能就明白,当我们要把struct CTag转换成java字节码时,我们只要把CTag转换成对应的类,然后把它编译成上面的java汇编代码也就可以了。剩下的问题是,我们如何访问一个类的成员变量。在jvm中,访问一个类的成员变量,要分两步走,首先把类的实例压入堆栈,然后使用getfield指令将对应的类成员变量的值读入堆栈顶部。如果我们想要读取CTag.x的值,那么对应的java汇编代码如下:
执行上面语句后,CTag.x的值就会存储在堆栈顶部。有了这些理论知识后,我们就可以着手实现代码的编译了。
当我们编译器在解析代码,遇到语句myTag.x 时,我们先看看myTag对应的结构体是否被编译成对应的java类,如果已经被编译过了,那么我们直接通过指令读取myTag.x的值,如果还没有被编译过,那么我们就生成对应的java类定义,由此,在ProgramGenerator.java中,添加如下代码:
上面代码的作用是把struct定义转换成java的class,并转换成前面讲解过的java类定义的汇编代码,实现的每个步骤都有相应的注释,更详细的讲解和调试请参看视频:用java开发C语言编译器
我们再看看如何实现对结构体成员变量值的修改:
实现读取结构体成员变量的代码如下:
有了实现结构体定义,结构体成员变量的修改和读取等功能的实现后,我们只要在编译器解析到相应的地方,要执行对应操作时,调用上面代码就可以了。当编译器读取到语句 myTag.x 时,它知道此时程序的目的是想读取结构体成员变量的值,负责解析这条语句的代码是在UnaryNodeExecutor.java中:
当代码要对结构体的成员变量赋值时,也就是要执行语句myTag.x = 1;时,编译器的代码会进入Symbol.setValue中,所以在该函数里,我们需要做相应修改如下:
上面代码完成后,将程序运行起来,前面给定的C语言代码会被编译成如下java汇编代码:
把上面的java汇编代码编译成字节码之后运行,结果如下:
运行结果跟C语言代码的目标是一致的,也就是说,我们把带有struct结构体的C语言代码编译成java字节码是成功的。
更详细的讲解和代码调试,请参看视频:
用java开发C语言编译器
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
用java开发C语言编译器
C语言是一种面向过程的语言,由于不像java那样具备面向对象的特性,所以在C语言中不存在类这样的对象,但C语言中的struct结构体跟java的类具有很多相通之处,struct本质上等价于一个没有方法只有数据,并且数据属性全是public的类。
本节我们要实现的目标是将包含struct定义的C源程序编译成java字节码,我们将会把struct编译成对应的java类,当完成本节代码后,我们的编译器能将下面C代码编译成java字节码并在jvm上正确运行:
struct CTag { int x; char c; }; void main() { struct CTag myTag; myTag.x = 1; printf("value of x in myTag is %d", myTag.x); }
我们先了解jvm用于创建和操作类对象的相关指令。当虚拟机创建一个具体类的实例之时,它需要指令new, 假设有个类,其名为ClassName,那么在虚拟机上创建一个它的实例对应的指令就是:
new ClassName
执行上面语句后,在虚拟机的堆栈顶部就会有一个对象实例,但代码还不能直接使用这个实例,该实例的使用必须要先初始化。我们知道,每个类必然都有自己的构造函数,例如下面这个类:
public ClassName { public ClassName(){} public ClassName(String name){} }
该类有两个构造函数,一个不带参数,一个带有一个String类型的参数。在初始化一个该类的实例时,这两个构造函数中,必有一个会被调用。从代码上看,每个类的构造函数都是跟类的名字是一样的,但在虚拟机内部,所有类的构造函数名一律转换为init,所以上面类的构造函数在虚拟机内部是这样的:
<init>() V <init>(Ljava/lang/Strin;)V
第一个init对应的是类定义里不带参数的构造函数,第二个init对应的是带String类型参数的构造函数。假设虚拟机通过new 指令在堆栈上构建了一个ClassName的实例对象,那么接下来它要调用不带输入参数的构造函数来初始化实例对象时,它会这么做:
new ClassName dup invokespecial ClassName/<init>() V
上面指令中, new ClassName现在堆栈顶部创建一个类的实例,执行后堆栈情况如下:
stack:
ClassName
接着dup指令的作用是,把堆栈顶部的对象复制一份后再次压入栈顶,执行这条指令后,堆栈情况如下:
stack:
ClassName
ClassName
invokespecial 是调用指定某个类实例中成员函数的指令,如果我们想调用某个类的相关接口,那么需要把该类的实例压入堆栈顶部,然后执行指令invokespecial, 该指令后面跟着的是要调用的类的接口名称,它的格式如下:
类名/接口名
因为我们要调用ClassName实例对象的无参数构造函数,根据上面原理,虚拟机就需要使用invokespecial指令.指令执行后,压入堆栈的类实例就会从堆栈顶部移除,所以调用完构造函数后,堆栈顶部就只剩下一个类的实例.
stack:
ClassName
接下来,我们看看java一个类的定义是如何在虚拟机里定义的,假设我们有一个类定义如下:
public class CTag { public int x; public char c; public CTag() { this.x = 0; this.c = 0; } }
这个类的定义很简单,它只含有两个公开成员变量,同时有一个不带输入参数的构造函数,那么上面代码转换成java汇编代码时,情况如下:
public class CTag
这句类声明会被转换成如下代码:
.class public CTag .super java/lang/Object
.class是java汇编语言的专有指令,它用来声明一个类,.super也是专有指令,用来表示一个类的父类,在java中,Object类是所以其他类的父类,所以上面代码转换成java汇编后会带有.super对应的语句,用来声明该类的父类。
接下来就是对类的成员变量进行声明,声明类成员变量的指令是.field 于是两个公开类型的成员变量在java汇编中会变成如下形式:
.field public c C .field public x I
跟着就是要将构造函数转换成Java汇编了,我们前面讲解过,当某个函数被调用的时候,相关输入参数会存放到局部变量队列。当类的成员函数被调用时,有点特别,那就是类实例本身会被当做参数存放到局部变量队列的第0个位置,这其实就相当于this指针。
完成了对成员变量的声明后,接下来就是构造函数的实现,首先是构造函数的接口声明:
.method public <init>()V
了解面向对象编程原理的话,我们就知道子类在初始化自己时,必须先调用父类的构造函数,所以当初始化构造函数init执行时,必须先执行父类构造函数,代码如下:
aload 0 invokespecial java/lang/Object/<init>()V
前面我们说过,当类的成员函数被调用时,类的实例对象会被存储在局部变量队列的第0个位置,所以指令aload 0 作用是把类的实例对象先压入栈顶,
invokespecial java/lang/Object/() V
的作用就是调用父类Object类的构造函数,完成这个步骤后,代码就要将两个成员变量赋初值为0.
要想改变一个类成员变量的值,jvm需要执行三个步骤,首先是把类的实例加载到堆栈顶部,然后把要赋值的内容压入堆栈,最后使用putfield指令把数值存入类的成员变量,所以对于与代码this.c = 0; 它转换成java汇编后,代码如下:
aload 0 sipush 0 putfield CTag/c C
同理可得,this.x = 0;这条语句对应的java汇编代码为:
aload 0 sipush 0 putfield CTag/x I
上面代码中putfield指令最后的C和I对应的是成员变量的数据类型,x是整形,所以它对应I, c是字符,所以它对应的类型就是C.终上所述,整个构造函数的java汇编实现如下:
.method public <init>()V
aload 0 invokespecial java/lang/Object/<init>()V
aload 0
sipush 0
putfield CTag/c C
aload 0
sipush 0
putfield CTag/x I
return
.end method
最后,整个类对应的java汇编代码如下:
.class public CTag .super java/lang/Object
.field public c C .field public x I
.method public <init>()V
aload 0 invokespecial java/lang/Object/<init>()V
aload 0
sipush 0
putfield CTag/c C
aload 0
sipush 0
putfield CTag/x I
return
.end method
.end class
到这里你可能就明白,当我们要把struct CTag转换成java字节码时,我们只要把CTag转换成对应的类,然后把它编译成上面的java汇编代码也就可以了。剩下的问题是,我们如何访问一个类的成员变量。在jvm中,访问一个类的成员变量,要分两步走,首先把类的实例压入堆栈,然后使用getfield指令将对应的类成员变量的值读入堆栈顶部。如果我们想要读取CTag.x的值,那么对应的java汇编代码如下:
aload 0 ;假设CTag实例位于具备变量队列第0个位置 putfield CTag/x I
执行上面语句后,CTag.x的值就会存储在堆栈顶部。有了这些理论知识后,我们就可以着手实现代码的编译了。
当我们编译器在解析代码,遇到语句myTag.x 时,我们先看看myTag对应的结构体是否被编译成对应的java类,如果已经被编译过了,那么我们直接通过指令读取myTag.x的值,如果还没有被编译过,那么我们就生成对应的java类定义,由此,在ProgramGenerator.java中,添加如下代码:
public class ProgramGenerator extends CodeGenerator { .... private ArrayList<String> structNameList = new ArrayList<String>(); public void putStructToClassDeclaration(Symbol symbol) { private ArrayList<String> structNameList = new ArrayList<String>(); public void putStructToClassDeclaration(Symbol symbol) { //判断传入的Symbol变量是否是结构体变量,不是的话立刻返回 Specifier sp = symbol.getSpecifierByType(Specifier.STRUCTURE); if (sp == null) { return; } /* * 在队列structNameList中查询Symbol对应的结构体名字是否已经存储在队列中,如果在队列中有了 * 那表明该结构体已经被转换成java类,并且类的定义已经转换成java汇编语言了 */ StructDefine struct = sp.getStructObj(); if (structNameList.contains(struct.getTag())) { return; } else { structNameList.add(struct.getTag()); } /* * 输出相应指令,把结构体转换成java类 */ this.emit(Instruction.NEW, struct.getTag()); this.emit(Instruction.DUP); this.emit(Instruction.INVOKESPECIAL, struct.getTag()+"/"+"<init>()V"); int idx = this.getLocalVariableIndex(symbol); this.emit(Instruction.ASTORE, ""+idx); //这条语句的作用是,把接下来生成的指令先缓存起来,而不是直接写入到文件里 this.setClassDefinition(true); this.emitDirective(Directive.CLASS_PUBLIC, struct.getTag()); this.emitDirective(Directive.SUPER, "java/lang/Object"); /* * 把结构体中的每个成员转换成相应的具有public性质的java类成员 */ Symbol fields = struct.getFields(); do { String fieldName = fields.getName() + " "; if (fields.getDeclarator(Declarator.ARRAY) != null) { fieldName += "["; } if (fields.hasType(Specifier.INT)) { fieldName += "I"; } else if (fields.hasType(Specifier.CHAR)) { fieldName += "C"; } else if (fields.hasType(Specifier.CHAR) && fields.getDeclarator(Declarator.POINTER) != null) { fieldName += "Ljava/lang/String;"; } this.emitDirective(Directive.FIELD_PUBLIC, fieldName); fields = fields.getNextSymbol(); }while (fields != null); /* * 实现类的初始构造函数,它调用父类的构造函数后,接下来通过putfield指令,把类的每个成员都初始化为0 */ this.emitDirective(Directive.METHOD_PUBLIC, "<init>()V"); this.emit(Instruction.ALOAD, "0"); String superInit = "java/lang/Object/<init>()V"; this.emit(Instruction.INVOKESPECIAL, superInit); fields = struct.getFields(); do { this.emit(Instruction.ALOAD, "0"); String fieldName = struct.getTag() + "/" + fields.getName(); String fieldType = ""; if (fields.hasType(Specifier.INT)) { fieldType = "I"; this.emit(Instruction.SIPUSH, "0"); } else if (fields.hasType(Specifier.CHAR)) { fieldType = "C"; this.emit(Instruction.SIPUSH, "0"); } else if (fields.hasType(Specifier.CHAR) && fields.getDeclarator(Declarator.POINTER) != null) { fieldType = "Ljava/lang/String;"; this.emit(Instruction.LDC, " "); } String classField = fieldName + " " + fieldType; this.emit(Instruction.PUTFIELD, classField); fields = fields.getNextSymbol(); }while (fields != null); this.emit(Instruction.RETURN); this.emitDirective(Directive.END_METHOD); this.emitDirective(Directive.END_CLASS); this.setClassDefinition(false); } .... }
上面代码的作用是把struct定义转换成java的class,并转换成前面讲解过的java类定义的汇编代码,实现的每个步骤都有相应的注释,更详细的讲解和调试请参看视频:用java开发C语言编译器
我们再看看如何实现对结构体成员变量值的修改:
public void assignValueToStructMember(Symbol structSym, Symbol field, Object val) { //先把类的实例压入堆栈顶部 int idx = getLocalVariableIndex(structSym); this.emit(Instruction.ALOAD, ""+idx); /* * field是要写入的结构体成员对象,假设我们要对myTag.x 赋值,那么下面的代码把myTag.x转换为 * CTag/x I */ String value = ""; String fieldType = ""; if (field.hasType(Specifier.INT)) { fieldType = "I"; value += (Integer)val; this.emit(Instruction.SIPUSH, value); } else if (field.hasType(Specifier.CHAR)) { fieldType = "C"; value += (Integer)val; this.emit(Instruction.SIPUSH, value); } else if (field.hasType(Specifier.CHAR) && field.getDeclarator(Declarator.POINTER) != null) { fieldType = "Ljava/lang/String;"; value += (String)val; this.emit(Instruction.LDC, value); } //执行putfield指令,把要修改的值写入结构体成员变量 Specifier sp = structSym.getSpecifierByType(Specifier.STRUCTURE); StructDefine struct = sp.getStructObj(); String fieldContent = struct.getTag() + "/" + field.getName() + " " + fieldType; this.emit(Instruction.PUTFIELD, fieldContent); }
实现读取结构体成员变量的代码如下:
public void readValueFromStructMember(Symbol structSym, Symbol field) { /* * 先把类的实例加载到堆栈顶部 */ int idx = getLocalVariableIndex(structSym); this.emit(Instruction.ALOAD, ""+idx); /* * 如果我们要读取myTag.x 下面的语句会构造出 * CTag/x I */ String fieldType = ""; if (field.hasType(Specifier.INT)) { fieldType = "I"; } else if (field.hasType(Specifier.CHAR)) { fieldType = "C"; } else if (field.hasType(Specifier.CHAR) && field.getDeclarator(Declarator.POINTER) != null) { fieldType = "Ljava/lang/String;"; } //通过getfield指令把结构体的成员变量读出来后压入堆栈顶部 Specifier sp = structSym.getSpecifierByType(Specifier.STRUCTURE); StructDefine struct = sp.getStructObj(); String fieldContent = struct.getTag() + "/" + field.getName() + " " + fieldType; this.emit(Instruction.GETFIELD, fieldContent); }
有了实现结构体定义,结构体成员变量的修改和读取等功能的实现后,我们只要在编译器解析到相应的地方,要执行对应操作时,调用上面代码就可以了。当编译器读取到语句 myTag.x 时,它知道此时程序的目的是想读取结构体成员变量的值,负责解析这条语句的代码是在UnaryNodeExecutor.java中:
public class UnaryNodeExecutor extends BaseExecutor implements IExecutorReceiver{ .... public Object Execute(ICodeNode root) { .... case CGrammarInitializer.Unary_StructOP_Name_TO_Unary: /* * 当编译器读取到myTag.x 这种类型的语句时,会走入到这里 */ child = root.getChildren().get(0); String fieldName = (String)root.getAttribute(ICodeKey.TEXT); symbol = (Symbol)child.getAttribute(ICodeKey.SYMBOL); //先把结构体变量的作用范围设置为定义它的函数名 symbol.addScope(ProgramGenerator.getInstance().getCurrentFuncName()); //如果是第一次访问结构体成员变量,那么将结构体声明成一个类 ProgramGenerator.getInstance().putStructToClassDeclaration(symbol); if (isSymbolStructPointer(symbol)) { copyBetweenStructAndMem(symbol, false); } /* * 假设当前解析的语句是myTag.x, 那么args对应的就是变量x * 通过调用setStructParent 把args对应的变量x 跟包含它的结构体变量myTag * 关联起来 */ Symbol args = symbol.getArgList(); while (args != null) { if (args.getName().equals(fieldName)) { args.setStructParent(symbol); break; } args = args.getNextSymbol(); } if (args == null) { System.err.println("access a filed not in struct object!"); System.exit(1); } /* * 把读取结构体成员变量转换成对应的java汇编代码,也就是使用getfield指令把对应的成员变量的值读取出来,然后压入堆栈顶部 */ if (args.getValue() != null) { ProgramGenerator.getInstance().readValueFromStructMember(symbol, args); } .... .... } .... }
当代码要对结构体的成员变量赋值时,也就是要执行语句myTag.x = 1;时,编译器的代码会进入Symbol.setValue中,所以在该函数里,我们需要做相应修改如下:
public class Symbol implements IValueSetter{ .... public void setValue(Object obj) { if (obj != null) { System.out.println("Assign Value of " + obj.toString() + " to Variable " + name); } this.value = obj; if (this.value != null) { /* * 先判断该变量是否是一个结构体的成员变量,如果是,那么需要通过assignValueToStructMember来实现成员变量 * 的赋值,如果不是,那么就直接通过IStore语句直接赋值 */ ProgramGenerator generator = ProgramGenerator.getInstance(); if (this.isStructMember() == false) { int idx = generator.getLocalVariableIndex(this); if (generator.isPassingArguments() == false) { generator.emit(Instruction.ISTORE, "" + idx); } } else { generator.assignValueToStructMember(this.getStructSymbol(), this, this.value); } } } .... }
上面代码完成后,将程序运行起来,前面给定的C语言代码会被编译成如下java汇编代码:
.class public CSourceToJava
.super java/lang/Object
.method public static main([Ljava/lang/String;)V
new CTag
dup
invokespecial CTag/<init>()V
astore 0
sipush 1
aload 0
sipush 1
putfield CTag/x I
aload 0
getfield CTag/x I
istore 1
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "value of x in myTag is "
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 1
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "
"
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
return
.end method
.end class
.class public CTag .super java/lang/Object
.field public c C .field public x I
.method public <init>()V
aload 0 invokespecial java/lang/Object/<init>()V
aload 0
sipush 0
putfield CTag/c C
aload 0
sipush 0
putfield CTag/x I
return
.end method
.end class
把上面的java汇编代码编译成字节码之后运行,结果如下:
运行结果跟C语言代码的目标是一致的,也就是说,我们把带有struct结构体的C语言代码编译成java字节码是成功的。
更详细的讲解和代码调试,请参看视频:
用java开发C语言编译器
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
相关文章推荐
- java开发编译器:把结构体数组编译成java字节码
- java开发编译器:把C语言的循环指令编译成java字节码
- [编译原理]用BDD方式开发lisp解释器(编译器)|开发语言java|Groovy|Spock
- java开发C编译器:把函数调用编译成字节码
- java开发C编译器:把C程序编译成java字节码
- 使用Myeclipse内置Ant编译项目时提醒警告java\lang\Object.class(java\lang:Object.class): 主版本 51 比 50 新,此编译器支持最新的主版本
- 反编译工具jad的使用(将*.class文件变成*.java文件,附带jad.zip包)
- eclipse不能自动将java文件编译成class文件,刷新(Refrelsh)和重新建立工程都不行。
- java class反编译后的代码还原(一)
- Java应用程序本地编译为EXE的几种方法-Java基础-Java-编程开发
- java class反编译后的代码还原(二)
- java,class反编译
- MyEclipse 开发 SSH 整合时 java.lang.NoSuchMethodError: org.objectweb.asm.ClassVisitor.visit 解决方案
- Java中class文件编译成exe文件的几种方法【推荐】
- 如何将.class文件转换成.java文件——JAVA反编译工具总结
- .java .class文件编译
- java编译错误:需要class或interface,GBK的不可映射字符
- MyEclipse 开发 SSH 整合时 java.lang.NoSuchMethodError: org.objectweb.asm.ClassVisitor.visit 解决方案
- Java中class文件编译成exe文件的几种方法
- 如何将java中class文件反编译成java文件[Java]