什么是编译?
因为机器是只能做数字计算的,能够让机器去运算数字语言就是机器语言。为了让机器可理解,相对于机器语言的高级语言都需要一个转换,从高级、机器不可理解,转换为机器可理解的机器语言,这样的一个转换过程就叫做 编译(Compile)。简单来说就是把人能看懂的代码,变成机器能读懂的指令的过程。负责这一过程处理的的工具叫做编译器。
不同的语言都有自己的编译器,Java语言中负责编译的编译器是一个命令:javac
在我们初学Java时,一般都会让我们写一个demo叫做“HelloWord”,我们可以使用javac这个命令将HelloWorld.java这个文件,生成HelloWorld.class文件,这个.class文件就是JVM可以识别的文件,通常我们认为这个过程就是Java语言的编译。但是这个.class文件依旧不是机器能够是别的语言,还需要JVM将这个.class文件转换成机器能够识别的语言。
什么是反编译?
反编译就是和编译正好相反的过程,就是将已经编译好的变成语言还原成未编译的状态。Java语言的反编译一般是指将.class文件装换成.java文件。
Java反编译工具
想要进行反编译就不得不提及反编译工具,Java中的反编译工具我用的最多的就是jd-gui,图形界面操作简单方便,只要要将文件拖进去就可以看到源码,但是断更很久,有些代码已经不支持,这里我们不多提及。在jd-gui不能满足我们的时候,我们可以尝试新的工具,例如CFR(官网地址:http://www.benf.org/other/cfr/)我们在官网下载好jar包,然后就可以使用了。
1.下面这个是我们已经写好的一个类,JDK1.7后添加了对String类型的判断switch反编译后的结果:
/**
* @author Y
*/
public class TestSwitchCompile {
public static void main(String[] args) {
String str = "world";
switch (str){
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default:
break;
}
}
}
执行以下命令:
java -jar cfr_0_132.jar TestSwitchCompile.class --decodestringswitch false
得到以下代码:
/*
* Decompiled with CFR 0_132.
*/
import java.io.PrintStream;
public class TestSwitchCompile {
public static void main(String[] arrstring) {
String string;
String string2 = string = "world";
int n = -1;
switch (string2.hashCode()) {
case 99162322: {
if (!string2.equals("hello")) break;
n = 0;
break;
}
case 113318802: {
if (!string2.equals("world")) break;
n = 1;
}
}
switch (n) {
case 0: {
System.out.println("hello");
break;
}
case 1: {
System.out.println("world");
break;
}
}
}
}
通过这段代码我们可以得到字符串的switch是通过equals()和hashCode()方法来实现的结论。
2.下面我们来看一下String对"+"的重载:
/**
* @author Y
*/
public class TestStringCompile {
public static void main(String[] args) {
String hello = "hello";
String world = hello + "world";
}
}
执行以下命令:
java -jar cfr_0_132.jar TestStringCompile.class --stringbuilder false
得到以下代码:
/**
* Decompiled with CFR 0_132.
*/
public class TestStringCompile {
public static void main(String[] arrstring) {
String string = "hello";
String string2 = new StringBuilder().append(string).append("world").toString();
}
}
看了反编译之后的代码我们发现,其实String对“+”的支持其实就是使用了StringBuilder以及他的方法append、toString两个方法。
3.下面我们看一下lambda反编译后的结果:
import java.util.ArrayList;
import java.util.List;
/**
* @author Y
*/
public class TestLamdbaCompile {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.forEach(s -> System.out.println(s));
}
}
执行以下命令:
java -jar cfr_0_132.jar TestLamdbaCompile.class --decodelambdas false
得到以下代码:
/**
* Decompiled with CFR 0_132.
*/
import java.io.PrintStream;
import java.lang.invoke.LambdaMetafactory;
import java.util.ArrayList;
import java.util.function.Consumer;
public class TestLamdbaCompile {
public static void main(String[] arrstring) {
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
arrayList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());
}
private static /* synthetic */ void lambda$main$0(String string) {
System.out.println(string);
}
}
可以看到,在forEach方法中,其实是调用了java.lang.invoke.LambdaMetafactory#metafactory方法,该方法的第四个参数implMethod指定了方法实现。可以看到这里其实是调用了lambda$main$0方法进行了输出。
4.下面我们看一下java10本地变量推断:
import java.util.ArrayList;
/**
* @author Y
*/
public class TestVarCompile {
public static void main(String[] args) {
//局部变量
var String = "hello";
//局部变量
var listString = new ArrayList<String>();
listString.add(String);
listString.add("world");
//增强型for循环索引
for(var list : listString){
System.out.println(list);
}
//传统for循环的局部变量
for(var i=0; i<listString.size(); i++){
System.out.println(listString.get(i));
}
}
}
执行以下命令:
java -jar cfr_0_132.jar TestVarCompile.class --collectioniter false
得到以下代码:
/**
* Decompiled with CFR 0_132.
*/
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
public class TestVarCompile {
public static void main(String[] arrstring) {
String string = "hello";
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add(string);
arrayList.add("world");
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
String string2 = (String)iterator.next();
System.out.println(string2);
}
for (int i = 0; i < arrayList.size(); ++i) {
System.out.println((String)arrayList.get(i));
}
}
}
反编译后这段代码我们看起来就比较熟悉了,虽然我们定义的是var但是在.java文件编译成.class文件的时候,会进行解语法糖,用变量真正的类型来代替var。
上面的代码我们看到ArrayList泛型了String类型,我们知道泛型其实是语法糖,在JDK1.5中,Java语言引入了泛型机制。但是这种泛型机制是通过类型擦除来实现的,类型擦除的主要过程如下: 1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。 2.移除所有的类型参数。意思就是Java泛型只存在于我们的源码里面。但是我用cfr的反编译的时候,我没有找到哪个参数是可以去除掉这个泛型的,这个也是我的一个遗留问题。然后我使用jad去反编译看了一下,结果如下:
执行以下命令:
jad TestVarCompile.class
执行完命令后会打印出下面这一行
Parsing TestVarCompile.class... Generating TestVarCompile.jad
得到以下代码:
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
public class TestVarCompile {
public static void main(String args[])
{
String s = "hello";
ArrayList arraylist = new ArrayList();
arraylist.add(s);
arraylist.add("world");
String s1;
for(Iterator iterator = arraylist.iterator(); iterator.hasNext(); System.out.println(s1))
s1 = (String)iterator.next();
for(int i = 0; i < arraylist.size(); i++)
System.out.println((String)arraylist.get(i));
}
}
这时我们再看arrayList,其实已经没有泛型了。所以在虚拟机中,没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。