美文网首页@IT·互联网
Java编译器对字节码的优化

Java编译器对字节码的优化

作者: 我可能是个假开发 | 来源:发表于2024-01-06 14:15 被阅读0次

    一、编译期处理

    语法糖 ,是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利

    编译器转换的结果直接就是 class 字节码。
    为了便于阅读,使用几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码。

    二、默认构造器

    public class Candy1 {
    }
    
    public class Candy {
        // 这个无参构造是编译器帮助我们加上的
        public Candy() {
          // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
          super();
        }
    }
    

    三、自动拆装箱

    这个特性是 JDK 5 开始加入的 :

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

    这段代码在 JDK 5 之前是无法编译通过的,必须改写为

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

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

    四、泛型集合取值

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

    public class Candy3 {
        public static void main(String[] args) throws Exception {
            List<Integer> 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();
    

    擦除的是字节码上的泛型信息,LocalVariableTypeTable 仍然保留了方法参数泛型的信息

    LocalVariableTypeTable:
    Start Length Slot Name Signature
    8        24    1  list   Ljava/util/List<Ljava/lang/Integer;>;
    

    使用反射,仍然能够获得这些信息:

    public class Candy3 {
        public static void main(String[] args) throws Exception {
            List<Integer> list = new ArrayList<>();
            list.add(10); // 实际调用的是 List.add(Object e)
            Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
    
            Method test = Candy3.class.getMethod("test", List.class, Map.class);
            Type[] types = test.getGenericParameterTypes();
            for (Type type : types) {
                if (type instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType) type;
                    System.out.println("原始类型 - " + parameterizedType.getRawType());
                    Type[] arguments = parameterizedType.getActualTypeArguments();
                    for (int i = 0; i < arguments.length; i++) {
                        System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
                    }
                }
    
            }
    
        }
    
        public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
            return null;
        }
    }
    
    原始类型 - interface java.util.List
    泛型参数[0] - class java.lang.String
    原始类型 - interface java.util.Map
    泛型参数[0] - class java.lang.Integer
    泛型参数[1] - class java.lang.Object
    

    五、可变参数

    可变参数也是 JDK 5 开始加入的新特性:

    public class Candy4 {
        public static void foo(String... args) {
            String[] array = args; // 直接赋值
            System.out.println(array);
        }
        public static void main(String[] args) {
            foo("hello", "world");
        }
    }
    

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

    public class Candy4 {
        public static void foo(String[] args) {
            String[] array = args; // 直接赋值
            System.out.println(array);
        }
        public static void main(String[] args) {
            foo("hello", "world");
        }
    }
    

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

    六、 foreach 循环

    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};
            int[] var2 = array;
            int var3 = array.length;
    
            for(int var4 = 0; var4 < var3; ++var4) {
                int e = var2[var4];
                System.out.println(e);
            }
    
        }
    }
    

    集合的循环:

    public class Candy5_2 {
        public static void main(String[] args) {
            List<Integer> 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<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
            Iterator var2 = list.iterator();
    
            while(var2.hasNext()) {
                Integer i = (Integer)var2.next();
                System.out.println(i);
            }
    
        }
    }
    

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

    七、switch 字符串

    从 JDK 7 开始,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;
                }
            }
        }
    }
    

    会被编译器转换为:

    public class Candy {
        public Candy() {
        }
    
        public static void choose(String str) {
            byte var2 = -1;
            switch(str.hashCode()) {
            case 99162322: // hello 的 hashCode
                if (str.equals("hello")) {
                    var2 = 0;
                }
                break;
            case 113318802: // world 的 hashCode
                if (str.equals("world")) {
                    var2 = 1;
                }
            }
    
            switch(var2) {
            case 0:
                System.out.println("h");
                break;
            case 1:
                System.out.println("w");
            }
    
        }
    

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

    第一遍时必须既比较 hashCode,又利用 equals 比较:
    hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BM 和 C. 这两个字符串的hashCode值都是2123 ,如果有如下代码:

    public class Candy {
        public static void choose(String str) {
            switch (str) {
                case "BM": {
                    System.out.println("h");
                    break;
                }
                case "C.": {
                    System.out.println("w");
                    break;
                }
            }
        }
    

    编译后:

    public class Candy {
        public Candy() {
        }
    
        public static void choose(String str) {
            byte var2 = -1;
            switch(str.hashCode()) {
            case 2123:  // hashCode 值可能相同,需要进一步用 equals 比较
                if (str.equals("C.")) {
                    var2 = 1;
                } else if (str.equals("BM")) {
                    var2 = 0;
                }
            default:
                switch(var2) {
                case 0:
                    System.out.println("h");
                    break;
                case 1:
                    System.out.println("w");
                }
    
            }
        }
    

    switch 配合 String 和枚举使用时,变量不能为null

    八、switch 枚举

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

    转换后代码:

    public class Candy {
        /**
         * 定义一个合成类(仅 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;
            }
        }
    }
    

    九、枚举类

    JDK 7 新增了枚举类,以前面的性别枚举为例:

    enum Sex {
        MALE, FEMALE
    }
    

    转换后代码:

    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 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);
        }
    }
    

    十、try-with-resources

    JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources:

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

    其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources 可以不用写 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 Candy9 {
        public Candy9() {
        }
    
        public static void main(String[] args) {
            try {
                InputStream is = new FileInputStream("d:\\1.txt");
                Throwable var2 = null;
    
                try {
                    System.out.println(is);
                } catch (Throwable var12) {
                  //我们代码出现的异常
                    var2 = var12;
                    throw var12;
                } finally {
                    // 判断了资源不为空
                    if (is != null) {
                        // 如果我们代码有异常
                        if (var2 != null) {
                            try {
                                is.close();
                            } catch (Throwable var11) {
                                // 如果 close 出现异常,作为被压制异常添加
                                var2.addSuppressed(var11);
                            }
                        } else {
                            // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
                            is.close();
                        }
                    }
    
                }
            } catch (IOException var14) {
                var14.printStackTrace();
            }
    
        }
    }
    
    

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

    public class Test {
        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 jvm.candy.Test.main(Test.java:6)
        Suppressed: java.lang.Exception: close 异常
            at jvm.candy.MyResource.close(Test.java:15)
            at jvm.candy.Test.main(Test.java:7)
    

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

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

    方法重写时对返回值分两种情况:

    • 父子类的返回值完全一致
    • 子类返回值可以是父类返回值的子类
    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 Candy {
        public static void main(String[] args) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("ok");
                }
            };
        }
    }
    

    转换后:

    // 额外生成的类
    final class Candy$1 implements Runnable {
        Candy$1() {
        }
        public void run() {
            System.out.println("ok");
        }
    }
    public class Candy {
        public static void main(String[] args) {
            Runnable runnable = new Candy$1();
        }
    }
    

    引用局部变量的匿名内部类,源代码:

    public class Candy {
        public static void test(final int x) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("ok:" + x);
                }
            };
        }
    }
    

    转换后

    // 额外生成的类
    final class Candy$1 implements Runnable {
        int val$x;
        Candy$1(int x) {
            this.val$x = x;
        }
        public void run() {
            System.out.println("ok:" + this.val$x);
        }
    }
    public class Candy {
        public static void test(final int x) {
            Runnable runnable = new Candy$(x);
        }
    }
    

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

    相关文章

      网友评论

        本文标题:Java编译器对字节码的优化

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