1

语法糖(Syntactic Sugar)的出现是为了降低我们编写某些代码时陷入的重复或繁琐,这使得我们使用语法糖后可以写出简明而优雅的代码。在Java中不加工的语法糖代码运行时可不会被虚拟机接受,因此编译器为了让这些含有语法糖的代码正常工作其实需要对这些代码进行加工,经过编译器在生成class字节码的阶段完成解语法糖(desugar)的过程,那么这些语法糖最终究竟被编译成了什么呢,在这里列举了如下的一些Java典型的语法糖,结合实例和它们的编译结果分析一下。本文为该系列的第一篇。

泛型和类型擦除

java的泛型实际上是伪泛型,在编译后编译器会擦除泛型对象的参数化类型,也就是说源代码中的<T>类型其实都会擦除,最终成为class字节码中的Object类型,赋值等操作也就会直接转换为强制的类型转换,这样做无风险的原因是在编译的标注检查阶段其实已经进行了泛型的检查,如果当时无法通过检查的话编译无法通过。

另外,这个泛型信息不是真的就此丢掉了,class字节码中还是会保留Signature属性来记录泛型对象在源码中的参数化类型。

代码:

public class Main {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        strList.add("aaa");
        String strEle = strList.get(0);
    }
}

main方法在javap编译后的字节码

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #4                  // String aaa
        11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        16: pop
        17: aload_1
        18: iconst_0
        19: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        24: checkcast     #7                  // class java/lang/String
        27: astore_2
        28: return

上面我们演示了一个参数化类型为StringList的泛型对象strListaddget操作:

  • add操作:对应字节码中的8~16个字节:我们可以看到最关键的add操作其实就是

    invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

    调用的其实是java/util/List类的add方法,此方法的入参类型是Ljava/lang/Object;,返回值类型是Z,翻译过来就是List类的boolean add(Object o)方法,这里并没有参数化类型String的什么事情。

  • get操作:对应字节码中的17~27个字节:我们可以看到最关键的get操作其实就是

    invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
    checkcast     #7                  // class java/lang/String

    调用的其实是java/util/List类的get方法,此方法的入参类型是I,返回值类型是Ljava/lang/Object;,翻译过来就是List类的Object get(int i)方法,执行完后将获得的结果做了checkcast,检查返回的对象类型是否是String

从上面的分析我们不难看出,Java泛型到了编译出结果的时候参数化类型已经没有什么作用了,就是简单做了强制的类型转换。这段去掉了语法糖的代码如下:

public class Main {
    public static void main(String[] args) {
        List strList = new ArrayList();
        strList.add((Object)"aaa");
        String strEle = (String) strList.get(0);
    }
}

Java的泛型是伪泛型的原因如上,在运行时这个代码完全体会不到不同参数化类型的List有什么不同。而泛型参数化类型的用武之地更多的是在编译时用来做检验类型使用的,正常情况下如果编译时通过检验当然就不会在运行期类型强制转换的时候出现异常,更何况其实字节码中还有checkcast的显式类型检查。

如果使用javac-g:vars参数来保留class字节码中方法的局部变量信息,那么我们可以看到额外的信息:

