JVM -- 编译器处理;语法糖

Posted MinggeQingchun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM -- 编译器处理;语法糖相关的知识,希望对你有一定的参考价值。

语法糖(Syntactic sugar),其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成 和转换的一些代码

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。——百度百科

一、默认构造器

//java源码
public class Candy 


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

二、自动拆装箱

JDK1.5之后,新增了自动拆、装箱功能

简化基本类型和对象转换的写法

装箱:基本类型的值被封装为一个包装类对象

拆箱:一个包装类对象被拆开并获取相应的值

这是编译器的工作,在class中已经添加,虚拟机没有自动装箱和拆箱的语句

==:基本类型是内容相同,对象时指针是否相同(内存同一个区域)

基本类型没有空值,对象有null

当一个基础数据类型与封装类进行==、+、-、*、/运算时,会将封装类进行拆箱,然后再运算

如以前创建一个Integer对象,需要 使用 “new”关键字 

而现在Java中可以直接赋值如下:

Integer不是new出Integer对象,而是直接赋值,就是自动装箱过程

Integer a = new Integer("100");
 
//JDK1.5之后
Integer b = 100;
int x = b;

三、泛型集合取值

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

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 仍然保留了方法参数泛型的信息

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;>;

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

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]);
        
    

 输出:

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

四、可变参数

可变参数也是 JDK 5 开始加入的新特性

public class Candy4 
    public static void foo(String... args) 
        String[] array = args; // 直接赋值
        System.out.println(array);
    

    public static void main(String[] args) 
        foo("hello", "world");
    

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

public class Candy4 
    public static void foo(String[] args) 
        String[] array = args; // 直接赋值
        System.out.println(array);
    

    public static void main(String[] args) 
        foo(new String[]"hello", "world");
    

注:

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

五、foreach 循环

foreach是 JDK 5 开始引入的语法糖,数组的循环

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


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

注:

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

六、switch 字符串

JDK 7 开始,switch 可以作用于字符串和枚举类

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 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 static void choose(String str) 
    switch (str) 
        case "BM": 
            System.out.println("h");
            break;
        
        case "C.": 
            System.out.println("w");
            break;
        
    

 会被编译器转换为

public static void choose(String str) 
    byte x = -1;
    switch(str.hashCode()) 
        case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
            if (str.equals("C.")) 
                x = 1;
             else if (str.equals("BM")) 
                x = 0;
            
        default:
            switch(x) 
                case 0:
                    System.out.println("h");
                    break;
                case 1:
                    System.out.println("w");
            
    

七、switch 枚举

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

 转换后代码

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

八、枚举类

JDK 7 新增了枚举类

转换后代码

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.
*
* @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);
    

九、try-with-resources

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

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

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

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();
        
    

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

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 异常");
    

输出:

java.lang.ArithmeticException: / by zero
at test.Test6.main(Test6.java:7)
Suppressed: java.lang.Exception: close 异常
at test.MyResource.close(Test6.java:18)
at test.Test6.main(Test6.java:6)

 如以上代码所示,两个异常信息都不会丢

十、方法重写时的桥接方法

方法重写时对返回值分两种情况

1、父子类的返回值完全一致

2、子类返回值可以是父类返回值的子类

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();
    

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

十一、匿名内部类

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 class Candy11 
    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 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变 化了,如果变化,那么 val$x 属性没有机会再跟着一起变化

以上是关于JVM -- 编译器处理;语法糖的主要内容,如果未能解决你的问题,请参考以下文章

JVM:Java中的语法糖

语法糖

浅析java中的语法糖

什么是语法糖?

Java 中的语法糖

Python语法糖