【JVM】JVM06(编译器处理-java语法糖总结)

⭐️写在前面


  • 这里是温文艾尔の学习之路
  • 如果对你有帮助,给博主一个免费的点赞以示鼓励把QAQ
  • 博客主页 温文艾尔の学习小屋
  • ⭐️更多文章‍请关注温文艾尔主页
  • 文章发布日期:2022.02.08
  • java学习之路!
  • 欢迎各位点赞评论收藏⭐️
  • 新年快乐朋友们
  • jvm学习之路!
  • ⭐️上一篇内容:【JVM】JVM05(从字节码角度分析i++和++i的执行流程))

文章目录

  • ⭐️1编译期处理(语法糖)
  • ⭐️1.1默认构造器
  • ⭐️1.2自动拆装箱
  • ⭐️1.3泛型集合取值
  • ⭐️1.4可变参数
  • ⭐️1.5 foreach循环
  • ⭐️1.6switch字符串
  • ⭐️1.7switch枚举
  • ⭐️1.8枚举类
  • ⭐️1.9 try-with-resources
  • ⭐️1.10方法重写时的桥接方法
  • ⭐️1.11匿名内部类


⭐️1编译期处理(语法糖)

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

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

⭐️1.1默认构造器

public class Candy1{
}

编译成class后的代码:

public class Candy1{
    //这个无参构造器是编译器帮助我们加上的
    public Candy1(){
        super();//即调用父类Object的无参构造方法
    }
}

⭐️1.2自动拆装箱

这个特性是JDK5开始加入的,代码片段1:

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

上面代码在JDK5之前是无法编译通过的,必须改写为代码片段2:

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

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

⭐️1.3泛型集合取值

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

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

所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:

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

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

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

这些事情现在都不用我们自己做

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

Compiled from "Demo02.java"
public class com.wql.jvm.Candy.Demo02
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // com/wql/jvm/Candy/Demo02
  super_class: #9                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #9.#29         // java/lang/Object."":()V
   #2 = Class              #30            // java/util/ArrayList
   #3 = Methodref          #2.#29         // java/util/ArrayList."":()V
   #4 = Methodref          #7.#31         // java/lang/Integer.valueOf:(I)Ljava/lang/Int
eger;
   #5 = InterfaceMethodref #32.#33        // java/util/List.add:(Ljava/lang/Object;)Z
   #6 = InterfaceMethodref #32.#34        // java/util/List.get:(I)Ljava/lang/Object;
   #7 = Class              #35            // java/lang/Integer
   #8 = Class              #36            // com/wql/jvm/Candy/Demo02
   #9 = Class              #37            // java/lang/Object
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/wql/jvm/Candy/Demo02;
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               args
  #20 = Utf8               [Ljava/lang/String;
  #21 = Utf8               list
  #22 = Utf8               Ljava/util/List;
  #23 = Utf8               x
  #24 = Utf8               Ljava/lang/Integer;
  #25 = Utf8               LocalVariableTypeTable
  #26 = Utf8               Ljava/util/List<Ljava/lang/Integer;>;
  #27 = Utf8               SourceFile
  #28 = Utf8               Demo02.java
  #29 = NameAndType        #10:#11        // "":()V
  #30 = Utf8               java/util/ArrayList
  #31 = NameAndType        #38:#39        // valueOf:(I)Ljava/lang/Integer;
  #32 = Class              #40            // java/util/List
  #33 = NameAndType        #41:#42        // add:(Ljava/lang/Object;)Z
  #34 = NameAndType        #43:#44        // get:(I)Ljava/lang/Object;
  #35 = Utf8               java/lang/Integer
  #36 = Utf8               com/wql/jvm/Candy/Demo02
  #37 = Utf8               java/lang/Object
  #38 = Utf8               valueOf
  #39 = Utf8               (I)Ljava/lang/Integer;
  #40 = Utf8               java/util/List
  #41 = Utf8               add
  #42 = Utf8               (Ljava/lang/Object;)Z
  #43 = Utf8               get
  #44 = Utf8               (I)Ljava/lang/Object;
{
  public com.wql.jvm.Candy.Demo02();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/wql/jvm/Candy/Demo02;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) 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."":()V

         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Lja
va/lang/Integer;
        14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Lja
va/lang/Object;)Z
        19: pop
        20: aload_1
        21: iconst_0
        22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)L
java/lang/Object;
        27: checkcast     #7                  // class java/lang/Integer
        30: astore_2
        31: return
      LineNumberTable:
        line 14: 0
        line 15: 8
        line 16: 20
        line 17: 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: "Demo02.java"

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

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
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]);
        }
    }
}

