美文网首页
04 JVM编译期处理优化

04 JVM编译期处理优化

作者: 攻城老狮 | 来源:发表于2021-09-22 10:43 被阅读0次

    语法糖:是指 java 编译器所有 *.java 源码编译为 *.class 字节码的过程,自动生成和转换的一些代码, 主要是为了减轻程序员的负担。

    1 默认构造器

    1. Java代码
    // 编译期处理(语法糖)——默认构造器
    public class TestDemo {
        
    }
    
    1. 自动转换的代码
    // 编译期处理(语法糖)——默认构造器
    public class TestDemo {
        // 这个无参构造是编译器帮助我们加止的
        public TestDemo() {
            super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
        }
    }
    
    1. 总结

    未声明构造方法,则无参构造会由编译器帮助加上。即调用父类 Object的无参构造方法:java/lang/Object."<init>"方法。

    2 自动拆装箱

    1. Java代码
    • 这段代码在 JDK 5 之前无法编译通过的
    // 编译期处理(语法糖)——自动拆装箱
    public class TestDemo {
        public static void main(String[] args) {
            Integer x = 1;
            int y = x;
        }
    }
    
    1. 自动转换的代码
    // 编译期处理(语法糖)——自动拆装箱
    public class TestDemo {
        public static void main(String[] args) {
            Integer x = Integer.valueOf(1); // 装箱
            int y = x.intValue(); // 拆箱
        }
    }
    
    1. 总结

    自动拆装箱转换的事情在 JDK 5 以后 装箱与拆箱操作 都由编译器在编译阶段完成

    3 泛型擦除

    1. Java代码
    // 编译期处理(语法糖)——泛型擦除
    public class TestDemo {
        public static void main(String[] args) {
            List<Integer> list = new ArrayList<>();
            list.add(10); // 实际调用的是 List.add(Object e)
            Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
        }
    }
    
    1. 自动转换的代码

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

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

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

    // 需要将 Object 转为 Integer,并执行拆箱操作
    int x = ((Integer)list.get(0)).intValue();
    
    1. 反编译Java代码
                     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: bipush        10
            11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
            14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 调用的是传递Object类型参数的方法
            19: pop
            20: aload_1
            21: iconst_0
            22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 调用的是返回Object类型的方法
            27: checkcast     #7                  // class java/lang/Integer 类型转换
            30: astore_2
            31: return
          LineNumberTable:
            line 9: 0
            line 10: 8
            line 11: 20
            line 12: 31
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      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<Ljava/lang/Integer;>;
    
    1. 总结
    • 在JDK 5 及之后,泛型的类型转换麻烦事都不用自己做,java编译时就会帮我们做好。也就是说泛型参数只用于编译阶段的类型检查,不影响运行阶段(字节码的执行)
    • 擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息

    4 可变参数

    1. Java代码
    // 编译期处理(语法糖)——可变参数
    public class TestDemo {
        public static void foo(String... args) {
            String[] array = args; // 直接赋值
            System.out.println(array);
        }
     
        public static void main(String[] args) {
            foo("hello", "world");
        }
    }
    
    1. 自动转换的代码
    // 编译期处理(语法糖)——可变参数
    public class TestDemo {
        public static void foo(String[] args) { // 编译器将可变参数转换为数组
            String[] array = args; // 直接赋值
            System.out.println(array);
        }
     
        public static void main(String[] args) {
            foo("hello", "world");
        }
    }
    
    1. 总结
    • 可变参数也是 JDK 5 开始加入的新特性;
    • 可变参数 String... args 其实是一个 String[] args,编译器会将可变参数转换为数组;
    • 如果调用了 foo() 则等价代码为 foo(new String[] {}),创建了一个空的数组,而不会传递 null 进去。

    5 foreach循环

    5.1 数组循环

    1. Java代码
    // 编译期处理(语法糖)——foreach
    public class TestDemo {
        public static void main(String[] args) {
            int[] array = {1,2,3,4,5}; // 数组赋初值的简化写法也是语法糖哦
            for (int e : array) {
                System.out.println(e);
            }
        }
    }
    
    1. 自动转换的代码
    // 编译期处理(语法糖)——foreach
    public class TestDemo {
        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);
            }
        }
    }
    

    5.2 集合循环

    1. Java代码
    // 编译期处理(语法糖)——foreach
    public class TestDemo {
        public static void main(String[] args) {
            List<Integer> list = Arrays.asList(1,2,3,4,5);
            for (Integer i : list) {
                System.out.println(i);
            }
        }
    }
    
    1. 自动转换的代码
    // 编译期处理(语法糖)——foreach
    public class TestDemo {
        public static void main(String[] args) {
            List<Integer> list = Arrays.asList(1,2,3,4,5);
            Iterator iter = list.iterator(); // 获取迭代器
            while (iter.hasNext()) {
                Integer e = (Integer)iter.next(); // 类型转换
                System.out.println(e);
            }
        }
    }
    
    1. 总结
    • JDK 5 开始引入的语法糖
    • 数组的循环 foreach 循环会被编译器转换为 fori 循环
    • 集合的循环 实际被编译器转换为对迭代器的调用
    • foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器 (Iterator)

    6 switch

    6.1 switch-String字符串

    6.1.1 hash未碰撞

    1. Java代码
    // 编译期处理(语法糖)——switch-string
    public class TestDemo {
        public static void choose(String str) {
            switch (str) {
                case "hello": {
                    System.out.println("h");
                    break;
                }
                case "world": {
                    System.out.println("w");
                    break;
                }
            }
        }
    }
    
    1. 自动转换的代码
    • switch 配合 String 和枚举使用时,变量不能为 null
    • 转换后的代码执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应的 byte 类型,第二遍才利用 byte 执行进行比较。
    // 编译期处理(语法糖)——switch-string
    public class TestDemo {
        public static void choose(String str) {
            byte x = -1;
            switch (str.hashCode()) {
                case 99162322: // hello 的 hashCode
                    if (str.equals("hello")) {
                        x = 0;
                    }
                case 113318802: // word 的 hashCode
                    if (str.equals("world")) {
                        x = 1;
                    }
            }
            switch (x) {
                case 0:
                    System.out.println("h");
                    break;
                case 1:
                    System.out.println("w");
                    break;
            }
        }
    }
    

    6.1.2 hash碰撞

    1. Java代码
    • hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BM 和 C. 这两个字符串的 hashCode 值都是 2123
    // 编译期处理(语法糖)——switch-string
    public class TestDemo {
        public static void choose(String str) {
            switch (str) {
                case "BM": {
                    System.out.println("h");
                    break;
                }
                case "C.": {
                    System.out.println("w");
                    break;
                }
            }
        }
    
    1. 自动转换的代码
    // 编译期处理(语法糖)——switch-string
    public class TestDemo {
        public static void choose(String str) {
            byte x = -1;
            switch (str.hashCode()) {
                case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
                    if (str.equals("C.")) {
                        x = 0;
                    } else if (str.equals("BM")) {
                        x = 1;
                    }
                default:
                    switch (x) {
                        case 0:
                            System.out.println("h");
                            break;
                        case 1:
                            System.out.println("w");
                            break;
                    }
            }
        }
    }
    

    6.1.3 总结

    • switch-string字符串语法糖,从JDK 7 开始
    • switch-string字符串,会java 编译器调整为执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应的 byte 类型,第二遍才利用 byte 执行进行比较。
    • hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突

    6.2 switch-枚举类

    1. Java代码
    // 编译期处理(语法糖)——switch-enum
    enum Sex {
        MALE, FEMALE;
    }
     
    public class TestDemo {
        public static void foo(Sex sex) {
            switch (sex) {
                case MALE:
                    System.out.println("男"); break;
                case FEMALE:
                    System.out.println("女"); break;
            }
        }
    }
    
    1. 自动转换的代码
    public class TestDemo {
        /**
         * 定义一个合成类(仅 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;
                }
            }
        }
    }
    
    1. 总结
    • 枚举类使用 switch 时,java编译器会帮我们自动合成一个静态类用来映射枚举的 ordinal 下标 与数组元素的关系,数组大小即为枚举元素个数,里面存储 case 是用来对比的数字。
    • 在 switch 方法中,取出枚举下标对应静态类中定义的数字,进行switch判断。

    7 枚举类

    1. Java代码
    enum Sex {
        MALE, FEMALE;
    }
    
    1. 自动转换的代码
    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};
        }
        /**
         * Sole constructor. Programmers cannot invoke this constructor.
         * It is for use 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);
        }
    }
    
    1. 总结
    • 枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。每个枚举类型都继承自 java.lang.Enum,并自动添加了 values 和 valueOf 方法。而每个枚举常量是一个静态常量字段。所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化。

    8 try-with-resources

    1. Java代码
    • 资源对象需要实现 AutoCloseable 接口,例如:InputStream、OutputStream、Connection、Statement、ResultSet 等接口实现了 AutoCloseable,使用 try-with-resources 可以不用写 finally 语句块,编译器会帮助生成 finally 代码关闭资源
    // 编译期处理(语法糖) —— try-with-resources
    public class TestDemo {
        public static void main(String[] args) {
            try (InputStream is = new FileInputStream("in/1.txt")){
                System.out.println(is);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    1. 自动转换的代码
    • 设计一个 addSuppressed (Throwable e) (添加被压制异常)的方法是为了防止异常信息的丢失。
    // 编译期处理(语法糖) —— try-with-resources
    public class TestDemo {
        public static void main(String[] args) {
            try {
                InputStream is = new FileInputStream("in/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();
            }
        }
    }
    
    1. 测试抛出未丢失异常信息
    // 编译期处理(语法糖) —— try-with-resources
    public class TestDemo {
        public static void main(String[] args) {
            try (MyResource resource = new MyResource()) {
                int i = 1/0;
            } catch (Exception e){
                e.printStackTrace();
            }
        }
     
        static class MyResource implements AutoCloseable {
            //@Override
            public void close() throws Exception {
                throw new Exception("close 异常");
            }
        }
    }
    
    1. 总结
    • JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources。
    • 资源对象需要实现 AutoCloseable 接口,例如:InputStream、OutputStream、Connection、Statement、ResultSet 等接口实现了 AutoCloseable,使用 try-with-resources 可以不用写 finally 语句块,编译器会帮助生成 finally 代码关闭资源。
    • try-with-resources 能实现正常资源的关闭,如果出现了异常,异常信息不会丢失。

    9 重写桥接

    1. Java代码
    class A {
        public Number m() {
            return 1;
        }
    }
     
    class B extends A {
        @Override
        // 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
        public Integer m() {
            return 2;
        }
    }
    
    1. 自动转换的代码
    • 其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突。
    class B extends A {
        public Integer m() {
            return 2;
        }
        // 此方法才是真正重写了父类 public Number m() 方法; synthetic bridge 是jvm合成的,对我们不可见
        public synthetic bridge Number m() {
            // 调用 public Integer m()
            return m();
        }
    }
    
    1. 验证该桥接方法的存在
    for (Method m : B.class.getDeclaredMethods()) {
        System.out.println(m);
    }
    

    输出结果:

    public java.lang.Integer com.yqj.B.m()
    public java.lang.Number com.yqj.B.m()
    
    1. 总结
    • 父类 A 的 m 的返回值是 Number 类型,子类 B 重写 m 返回的值是 Integer 类型,对于 Java 语言是重写的,但对于 Java 虚拟机解析来说却不是重写的,只有当两个方法的参数类型以及返回类型一致时,Java 虚拟机才会判定为重写,为了保持重写的语义,Java 编译器会在 B类 的字节码文件中自动生成一个桥接方法来保证重写语义。

    10 匿名内部类

    10.1 未引用局部变量的匿名内部类

    1. Java代码
    public class TestDemo {
        public static void main(String[] args) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("ok");
                }
            };
            //Runnable runnable = new TestDemo$1(); //调用由 java 编译器自动生成的 xxx$1 类对象
        }
    }
    
    1. 自动转换的代码
    final class TestDemo$1 implements Runnable {
        TestDemo$1() {
        }
     
        public void run() {
            System.out.println("ok")
        }
    }
    

    10.2 引用局部变量的匿名内部类

    1. Java代码
    // 编译期处理(语法糖) —— 匿名内部类
    public class TestDemo {
        public static void test(final int x) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("ok:" + x);
                }
            };
        }
    }
    
    1. 自动转换的代码
    • java 编译器会自动生成一个叫 xxx1的类,该类实现 Runnable 接口。如有存在引用局部变量,则会将局部变量 x 做为动态生成类 xxx1 类的一个成员变量,通过构造方法传入。
    • 这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 xxx1 对象时,将 x 的值赋值给了 xxx1 对象的 valx 属性,所以 x 不应该再发生变化了,如果变化,那 valx 属性没有机会再跟着一起变化
    final class TestDemo$1 implements Runnable {
        int val$x;
        TestDemo$1(int x) {
            this.val$x = x;
        }
     
        public void run() {
            System.out.println("ok" + this.val$x)
        }
    }
    

    10.3 总结

    • 匿名内部类的底层实现,由Java 编译器自动生成一个叫 xxx1的类,该类实现 Runnable 接口。如果存在引用局部变量,则会将局部变量 x 做为动态生成类 xxx1 类的一个成员变量,通过构造方法传入。
    • 局部变量必须是 final 的:因为在创建 xxx1 对象时,将 x 的值赋值给了 xxx1 对象的 valx 属性,所以 x 不应该再发生变化了,如果变化,那 valx 属性没有机会再跟着一起变化。

    相关文章

      网友评论

          本文标题:04 JVM编译期处理优化

          本文链接:https://www.haomeiwen.com/subject/frbwgltx.html