LocalVariableTable:
Start  Length  Slot  Name   Signature
    0      29     0  args   [Ljava/lang/String;
    8      21     1 strList   Ljava/util/List;
   28       1     2 strEle   Ljava/lang/String;
LocalVariableTypeTable:
Start  Length  Slot  Name   Signature
    8      21     1 strList   Ljava/util/List<Ljava/lang/String;>;

其中的LocalVariableTypeTable属性记录了strList的擦除泛型前的类型:Ljava/util/List<Ljava/lang/String;>;,翻译过来其实就是List<String>,如果在反射中获取泛型变量的类型元信息,其来源其实就是这个Signature。这也算是Java为了弥补因类型擦除而导致的class字节码中的类型数据缺失而做出的额外努力吧。

变长参数:编译后变成数组类型的参数

变长参数会被编译成为数组类型的参数,变长参数只能出现在参数列表的结尾以消除歧义。

代码:

public class Main {
    public static void method(String... args) {

    }
}

method方法在编译后:

public static void method(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=0, locals=1, args_size=1
         0: return

我们可以清楚地看到方法的特征符是([Ljava/lang/String;)V,即参数是[Ljava/lang/String;,翻译过来就是String[],即数组类型。

这段去掉了语法糖的代码如下:

public class Main {
    public static void method(String[] args) {

    }
}

自动装箱拆箱

编译后装箱通过valueOf()变成了对象,拆箱通过xxxValue()变成了原始类型值。

代码:

public class Main {
    public static void main(String[] args) {
        Integer x = 1;
        int y = x;
    }
}

main方法编译后:

    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: astore_1
         5: aload_1
         6: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
         9: istore_2
        10: return

这里我们可以明显看到Integer x = 1;编译时x转换成了java/lang/Integer.valueOf生成的引用类型Integer变量,而int y = x;编译时y转换成了java/lang/Integer.intValue生成的原始类型int变量。

去掉了语法糖的代码如下:

public class Main {
    public static void main(String[] args) {
        Integer x = Integer.valueOf(1);
        int y = x.intValue();
    }
}

遍历循环

编译后变成了迭代器遍历。

代码:

public class Main {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        for (String str : strList) {
            System.out.println(str);
        }
    }
}

main方法编译后:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: invokeinterface #4,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
        14: astore_2
        15: aload_2
        16: invokeinterface #5,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
        21: ifeq          44
        24: aload_2
        25: invokeinterface #6,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
        30: checkcast     #7                  // class java/lang/String
        33: astore_3
        34: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        37: aload_3
        38: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        41: goto          15
        44: return
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 15
          locals = [ class java/util/List, class java/util/Iterator ]
        frame_type = 250 /* chop */
          offset_delta = 28

从上面我们可以看到遍历循环的语法糖被替换成了List.iterator的循环操作,用下面的代码即可表达这段编译后的去掉语法糖的代码:

public class Main {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        Iterator strIterator = strList.iterator();
        while(strIterator.hasNext()){
            System.out.println((String) strIterator.next());
        }
    }
}

条件编译

编译后将常量不可达条件分支直接在编译结果中消除掉。

代码:

public class Main {
    public static void main(String[] args) {
        if (true) {
            System.out.println("Yes");
        } else {
            System.out.println("No");
        }
    }
}

main方法编译后:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Yes
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}

从上面我们可以看到常量不可达条件直接就在编译结果中略去了,仿佛就没有这个分支一样,用下面的代码即可表达这段编译后的去掉语法糖的代码:

public class Main {
    public static void main(String[] args) {
        System.out.println("Yes");
    }
}

需要注意的是这里强调的是常量不可达条件才会略去,比如直接就是true的分支或者1==1这样的分支是会保留的,如果是变量经过运算后才被确定为不可达是不会发生这种条件编译的,比如:

public class Main {
    public static void main(String[] args) {
        int i = 1;
        if (i==1) {
            System.out.println("Yes");
        } else {
            System.out.println("No");
        }
    }
}

编译后还是会走ifelse判断:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: iload_1
         3: iconst_1
         4: if_icmpne     18
         7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #3                  // String Yes
        12: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: goto          26
        18: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        21: ldc           #5                  // String No
        23: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        26: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 18
          locals = [ int ]
        frame_type = 7 /* same */
}

内部类

内部类即是类中类,我们来看这个简单的例子:

代码:

public class Main {

    class Person{
        String name;
        Integer age;

        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    }

    public void demo(String[] args) {
        Person person = new Person("ccc", 20);
    }
}

来看看编译后的结果,编译后会将内部类Person单独拿出来做编译,不过语法糖褪去后编译器做了一些处理,比如为Person类加了与外部的Main类相联系的字段this$0

