所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成
和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外,
编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并
不是编译器还会转换出中间的 java 源码,切记。
public class Candy1 {
}
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的public Candy1() {super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."":()V}
}
这个特性是在JDK1.5添加的
public class Candy2 {public static void main(String[] args) {Integer x = 1;int y = x;}
}
在JDK1.5之前上面这种代码是无法通过编译的
public class Candy2 {public static void main(String[] args) {Integer x = Integer.valueOf(1);int y = x.intValue();}
}
显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2
当没有引入泛型的时候
public class Candy3 {public static void main(String[] args) {Object [] array=new Object[10];array[0]=10; //编译器不会有问题ArrayList arrayList=new ArrayList();arrayList.add(Integer.valueOf(10)); //编译器和运行时都不会有问题arrayList.add("hello");}
}
我们知道在我们的Java中,有一个二进制向后兼容,也就是比如一个在JDK1.2中编译出来的Class文件,必须保证在JDK12乃至之后的版本也能正常运行
为了能正常运行,所以有两条路走
我们的Java采用的是第二种
我们继续以ArrayList为例来介绍Java泛型的类型擦除具体是如何实现的。由于Java选择了第二条路,直接把已有的类型泛型化。要让所有需要泛型化的已有类型,譬如ArrayList,原地泛型化后变成了ArrayList, 而且保证以前直接用ArrayList的代码在泛型新版本里必须还能继续用这同一个容器,这就必须让所有泛型化的实例类型,譬如ArrayList、ArrayLists 这些全部自动成为ArayList的子类型才能可以,否则类型转换就是不安全的。由此就引出了“裸类型” (Raw Type)的概念,裸类型应被视为所有该类型泛型化实例的共同父类型(Super Type), 只有这样,像代码清单10-4中的赋值才是被系统允许的从子类到父类的安全转型。
ArrayList ilist = new ArayListLni
ArrayList list; //裸类型
list = ilist;
list = slist;
public class Candy3 {public static void main(String[] args) {List list = new ArrayList<>();list.add(10); // 实际调用的是 List.add(Object e)Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);}
}
所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
验证一下
Classfile /E:/JAVA代码/JVMDemo/out/production/chapter01/com/atguigu/java/Candy3.classLast modified 2022-12-1; size 791 bytesMD5 checksum 2c1fe36d6e75a1c8176e8d87343351e4Compiled from "Candy3.java"
public class com.atguigu.java.Candy3minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #9.#29 // java/lang/Object."":()V#2 = Class #30 // java/util/ArrayList#3 = Methodref #2.#29 // java/util/ArrayList."":()V#4 = Methodref #7.#31 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;#5 = InterfaceMethodref #32.#33 // java/util/List.add:(Ljava/lang/Object;)Z#6 = InterfaceMethodref #32.#34 // java/util/List.get:(I)Ljava/lang/Object;#7 = Class #35 // java/lang/Integer#8 = Class #36 // com/atguigu/java/Candy3#9 = Class #37 // java/lang/Object#10 = Utf8 #11 = Utf8 ()V#12 = Utf8 Code#13 = Utf8 LineNumberTable#14 = Utf8 LocalVariableTable#15 = Utf8 this#16 = Utf8 Lcom/atguigu/java/Candy3;#17 = Utf8 main#18 = Utf8 ([Ljava/lang/String;)V#19 = Utf8 args#20 = Utf8 [Ljava/lang/String;#21 = Utf8 list#22 = Utf8 Ljava/util/List;#23 = Utf8 x#24 = Utf8 Ljava/lang/Integer;#25 = Utf8 LocalVariableTypeTable#26 = Utf8 Ljava/util/List;#27 = Utf8 SourceFile#28 = Utf8 Candy3.java#29 = NameAndType #10:#11 // "":()V#30 = Utf8 java/util/ArrayList#31 = NameAndType #38:#39 // valueOf:(I)Ljava/lang/Integer;#32 = Class #40 // java/util/List#33 = NameAndType #41:#42 // add:(Ljava/lang/Object;)Z#34 = NameAndType #43:#44 // get:(I)Ljava/lang/Object;#35 = Utf8 java/lang/Integer#36 = Utf8 com/atguigu/java/Candy3#37 = Utf8 java/lang/Object#38 = Utf8 valueOf#39 = Utf8 (I)Ljava/lang/Integer;#40 = Utf8 java/util/List#41 = Utf8 add#42 = Utf8 (Ljava/lang/Object;)Z#43 = Utf8 get#44 = Utf8 (I)Ljava/lang/Object;
{public com.atguigu.java.Candy3();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/atguigu/java/Candy3;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: new #2 // class java/util/ArrayList3: dup4: invokespecial #3 // Method java/util/ArrayList."":()V7: astore_18: aload_19: bipush 1011: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;//注意点 调用的方法对应的参数是Object14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z19: pop20: aload_121: iconst_0//注意点 调用的方法对应返回的参数是Object22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;//注意点 这种指令是类型转换的意思27: checkcast #7 // class java/lang/Integer30: astore_231: returnLineNumberTable:line 8: 0line 9: 8line 10: 20line 11: 31LocalVariableTable:Start Length Slot Name Signature0 32 0 args [Ljava/lang/String;8 24 1 list Ljava/util/List;31 1 2 x Ljava/lang/Integer;LocalVariableTypeTable:Start Length Slot Name Signature//注意点8 24 1 list Ljava/util/List;
}
SourceFile: "Candy3.java"
正是由于类型擦除的隐蔽存在,直接导致了众多的泛型灵异问题,如当泛型遇见重载
public class GenericTypes {public static void method(List list) {System.out.println("List list");}public static void method(List list) {System.out.println("List list");}
}
这段代码将无法进行编译,因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型List<E>,擦除动作导致这两种方法的特征签名变得一模一样。初步看来,无法重载的原因已经找到了,但真的就是如此吗?只能说,泛型擦除成相同的原生类型只是无法重载的其中一部分原因,请再接着如下代码
public class GenericTypes {public static String method(List list) {System.out.println("invoke method List list");return "";}public static int method(List list) {System.out.println("invoke method List list");return 1;}public static void main(String[] args) {method(new ArrayList());method(new ArrayList());}
}
执行结果
invoke method List list
invoke method List list
方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,但是在 Class 文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说,两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个 Class 文件中的。
由于 List<String>和 List<Integer>擦除后是同一个类型,我们只能添加两个并不需要实际使用到的返回值才能完成重载。
擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。
仍是 JDK 5 开始引入的语法糖,数组的循环
public class Candy5_1 {public static void main(String[] args) {int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦for (int e : array) {System.out.println(e);}}
}
编译后的代码
public class Candy5_1 {public Candy5_1() {} public static void main(String[] args) {int[] array = new int[]{1, 2, 3, 4, 5};for(int i = 0; i < array.length; ++i) {int e = array[i];System.out.println(e);}}
}
集合的循环
public class Candy5_2 {public static void main(String[] args) {List list = Arrays.asList(1,2,3,4,5);for (Integer i : list) {System.out.println(i);}}
}
实际被编译器转换为对迭代器的调用
public class Candy5_2 {public Candy5_2() {}public static void main(String[] args) {List list = Arrays.asList(1, 2, 3, 4, 5);Iterator iter = list.iterator();while(iter.hasNext()) {Integer e = (Integer)iter.next();System.out.println(e);}}
}
注意
foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中
Iterable 用来获取集合的迭代器( Iterator )
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
public class Candy6_1 {public static void choose(String str) {switch (str) {case "hello": {System.out.println("h");break;}case "world": {System.out.println("w");break;}}}
}
注意
switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚
public class Candy6_1 {public Candy6_1() {}public static void choose(String str) {byte x = -1;switch(str.hashCode()) {case 99162322: // hello 的 hashCodeif (str.equals("hello")) {byte x = 0;} break;case 113318802: // world 的 hashCodeif (str.equals("world")) {x = 1;}}switch(x) {case 0:System.out.println("h");break;case 1:System.out.println("w");}}
}
enum Sex {MALE, FEMALE
}
public class Candy7 {public static void foo(Sex sex) {switch (sex) {case MALE:System.out.println("男"); break;case FEMALE:System.out.println("女"); break;}}
}
编译后的代码
public class Candy7 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/static class $MAP {// 数组大小即为枚举元素个数,里面存储case用来对比的数字static int[] map = new int[2];static {map[Sex.MALE.ordinal()] = 1;map[Sex.FEMALE.ordinal()] = 2;}}
public static void foo(Sex sex) {int x = $MAP.map[sex.ordinal()];switch (x) {case 1:System.out.println("男");break;case 2:System.out.println("女");break;
}
enum Sex {MALE, FEMALE
}
编译后的代码
JDK 7 新增了枚举类,以前面的性别枚举为例
public final class Sex extends Enum {public static final Sex MALE;public static final Sex FEMALE;private static final Sex[] $VALUES;static {MALE = new Sex("MALE", 0);FEMALE = new Sex("FEMALE", 1);$VALUES = new Sex[]{MALE, FEMALE};}
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
* *
@param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is
assigned
*/private Sex(String name, int ordinal) {super(name, ordinal);}public static Sex[] values() {return $VALUES.clone();} public static Sex valueOf(String name) {return Enum.valueOf(Sex.class, name);}
}
JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources`
try(资源变量 = 创建资源对象){
}
catch( ) {
}
其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如
public class Candy9 {public static void main(String[] args) {try(InputStream is = new FileInputStream("d:\\1.txt")) {System.out.println(is);} catch (IOException e) {e.printStackTrace();}}
}
会被转换成
public class Candy9 {public Candy9() {}public static void main(String[] args) {try {InputStream is = new FileInputStream("d:\\1.txt");Throwable t = null;try {System.out.println(is);} catch (Throwable e1) {// t 是我们代码出现的异常t = e1;throw e1;} finally {// 判断了资源不为空if (is != null) {// 如果我们代码有异常if (t != null) {try {is.close();} catch (Throwable e2) {// 如果 close 出现异常,作为被压制异常添加t.addSuppressed(e2);}} else {// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 eis.close();}}}} catch (IOException e) {e.printStackTrace();}}
}
为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常)
我们都知道,方法重写时对返回值分两种情况:
class A {public Number m() {return 1;}
}
class B extends A {@Override// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类public Integer m() {return 2;}
}
对于子类,java 编译器会做如下处理:
class B extends A {public Integer m() {return 2;}// 此方法才是真正重写了父类 public Number m() 方法public synthetic bridge Number m() {// 调用 public Integer m()return m();//向上转型是可以通过的}
}
其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突
这里是内部类的语法糖,只是挑选了典型的匿名内部类
public class Candy11 {public static void main(String[] args) {Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("ok");}};}
}
// 额外生成的类
final class Candy11$1 implements Runnable {Candy11$1() {}public void run() {System.out.println("ok");}
}
//外部类的代码
public class Candy11 {public static void main(String[] args) {Runnable runnable = new Candy11$1();}
}
引入局部变量的匿名内部类
public class Candy11 {public static void test(final int x) {Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("ok:" + x);}};}
}
转换后的代码
// 额外生成的类
final class Candy11$1 implements Runnable {int val$x;Candy11$1(int x) {this.val$x = x;}public void run() {System.out.println("ok:" + this.val$x);}
}
public class Candy11 {public static void test(final int x) {Runnable runnable = new Candy11$1(x);}
}
首先我们的Inner和Outer各自会生成对应的lclass对象,那么就是内部类不会随着方法执行完毕就会消耗,那么当外部方法执行完毕,其局部变量也会销毁。但是内部类的对象还可能存在(只有没有人引用才会死亡)。这就出现了一个矛盾,那么内部类对象就访问了一个不存在的变量,所以我们的解决方法就是将局部变量复制一份给内部类作为成员变量,这样当方法死亡的时候,内部类依然可以访问,当时这样我们必须要保证其两个变量是一样的,可能我们内部类去做修改变量的操作,为了防止这种情况的出现,所以在内部类使用的局部变量必须是final的