转自:https://1fishman.github.io/2019/04/21/java%E7%BC%96%E8%AF%91%E4%BC%98%E5%8C%96/
java编译器为我们做了很多优化,比如在java中泛型并不是真正的泛型,在编译的时候会进行泛型擦除,使用的时候再进行类型转换.或者Integer自动装箱和拆箱.foreach循环遍历等等.
在java中泛型并不是真正的泛型,因为有一个java早期没有泛型的时候都是通过Object来代替泛型的,因为java中每个对象都是继承自Object的.在通过类型转换来实现泛型.现在有了泛型.通过泛型来指定类型.但是这个泛型也不是真正的泛型.在编译期间都会进行泛型擦除.使其变为普通的类型.下面来看个例子
源代码
ArrayList<Integer> ls = new ArrayList<>(); ls.add(1); ls.add(2); int a = ls.get(0); System.out.println(a);
编译之后的字节码反编译过来的文件.
ArrayList var1 = new ArrayList(); var1.add(1); var1.add(2); int var2 = (Integer)var1.get(0); System.out.println(var2);
这里可以看到这个时候哦ArrayList已经没有泛型了,只是他原来的类型.在获取字段的时候会有一个类型转换的操作.所以有一点就是在进行方法重载的时候ArrayList 和 ArrayList 类型是一样的,编译会出现错误.
也正是有了泛型擦除,就有了一个问题,你可以向一个定义了类型的容器中添加其他类型的变量.看下面代码:
1 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方法前加入强制类型转换,所以会在运行时期报错.
在java中提供了自动装箱与拆箱的功能,就是把int变成Integer对象或者反过来.因为在泛型中只能存储对象而不能是普通值.而且在Integer或者Long中都有自己的数字缓存.都缓存了从-128~127之间的数字.意思就是在这些数字范围内的Integer对象都引用的是同一个对象.在看一个例子
1 5 9 13 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循环遍历是代码中很常见的一个用法,但是他底层是怎么实现的呢,很多人不知道.其实也是很简单的,下面看实例;
1 5 9 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 5 9 13 17 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 5 |
public static void close(Closeable... objs) throws IOException { for (Closeable obj: objs){ obj.close(); } } |
字节码反编译过后的代码
1 5 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优化,这也是不小心发现的.例如下面的例子
1 5 9 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 5 9 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类型可能是占用内存最小的了.