zoukankan      html  css  js  c++  java
  • 【JVM】-- Java编译期处理

    @


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

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

    1.默认构造器

    public class Candy1 {
    }  
    

    经过编译的代码,可以看到在编译阶段,如果我们没有添加构造器。那么Java编译器会为我们添加一个无参构造方法。

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

    2.自动拆装箱

    在JDK5以后,Java提供了自动拆装箱的功能。

    如以下代码:

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

    在Java5以前会编译失败,必须该写为以下代码:

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

    以上的转换,在JDK5以后都会由Java编译器自动完成。

    3.泛型与类型擦除

    泛型延时在JDK5以后加入的特性,但Java中的泛型并不是真正的泛型。因为Java中的泛型只存在于Java的源码中,在经过编译的字节码文件中,就已经替换为原来的原生类型(RawType,也称为裸类型,可以认为是被Object替换)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说, ArrayList < int>与ArrayList< String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

    如以下代码:

    public class Candy3 {
        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);
        }
    }  
    

    在从list集合中取值时,在编译器真正的字节码文件中还需要一个类型转换的动作

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

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

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

    不过因为语法糖的存在,所以以上的动作都不需要我们自己来做。
    不过,虽然编译器在编译过程中,将泛型信息都擦除了,但是并不意味着,泛型信息就丢失了。泛型的信息还是会存储在LocalVariableTypeTable 中:

    {
      public wf.Candy3();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 6: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lwf/Candy3;
    
    
      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: 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
            19: pop
            20: aload_1
            21: iconst_0
            22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
            27: checkcast     #7                  // class java/lang/Integer
            30: astore_2
            31: return
          LineNumberTable:
            line 8: 0
            line 9: 8
            line 10: 20
            line 11: 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;>;
    }
    SourceFile: "Candy3.java"
    

    我们可以通过反射的方式获得被擦除的泛型信息。不过只能获取方法参数或返回值上的信息。

    public class Candy3 {
    
        List<String> str = new ArrayList<>();
    
        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);
            fs();
        }
        public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
            return null;
        }
    
        private static void fs() throws Exception {
            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
    ", i, arguments[i]);
                    }
                }
            }
    
            Field list = Candy3.class.getDeclaredField("str");
            Class<?> type = list.getType();
            System.out.println(type.getName());
        }
    }
    

    输出:

    原始类型 - interface java.util.List
    泛型参数[0] - class java.lang.String
    原始类型 - interface java.util.Map
    泛型参数[0] - class java.lang.Integer
    泛型参数[1] - class java.lang.Object
    java.util.List
    

    4.可变参数

    可变参数也是JDK5新加入的特性。其具体形式如下:

    public class Test3 {
    
        public static void main(String[] args) {
            foo("hello","world");
        }
    
        private static void foo(String... args){
            String[] str = args;
            for (int i = 0; i < str.length; i++) {
                System.out.println(str[i]);
            }
        }
    }
    

    其结果由一个字符串数组直接接受,程序能够正常执行。
    注意:如果调用方法时没有参数如foo(),那么传入方法的不是null,而是一个空数组foo(new String[]{})。

    5.foreach

    依旧是JDK5引入的语法糖。简化了for循环的写法。

    示例:

    public class Test4_1 {
    
        public static void main(String[] args) {
            int[] arr = {1,2,3,4,5};
            for (int i : arr) {
                System.out.print(" " + i);
            }
        }
    }
    

    在对其字节码反编译后:

    public class Test4_1 {
        public Test4_1() {
        }
    
        public static void main(String[] args) {
            int[] arr = new int[]{1, 2, 3, 4, 5};
            int[] var2 = arr;
            int var3 = arr.length;
    
            for(int var4 = 0; var4 < var3; ++var4) {
                int i = var2[var4];
                System.out.print(" " + i);
            }
        }
    }
    

    此处包含两个语法糖

    • {1,2,3,4,5}转为数组才进行复制
    • foreach循环被转换为了简单的for循环。

    foreach循环还可以对集合进行遍历:

    public class Test4_2 {
    
        public static void main(String[] args) {
            List<Integer> list = Arrays.asList(1,2,3,4,5);
    
            for (Integer integer : list) {
                System.out.print(integer + " ");
            }
        }
    }
    

    其编译后字节码的反编译出的代码为:

    public class Test4_2 {
        public Test4_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 integer = (Integer)var2.next();
                System.out.print(integer + " ");
            }
        }
    }
    

    编译器先获取集合的迭代器对象,在通过while循环对迭代器对象进行遍历。其中还包含泛型擦除的语法糖。
    foreach循环写法,配合数组,及实现了Iterable接口的集合类使用,Iterable来获取迭代器对象(Iterator)

    6.switch支持case使用字符串及枚举类型

    JDK7开始,Java的switch支持字符串和枚举类型,而其中也包含了语法糖。

    switch字符串

    示例:

    public class Test5_1 {
    
        public static void main(String[] args) {
            switch ("hello"){
                case "hello":
                    System.out.println("hello");
                    break;
                case "world":
                    System.out.println("world");
            }
        }
    }
    

    注意:在使用String时,不能传入一个null,会发生空指针异常。因为以上代码会被编译器转换为:

    public class Test5_1 {
        public Test5_1() {
        }
    
        public static void main(String[] args) {
            String var1 = "hello";
            byte var2 = -1;
            switch(var1.hashCode()) {
            case 99162322:
                if (var1.equals("hello")) {
                    var2 = 0;
                }
                break;
            case 113318802:
                if (var1.equals("world")) {
                    var2 = 1;
                }
            }
    
            switch(var2) {
            case 0:
                System.out.println("hello");
                break;
            case 1:
                System.out.println("world");
            }
    
        }
    }
    

    可以看到,switch支持字符实际上是把对象,获取其哈希值进行一次比较在确定了,之后再用一个switch来实现代码逻辑。
    为什么第一次既要进行一次哈希比较,又要进行一次equals()?使用hashcode是为了提高比较的效率,而equals是为了防止哈希冲突。如BM和C.两个字符串的哈希值相同都为2123,如果有以下代码:

    public class Test5_2 {
    
        public static void main(String[] args) {
            switch ("BM"){
                case "BM":
                    System.out.println("hello");
                    break;
                case "C.":
                    System.out.println("world");
            }
        }
    }
    

    经过反编译后:

    public class Test5_2 {
        public Test5_2() {
        }
    
        public static void main(String[] args) {
            String var1 = "BM";
            byte var2 = -1;
            switch(var1.hashCode()) {
            case 2123://哈希值相同需要进一步比较
                if (var1.equals("C.")) {
                    var2 = 1;
                } else if (var1.equals("BM")) {
                    var2 = 0;
                }
            default:
                switch(var2) {
                case 0:
                    System.out.println("hello");
                    break;
                case 1:
                    System.out.println("world");
                }
    
            }
        }
    }
    

    switch枚举

    代码如下:

    public class Test5_3 {
    
        public static void main(String[] args) {
            Sex sex = Sex.MALE;
    
            switch (sex){
                case MALE:
                    System.out.println("男");
                    break;
                case FEMALE:
                    System.out.println("女");
            }
        }
    }
    
    enum Sex{
        MALE,FEMALE;
    }
    

    转换后:

    /**
    * 定义一个合成类(仅 jvm 使用,对我们不可见)
    * 用来映射枚举的 ordinal 与数组元素的关系
    * 枚举的 ordinal 表示枚举对象的序号,从 0 开始
    * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
    * 该转换需要使用其他工具进行转换,idea转换不出来
    */
    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;
            }
        }
    }  
    

    7.枚举

    JDK7以后Java引入了枚举类,它也是一个语法糖。

    以上一个性别类型为例:

    enum Sex {
        MALE, FEMALE
    }
    

    转换后(idea依旧不能转换):

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

    8.try-with-resourcs

    JDK7加入对需要关闭资源处理的特殊语法。

    try(资源大小 = 创建对象资源){
      
    }catch(){
      
    }
    

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

    public class Test6 {
    
        public static void main(String[] args) {
            try(InputStream stream = new FileInputStream("F://test.txt")) {
                System.out.println(stream);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    

    会被转换为;

    public class Test6 {
        public Test6() {
        }
    
        public static void main(String[] args) {
            try {
                InputStream stream = new FileInputStream("F://test.txt");
                Throwable var2 = null;
    
                try {
                    System.out.println(stream);
                } catch (Throwable var12) {
                    //var2是可能出现的异常
                    var2 = var12;
                    throw var12;
                } finally {
                    //判断资源是否为空
                    if (stream != null) {
                        //如果代码出现异常
                        if (var2 != null) {
                            try {
                                stream.close();
                            } catch (Throwable var11) {
                                //关闭资源时出现异常,作为被压制异常添加
                                var2.addSuppressed(var11);
                            }
                        } else {
                            //如果代码没有异常,close出现的异常就是catch中var12
                            stream.close();
                        }
                    }
    
                }
            } catch (Exception var14) {
                var14.printStackTrace();
            }
    
        }
    }
    

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

    public class Test6_1 {
    
        public static void main(String[] args) {
            try(Myresource myresource = new Myresource()) {
                int a = 1/0;
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    
    class Myresource implements AutoCloseable{
    
        @Override
        public void close() throws Exception {
            throw new IOException("close异常");
        }
    }
    

    其输出为;

    java.lang.ArithmeticException: / by zero
    	at wf.test.Test6_1.main(Test6_1.java:9)
    	Suppressed: java.io.IOException: close异常
    		at wf.test.Myresource.close(Test6_1.java:20)
    		at wf.test.Test6_1.main(Test6_1.java:10)
    

    TWR将两个异常信息都保留了下来。

    9.方法重写时的桥接方法

    我们都知道,方法重写时对返回值分两种情况:

    • 父子类的返回值完全一致
    • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
    class A{
        public Number m(){
            return 1;
        }
    }
    
    class B extends A{
        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 Test7 {
    
        public static void main(String[] args) {
            for (Method m :B.class.getDeclaredMethods()) {
                System.out.println(m);
            }
    
            A a = new B();
            System.out.println(a.m());
        }
    }
    

    输出结果:

    public java.lang.Integer wf.test.B.m()
    public java.lang.Number wf.test.B.m()
    2
    

    也可以验证该方法重写起作用了。

    10.匿名内部类

    代码:

    public class Candy11 {
    
        public static void main(String[] args) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello ");
                }
            };
        }
    

    }

    转码后:

    // 额外生成的类
    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();
        }
    }
    

    当匿名内部类引用外部类变量时

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

    转换后:

    // 额外生成的类
    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 static void test(final int x) {
        Runnable runnable = new Candy11$1(x);
    }
    

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

  • 相关阅读:
    浙大数据结构课后习题 练习二 7-2 Reversing Linked List (25 分)
    浙大数据结构课后习题 练习二 7-2 一元多项式的乘法与加法运算 (20 分)
    浙大数据结构课后习题 练习一 7-1 Maximum Subsequence Sum (25 分)
    浙大数据结构课后习题 练习一 7-1 最大子列和问题 (20 分)
    PAT Basic 1019 数字黑洞 (20 分)
    PAT Basic 1017 A除以B (20 分)
    PAT Basic 1013 数素数 (20 分)
    PAT Basic 1007 素数对猜想 (20 分)
    PAT Basic 1003 我要通过! (20 分)
    自动化运维——HelloWorld(一)
  • 原文地址:https://www.cnblogs.com/wf614/p/12332056.html
Copyright © 2011-2022 走看看