• JVM总结-Java语法糖与Java编译器


    自动装箱与自动拆箱

    首先要提到的便是 Java 的自动装箱(auto-boxing)和自动拆箱(auto-unboxing)。

    我们知道,Java 语言拥有 8 个基本类型,每个基本类型都有对应的包装(wrapper)类型。

    之所以需要包装类型,是因为许多 Java 核心类库的 API 都是面向对象的。举个例子,Java 核心类库中的容器类,就只支持引用类型。

    当需要一个能够存储数值的容器类时,我们往往定义一个存储包装类对象的容器。

    对于基本类型的数值来说,我们需要先将其转换为对应的包装类,再存入容器之中。在 Java 程序中,这个转换可以是显式,也可以是隐式的,后者正是 Java 中的自动装箱。

    public int foo() {
      ArrayList<Integer> list = new ArrayList<>();
      list.add(0);
      int result = list.get(0);
      return result;
    }

    以上图中的 Java 代码为例。我构造了一个 Integer 类型的 ArrayList,并且向其中添加一个 int 值 0。然后,我会获取该 ArrayList 的第 0 个元素,并作为 int 值返回给调用者。这段代码对应的 Java 字节码如下所示:

    public int foo();
      Code:
         0: new java/util/ArrayList
         3: dup
         4: invokespecial java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_0
        10: invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        13: invokevirtual java/util/ArrayList.add:(Ljava/lang/Object;)Z
        16: pop
        17: aload_1
        18: iconst_0
        19: invokevirtual java/util/ArrayList.get:(I)Ljava/lang/Object;
        22: checkcast java/lang/Integer
        25: invokevirtual java/lang/Integer.intValue:()I
        28: istore_2
        29: iload_2
        30: ireturn

    当向泛型参数为 Integer 的 ArrayList 添加 int 值时,便需要用到自动装箱了。在上面字节码偏移量为 10 的指令中,我们调用了 Integer.valueOf 方法,将 int 类型的值转换为 Integer 类型,再存储至容器类中。

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    这是 Integer.valueOf 的源代码。可以看到,当请求的 int 值在某个范围内时,我们会返回缓存了的 Integer 对象;而当所请求的 int 值在范围之外时,我们则会新建一个 Integer 对象。

    在介绍反射的那一篇中,我曾经提到参数 java.lang.Integer.IntegerCache.high。这个参数将影响这里面的 IntegerCache.high。

    也就是说,我们可以通过配置该参数,扩大 Integer 缓存的范围。Java 虚拟机参数 -XX:+AggressiveOpts 也会将 IntegerCache.high 调整至 20000。

    奇怪的是,Java 并不支持对 IntegerCache.low 的更改,也就是说,对于小于 -128 的整数,我们无法直接使用由 Java 核心类库所缓存的 Integer 对象。

    当从泛型参数为 Integer 的 ArrayList 取出元素时,我们得到的实际上也是 Integer 对象。如果应用程序期待的是一个 int 值,那么就会发生自动拆箱。

    在我们的例子中,自动拆箱对应的是字节码偏移量为 25 的指令。该指令将调用 Integer.intValue 方法。这是一个实例方法,直接返回 Integer 对象所存储的 int 值。

    泛型与类型擦除

    你可能已经留意到了,在前面例子生成的字节码中,往 ArrayList 中添加元素的 add 方法,所接受的参数类型是 Object;而从 ArrayList 中获取元素的 get 方法,其返回类型同样也是 Object。

    前者还好,但是对于后者,在字节码中我们需要进行向下转换,将所返回的 Object 强制转换为 Integer,方能进行接下来的自动拆箱。

    之所以会出现这种情况,是因为 Java 泛型的类型擦除。这是个什么概念呢?简单地说,那便是 Java 程序里的泛型信息,在 Java 虚拟机里全部都丢失了。这么做主要是为了兼容引入泛型之前的代码。

    当然,并不是每一个泛型参数被擦除类型后都会变成 Object 类。对于限定了继承类的泛型参数,经过类型擦除后,所有的泛型参数都将变成所限定的继承类。也就是说,Java 编译器将选取该泛型所能指代的所有类中层次最高的那个,作为替换泛型的类。

    class GenericTest<T extends Number> {
      T foo(T t) {
        return t;
      }
    }

    举个例子,在上面这段 Java 代码中,我定义了一个 T extends Number 的泛型参数。它所对应的字节码如下所示。可以看到,foo 方法的方法描述符所接收参数的类型以及返回类型都为 Number。方法描述符是 Java 虚拟机识别方法调用的目标方法的关键。

    T foo(T);
      descriptor: (Ljava/lang/Number;)Ljava/lang/Number;
      flags: (0x0000)
      Code:
        stack=1, locals=2, args_size=2
           0: aload_1
           1: areturn
      Signature: (TT;)TT;

    既然泛型会被类型擦除,那么我们还有必要用它吗?

    我认为是有必要的。Java 编译器可以根据泛型参数判断程序中的语法是否正确。举例来说,尽管经过类型擦除后,ArrayList.add 方法所接收的参数是 Object 类型,但是往泛型参数为 Integer 类型的 ArrayList 中添加字符串对象,Java 编译器是会报错的。

    桥接方法

    泛型的类型擦除带来了不少问题。其中一个便是方法重写。

    总结与实践

    今天我主要介绍了 Java 编译器对几个语法糖的处理。

    基本类型和其包装类型之间的自动转换,也就是自动装箱、自动拆箱,是通过加入 [Wrapper].valueOf(如 Integer.valueOf)以及 [Wrapper].[primitive]Value(如 Integer.intValue)方法调用来实现的。

    Java 程序中的泛型信息会被擦除。具体来说,Java 编译器将选取该泛型所能指代的所有类中层次最高的那个,作为替换泛型的具体类。

    由于 Java 语义与 Java 字节码中关于重写的定义并不一致,因此 Java 编译器会生成桥接方法作为适配器。此外,我还介绍了 foreach 循环以及字符串 switch 的编译。

  • 相关阅读:
    js获取元素位置和style的兼容性写法
    javascript正则表达式---正向预查
    Typescript学习笔记(五) 模块机制
    Typescript学习笔记(四)class 类
    Typescript学习笔记(三)变量声明及作用域
    Typescript学习笔记(二)枚举
    Typescript学习笔记(一)基础类型
    tar命令
    linux的nohup命令的用法。
    vue.js移动端app实战1
  • 原文地址:https://www.cnblogs.com/yintingting/p/8892734.html
Copyright © 2020-2023  润新知