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属性没有机会再跟着一起变化

  • 相关阅读:
    HRMSYS项目源码分析(二)
    HRMSYS项目源码分析(一)
    SQL类型转换以及自动在前面补0满足10位工号标示法
    android—资源文件(res)的引用
    SQL serve创建与调用存储过程
    .wsdl文件生成.cs文件
    android 文件操作类简易总结
    android EncodingUtils
    FTP创建与操作
    如何调试框架中的app
  • 原文地址:https://www.cnblogs.com/wf614/p/12332056.html
Copyright © 2011-2022 走看看