...
class top.jinhaoplus.Main$Person
...
{
  java.lang.String name;
    descriptor: Ljava/lang/String;
    flags:

  java.lang.Integer age;
    descriptor: Ljava/lang/Integer;
    flags:

  final top.jinhaoplus.Main this$0;
    descriptor: Ltop/jinhaoplus/Main;
    flags: ACC_FINAL, ACC_SYNTHETIC

  public top.jinhaoplus.Main$Person(top.jinhaoplus.Main, java.lang.String, java.lang.Integer);
    descriptor: (Ltop/jinhaoplus/Main;Ljava/lang/String;Ljava/lang/Integer;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=4
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Ltop/jinhaoplus/Main;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: aload_0
        10: aload_2
        11: putfield      #3                  // Field name:Ljava/lang/String;
        14: aload_0
        15: aload_3
        16: putfield      #4                  // Field age:Ljava/lang/Integer;
        19: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   Ltop/jinhaoplus/Main$Person;
            0      20     1 this$0   Ltop/jinhaoplus/Main;
            0      20     2  name   Ljava/lang/String;
            0      20     3   age   Ljava/lang/Integer;
}

这里翻译过来类似这样的:

class Person {
    String name;
    Integer age;
    final Main this$0;

    public Person(final Main this$0, String name, Integer age) {
        this.this$0 = this$0;
        this.name = name;
        this.age = age;
    }
}

public class Main {
    public void demo(String[] args) {
        Person person = new Person(this, "ccc", 20);
    }
}

至于为什么需要这个多余的外部类的字段呢,其实是为了通过它来获取外部类中的信息,我们对例子加以改造,添加两个外部类的字段secret1secret2

public class Main {

    private String secret1;
    private String secret2;

    class Person{
        String name;
        Integer age;

        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        public void getSecrets(){
            System.out.println(secret1);
            System.out.println(secret2);
        }
    }

    public void demo(String[] args) {
        Person person = new Person("ccc", 20);
        person.getSecrets();
    }
}

这个时候编译的结果是Main为了对外提供自己属性的值自动添加了静态方法access$000(Main)access$100(Main)

static java.lang.String access$000(top.jinhaoplus.Main);
    descriptor: (Ltop/jinhaoplus/Main;)Ljava/lang/String;
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field secret1:Ljava/lang/String;
         4: areturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0    x0   Ltop/jinhaoplus/Main;

static java.lang.String access$100(top.jinhaoplus.Main);
    descriptor: (Ltop/jinhaoplus/Main;)Ljava/lang/String;
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #1                  // Field secret2:Ljava/lang/String;
         4: areturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0    x0   Ltop/jinhaoplus/Main;
}

而内部类编译后的结果在获取外部类的属性的时候其实就是调用暴露出的这些方法:

public void getSecret();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: getfield      #1                  // Field this$0:Ltop/jinhaoplus/Main;
         7: invokestatic  #6                  // Method top/jinhaoplus/Main.access$000:(Ltop/jinhaoplus/Main;)Ljava/lang/String;
        10: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: aload_0
        17: getfield      #1                  // Field this$0:Ltop/jinhaoplus/Main;
        20: invokestatic  #8                  // Method top/jinhaoplus/Main.access$100:(Ltop/jinhaoplus/Main;)Ljava/lang/String;
        23: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        26: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  this   Ltop/jinhaoplus/Main$Person;
}

翻译过来其实就是这样子的:

class Person {
    String name;
    Integer age;
    final Main this$0;

    public Person(final Main this$0, String name, Integer age) {
        this.this$0 = this$0;
        this.name = name;
        this.age = age;
    }

    public void getSecrets(){
        System.out.println(Main.access$000(this$0));
        System.out.println(Main.access$100(this$0));
    }
}

public class Main {
    private String secret1;
    private String secret2;

    public void demo(String[] args) {
        Person person = new Person(this, "ccc", 20);
    }

    public static String access$000(Main main) {
        return main.secret1;
    }

    public static String access$100(Main main) {
        return main.secret2;
    }
}

JinhaoPlus
1.5k 声望92 粉丝

扎瓦程序员