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

posted @ 2020-02-19 17:10  紫月冰凌  阅读(541)  评论(0编辑  收藏  举报