JVM 编译期处理(语法糖)

一、简介

        所谓编译期处理,其实就是语法糖,指的是 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)。

        下面的代码分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外,编译器转换的结果直接就是 class 字节码,只是为了方便阅读,给出了几乎等价的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

二、默认构造器

public class Candy {

    // 未提供任何的构造器
}

        编译成 class 后的代码如下

public class Candy {

    // 这个无参构造是编译器帮助我们加上的
    public Candy(){
        // 即调用父类 Object 的无参构造方法
        super();
    }
}

三、自动拆装箱

        自动拆装箱指的是 java 的基本类型和它的包装类型之间会有一个自动转换。这个特性是 jdk5 开始加入的,如下

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

// 上面的这段代码在 jdk5 之前是无法运行的,必须改成下面的这种写法
public class Candy2 {
    public static void main(String[] args) {
        Integer x = Integer.valueOf(1);
        int y=x.intValue();
    }
}

         显然之前版本的代码太麻烦了,需要在基本数据类型和包装类之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 jdk5 以后都由编译器在编译阶段完成,即 Candy1 类会在编译阶段被转换成 Candy2

四、泛型集合取值

        泛型也是在 jdk5 开始加入的新特性,但 java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当作了 Object 类型来处理

public class Candy {
    public static void main(String[] args) {
      List<Integer> list = new ArrayList<Integer>();
        // 实际调用的是 List.add(Object e)
      list.add(10);
        // 实际调用的是 Object obj = List.get(int index)
      Integer x = list.get(0);
    }
}

        在取值的时候,编译器真正生成的字节码中,还要一个额外做一个类型转换的操作

// 需要将 Object 转换为 Integer
Integer x = (Integer) list.get(0);

        如果前面的 x 变量类型修改为 int 基本类型,那么最终生成的字节码是

// 需要将 Object 转为 Integer,并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();

        幸运的是这些事情都不用自己来做。 

五、可变参数

        可变参数也是 jdk5 开始加入的新特性

public class Candy {
   public static void foo(String... args){
       // 直接赋值
       String [] argArr = args;
        System.out.println(argArr);
    }

    public static void main(String[] args) {
        foo("hello","world");
    }

}

        可变参数 String... args 其实是一个 String [] args,从代码中的赋值语句就可以看的出来,同样 java 编译器会在编译期间将上述代码变换为

public class Candy {
   public static void foo(String args []){
       // 直接赋值
       String [] argArr = args;
        System.out.println(argArr);
    }

    public static void main(String[] args) {
        foo(new String []{"hello","world"});
    }
}

        需要注意的是,如果调用了 foo() 则等价代码为 foo(new String []{}),创建了一个空的数据,而不会传递 null 进去。 

六、foreach 循环

        仍是 jdk5 开始加入的语法糖,数组的循环。

6.1 数组循环

public class Candy {
    public static void main(String[] args) {
        // 数组赋初值的简化写法也是语法糖
       int [] array = {1,2,3,4,5,6};
        for (int e:array) {
            System.out.println(e);
        }
    }
}

        会被编译器转换为

public class Candy {

    public Candy(){
    }
    public static void main(String[] args) {
        // 数组赋初值的简化写法也是语法糖
       int [] array = new int[]{1,2,3,4,5,6};
       for(int i=0;i<array.length;i++){
           int e= array[i];
           System.out.println(e);
       }
    }
}

 6.2 集合循环

public class Candy {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6);
        for (Integer e:list) {
            System.out.println(e);
        }
    }
}

        实际被编译器转换为对迭代器的调用

public class Candy {
    public Candy() {
    }
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer e = (Integer)iterator.next();
            System.out.println(e);
        }
    }
}

        需要注意的是,foreach 循环写法,能够配合数组,以及所有实现了 Iterator 接口的集合类一起使用,其中 Iterator 用来获取集合的迭代器(Iterator) 。

