java编译优化

java编译

java编译器为我们做了很多优化,比如在java中泛型并不是真正的泛型,在编译的时候会进行泛型擦除,使用的时候再进行类型转换.或者Integer自动装箱和拆箱.foreach循环遍历等等.

泛型擦除

在java中泛型并不是真正的泛型,因为有一个java早期没有泛型的时候都是通过Object来代替泛型的,因为java中每个对象都是继承自Object的.在通过类型转换来实现泛型.现在有了泛型.通过泛型来指定类型.但是这个泛型也不是真正的泛型.在编译期间都会进行泛型擦除.使其变为普通的类型.下面来看个例子
源代码

1
2
3
4
5
ArrayList<Integer> ls = new ArrayList<>();
ls.add(1);
ls.add(2);
int a = ls.get(0);
System.out.println(a);

编译之后的字节码反编译过来的文件.

1
2
3
4
5
ArrayList var1 = new ArrayList();
var1.add(1);
var1.add(2);
int var2 = (Integer)var1.get(0);
System.out.println(var2);

这里可以看到这个时候哦ArrayList已经没有泛型了,只是他原来的类型.在获取字段的时候会有一个类型转换的操作.所以有一点就是在进行方法重载的时候ArrayList 和 ArrayList 类型是一样的,编译会出现错误.
也正是有了泛型擦除,就有了一个问题,你可以向一个定义了类型的容器中添加其他类型的变量.看下面代码:

1
2
3
4
List<String> ls = new ArrayList<>();
Class<? extends ArrayList> cls = (Class<? extends ArrayList>) ls.getClass();
Method method = cls.getDeclaredMethod("add",Object.class);
method.invoke(ls,10);

这里通过反射向list中添加Integer变量,并没有报错,但是,这个时候如果在运行时候调用get()方法,那么如上所说,会在get方法前加入强制类型转换,所以会在运行时期报错.

Integer. Double 自动装箱与拆箱

在java中提供了自动装箱与拆箱的功能,就是把int变成Integer对象或者反过来.因为在泛型中只能存储对象而不能是普通值.而且在Integer或者Long中都有自己的数字缓存.都缓存了从-128~127之间的数字.意思就是在这些数字范围内的Integer对象都引用的是同一个对象.在看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
Integer a = 128;
Integer b = 128;
Integer c = 1;
Integer d = 2;
Integer e = 3;
Integer f = 3;
System.out.println(f == e);
System.out.println(c+d == e );
System.out.println(a==b);
}
输出:
true
true
false

这里可以看到从128开始就不缓存了.但是在128之前的数字都是缓存的.都引用的是同一个缓存的对象.但是在代码中最后还是使用equals来比较对象.这是最稳妥的.

foreach循环遍历

foreach循环遍历是代码中很常见的一个用法,但是他底层是怎么实现的呢,很多人不知道.其实也是很简单的,下面看实例;

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
ArrayList<Integer> ls = new ArrayList<>();
// 普通容器变为迭代器遍历
for (int i: ls){
System.out.println(i);
}
int[] arr = new int[10];
// foreach循环在数组中变为普通的for遍历循环
for (int i: arr){
System.out.println(i);
}
}

上面代码编译过后就能看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] var0) {
ArrayList var1 = new ArrayList();

Iterator var2 = var1.iterator();
//这里能看到 循环遍历变成了通过迭代器遍历
int var3;
while(var2.hasNext()) {
var3 = (Integer)var2.next();
System.out.println(var3);
}
int[] var7 = new int[10];
int[] var3 = var7;
int var4 = var7.length;
// 数组的变成了普通的遍历
for(int var5 = 0; var5 < var4; ++var5) {
int var6 = var3[var5];
System.out.println(var6);
}
}

循环遍历在普通容器中变成了迭代器遍历,在数组中变成了普通的for遍历.这也是为什么循环遍历的容器必须实现iterator接口的原因.

变长参数

变长参数其实就是一个数组,取决于你传入了几个参数.

1
2
3
4
5
public static void close(Closeable... objs) throws IOException {
for (Closeable obj: objs){
obj.close();
}
}

字节码反编译过后的代码

1
2
3
4
5
6
7
8
9
public static void close(Closeable... var0) throws IOException {
Closeable[] var1 = var0;
int var2 = var0.length;
for(int var3 = 0; var3 < var2; ++var3) {
Closeable var4 = var1[var3];
var4.close();
}

}

能够看到实际上objs就是一个数组,应该就是通过得到一个数组,然后在循环遍历.所以说尽量少用变长参数,因为变长参数会有一个内部的数组建立的过程,所以速度肯定会降低.

int short 优化

平时编写代码的时候可能不注意,但是看的话会发现编译器做了很多优化,比如就是int优化,这也是不小心发现的.例如下面的例子

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int a = 10;
int b = 128;
int c = 255;
int d = 1000000;
b+=10000000;
char g = 100;
double e = 20;
double f = 1000000;
System.out.println(a+b+c+d);
}

编译过后

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] var0) {
byte var1 = 10; //int a = 10;
short var2 = 128; // int b = 128;
short var3 = 255; // int c = 255;
int var4 = 1000000; // int d = 1000000;
int var10 = var2 + 10000000; // b+=10000000; 这里就是当数值变大了之后就会发现新申请了一个数值,之后变量var2的使用都变成了var10.
boolean var5 = true; //char g = 100;
double var6 = 20.0D; //double e = 20;
double var8 = 1000000.0D; //double f = 1000000;
System.out.println(var1 + var10 + var3 + var4);
}

可以看到当int的值比较小的时候可能会用byte或者short来代替int.
当此值变成比这个值更大的值的时候就能发现这个变量变了.var2变成var10,在之后用到var2的地方也都变成了var10.
但是细看这里的编译过后的代码就会发现一个问题,就是在char变量g变成了boolean类型.这里也就是编译器的第二个优化了.这里它会看此变量是否用过,如果在只有的程序中没有用过,那么就会赋值一个boolean类型.因为boolean类型可能是占用内存最小的了.