输出
【JVM】JVM06(编译器处理-java语法糖总结)_第1张图片

⭐️1.4可变参数

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

例如:

public class Demo03 {
    public static void foo(String... args){
        String[] array = args;
        System.out.println(array);
    }
    public static void main(String[] args) {
        foo("hello","java");
    }
}

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

public class Demo03 {
    public static void foo(String[] args){
        String[] array = args;
        System.out.println(array);
    }
    public static void main(String[] args) {
        foo(new String[]{"hello","java"});
    }
}

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

⭐️1.5 foreach循环

JDK5开始引入的语法糖,数组的循环:

public class Demo04 {
    public static void main(String[] args) {
        int[] array = {1,2,3,4,5};//数组赋初值的简化写法也是语法糖
        for (int e:array){
            System.out.println(e);
        }
    }
}

会被编译器转换为:

public class Demo04 {
    public static void main(String[] args) {
        int[] array = new int[]{1,2,3,4,5};//数组赋初值的简化写法也是语法糖
        for (int i = 0; i < array.length; ++i) {
            int e =array[i];
            System.out.println(e);
        }
    }
}

而集合的循环:

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

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

⭐️1.6switch字符串

从JDK7开始,switch可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

public class Demo06 {
    public static void choose(String str){
        switch (str){
            case "hello":{
                System.out.println("h");
                break;
            }
            case "world":{
                System.out.println("w");
                break;
            }
        }
    }
}

注意:

switch配合String和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚会被编译器转换为:

public class Demo06 {
    public static void choose(String str){
        byte x = -1;
        switch (str.hashCode()){
            case 99162322://hello的hashCode
                if (str.equals("hello")){
                    x=0;
                }
                break;
            case 113318802://world的hashCode
                if (str.equals("world")){
                    x=1;
                }
        }
        switch (x){
            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 Demo07 {
    public static void choose(String str) {
        byte x = -1;
        switch (str.hashCode()){
            case 2123: //hashCode值相等,需要进一步用equals比较
                if (str.equals("C")){
                    x=0;
                }else if (str.equals("BM")){
                    x=0;
                }
            default:
                switch (x){
                    case 0:
                        System.out.println("h");
                        break;
                    case 1:
                        System.out.println("w");
                }
        }
    }
}

⭐️1.7switch枚举

switch枚举的例子,原始代码:

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

转换后代码:

public class Demo09 {
    /**
     * 定义一个合成类(仅jvm使用,对我们不可见)
     * 用来映射枚举的ordinal与数组元素的关系
     * 枚举的ordinal表示枚举对象的序号,从0开始
     * 即MALE的ordinal()=0,FEMALE的ordinal()=1
     */
    static class $MAP{
        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;
        }
    }
}

⭐️1.8枚举类

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

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",0);
        $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);
    }
}

⭐️1.9 try-with-resources

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

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

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

public class Demo11 {
    public static void main(String[] args) {
        try(InputStream is = new FileInputStream("d:\\1.txt")) {
            System.out.println(is);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

会被转换为:

   public static void main(String[] args) {
        try {
            InputStream is = new FileInputStream("d:\\1.txt");
            Throwable t = null;
            try {
                System.out.println(is);
            } catch (Throwable e1) {
                // t 是我们代码出现的异常
                t = e1;
                throw e1;
        	} finally {
                // 判断了资源不为空
                if (is != null) {
                    // 如果我们代码有异常
                    if (t != null) {
                        try {
                            is.close();
                        } catch (Throwable e2) {
                            // 如果 close 出现异常,作为被压制异常添加
                            t.addSuppressed(e2);
                        }
                    } else {
                        // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
                        is.close();
                    }
                }
            }
        } catch (IOException e) {
        	e.printStackTrace();
        }
    }
}

希望在关闭异常时也有可能发生异常,try中的异常和资源关闭中的异常都想保留就需要t.addSuppressed(e2);

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

例如

public class Demo12 {
    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 异常");
    }
}

外层异常为int i = 1/0;,内层异常为资源关闭时的异常
【JVM】JVM06(编译器处理-java语法糖总结)_第2张图片
可以看到两个异常都没有丢

⭐️1.10方法重写时的桥接方法

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

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
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();
    }
}

synthetic bridge的含义是合成方法,对程序员是不可见的

桥接方法比较特殊,仅对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()

⭐️1.11匿名内部类

源代码:

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

转换后代码:

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

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

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

转换后代码

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

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

你可能感兴趣的:(jvm,java,开发语言,后端,jvm,java字节码)