七、switch 字符串

        从 jdk7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,如下

public class Candy {
  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 Candy {
    public Candy() {
    }
    public static void choose(String str) {
        byte x = -1;
        switch (str.hashCode()) {
            // hello 的 hashcode
            case 99162322:
                if (str.equals("hello")) {
                    x = 0;
                }
                break;
            // world 的 hashcode
            case 113318802:
                if (str.equals("world")) {
                    x = 1;
                }
        }
        switch (x) {
            case 0:
                System.out.println("h");
                break;
            case 1:
                System.out.println("w");
        }
    }
}

        可以看到,执行了两遍 switch ,第一遍是根据字符串的 hashCode equals 将字符串转换为相应的 byte 类型,第二遍才是利用 byte 进行比较。 

        为什么第一遍时既比较 hashCode,又进行比较 equals 比较呢?hashCode 是为了提高效率,减少可能的比较,而 equals 是为了减少 hash 冲突。

八、switch 枚举

        switch 枚举的例子,原始代码如下

enum Sex{
    MAIL,FEMAIL
}
public class Candy {
    public static void foo(Sex sex){
        switch (sex){
            case MAIL:
                System.out.println("男");
                break;
            case FEMAIL:
                System.out.println("女");
                break;
        }
    }
}

         转换后代码如下

public class Candy {
    /**
     * 定义一个合成类(仅 JVM 使用,我们不可见)
     * 用来映射枚举的 oridinal 与数组元素的对应关系
     * 枚举的 oridinal 表示枚举对象的序号,从 0 开始
     * 即 MALE 的 oridinal()=0,FEMALE 的 oridinal()=1
     */
    static class $MAP{
        static int [] map = new int[2];
        static {
            map[Sex.MALE.ordinal()] = 1;
            map[Sex.FEMALE.ordinal()] = 2;
        }
    }
    public static void foo(Sex sex){
        // 数组的大小即为枚举元素的个数,里面存储 case 用来比对数字
        int x = $MAP.map[sex.ordinal()];
        switch (x){
            case 1:
                System.out.println("男");
                break;
        }
    }
}

九、枚举类

        jdk7 新增了枚举类,以前面的代码为例

enum Sex{
    MAIL,FEMAIL
}

        转换后的代码

public final class Sex extends Enum<Sex> {
    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};
    }
 
    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);
    }
}

十、try-with-resources

        jdk7 开始新增了对需要关闭的资源处理的特殊写法,可以省略 finally 里面的代码,如下

try(资源变量 = 创建资源对象){
   
}catch(){
   
}

        其中资源对象需要实现 AutoCloseable 接口,例如 InputStreamOutputStream Connection Statement ResultSet 等接口都实现了 AutoCloseable ,使用 try-with-resources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,如下

public class Candy {
    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 Candy {
    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 块中的 e
                        is.close();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

        为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常)

public class Candy {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource()) {
            int i = 1/0;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
class MyResource implements AutoCloseable {
    public void close() throws Exception {
        throw new Exception("close 异常");
    }
}

        输出如下:

java.lang.ArithmeticException: / by zero
	at test.Test6.main(Test6.java:7)
	Suppressed: java.lang.Exception: close 异常
		at test.MyResource.close(Test6.java:18)
		at test.Test6.main(Test6.java:6)

        如以上代码所示,两个异常信息都不会丢失。

十一、方法重写时的桥接方法

        我们都知道,方法重写时对返回值分两种情况。第一种父子类的返回值完全一致;第二种子类返回值可以是父类返回值的子类。

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() 没有命名冲突,可以用下面反射代码来验证: 

for (Method m : B.class.getDeclaredMethods()) {
    System.out.println(m);
}

        会输出

public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()

十二、匿名内部类

public class Candy11 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public 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() {
            @Override
            public 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);
    }
}

        需要注意的是,这里解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变化了,如果变化,那么 val$x 属性没有机会再跟着一起变化。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值