【一起学习JVM】Java中的语法糖是什么味道

Java提供了多种 不同口味的语法糖,每种口味的糖都有不同的食用方法和魔力,你知道哪几种口味的糖呢? 让我们一起来尝一尝吧

泛型和类型擦除

  • Java中的泛型只存在于编译阶段,因为只有在编译的时候会对泛型的类型进行校验,在运行期,其实是没有泛型的概念,在经过javac编译之后,就已经将泛型擦除了。

  • 示例1——判断字节码对象

    public class Test {
    
        // 
        public static void main(String[] args) {
    
            ArrayList<String> list1 = new ArrayList<String>();
            list1.add("abc");
    
            ArrayList<Integer> list2 = new ArrayList<Integer>();
            list2.add(123);
    
            // true  不同的泛型,但是class对象是相同的
            System.out.println(list1.getClass() == list2.getClass());
        }
    
    }
    
  • 示例2——反射获取运行期对象

    public static void main(String[] args) throws Exception {
        List<String> list = new ArrayList<>();
        list.add("一");
    
        Method method = list.getClass().getMethod("add", Object.class);
    
        method.invoke(list, 2);
    
        System.out.println(list); // [一, 2]
    }
    

    通过上面的示例,可以看到在运行期可以直接将不同的类型设置到集合中,虽然该集合的泛型指定的是其他的类型,泛型只会在编译期对类型进行限制

自动装箱、拆箱

  • 在使用Integer、int等包装类和基础类型进行判断的时候,我们无需进行引用类型和基础类型的转换就可以直接比较,众所周知,引用类型如果直接判断,那么比较的是内存地址,那么是怎么判断的呢?

  • 原理

    // Integer 和 int 直接进行比较
    public static void main(String[] args) throws Exception {
        Integer a = new Integer(1);
        int b = 1;
        System.out.println(a == b); // true
    }
    
    // Integer的自动拆箱原理
    public static void main(String[] args) throws Exception {
        Integer a = new Integer(1);
        int b = 1;
    
        // 拆箱
        int i = a.intValue();
    }
    
    // Integer的自动装箱
    public static void main(String[] args) throws Exception {
        Integer c = 1;
        // c 经过装箱为Integer对象
        Integer d = Integer.valueOf(1);
    }
    

    实际上自动装箱和拆箱是通过调用IntegerintValue()valueOf()方法完成,在包装类需要转换为基础类型 或 基础类型需要转换为包装类的时候,JVM会自动调用方法帮助我们完成转换。

遍历循环

  • forEach增强for循环是我们在遍历数组或者集合中最常用的方式了,那么forEach是如何做到支持简洁的代码实现循环的呢?其实,forEach也是一种语法糖~

  • 示例:

    • 原始遍历方式:
    public static void main(String[] args) throws Exception {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
    
        for (String s : list) {
            System.out.println(s);
        }
    }
    
    • 编译之后的代码:
    public static void main(String[] args) throws Exception {
        List<String> list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        
        Iterator var2 = list.iterator();
        while(var2.hasNext()) {
            String s = (String)var2.next();
            System.out.println(s);
        }
    
    }
    

    通过反编译的代码可以看到,其实forEach就是使用迭代器来进行循环的,只是迭代器繁琐的操作交给JVM去做了,我们只需要使用简洁的forEach就可以完成循环操作

条件编译

  • JVM会对if语句的条件进行判断,将不会成立的分支的代码消除掉,减少代码指令

  • 示例:

    • 源代码:
    public static void main(String[] args) throws Exception {
        if(true) {
            System.out.println("true");
        } else {
            System.out.println(false);
        }
    }
    
    • 反编译代码:
    public static void main(String[] args) throws Exception {
        System.out.println("true");
    }
    

    反编译后,消除了不可能成立的分支代码,将5行代码变为了1行代码,这也是JVM对代码的优化方式,条件编译只可能出现在判断条件为常量的形式中

swtich支持字符串

  • jdk1.7之前,switch支持的类型有:byteshortcharint这几个基本数据类型和对应的包装类。其实switch后面只能写int,但是byteshortchar可以转换为int(向大的数据类型提升),对于大数据类型longdoublefloat等必须强制转换为int才可以。但是string是不可以的会报错,那么jdk1.8之后,string是怎么支持的呢?

  • 原理:

    jdk1.8并没有新增指令来处理string类型,而是通过string#hashcode计算出每个字符串的hash值,对switch中的case进行匹配

    看到这里,你可能会有疑问了,如果发生hash冲突怎么办? JVM开发团队当然也想到了,和HashMap的解决方案相同,只需要通过hashcodeequals方法就可以保证在hash冲突的情况下判断是否相等

  • 示例:

    • 源代码:
    public static void main(String[] args) {
        String str = "a";
    
        switch (str) {
            case "a":
                System.out.println("a");
                break;
            case "b":
                System.out.println("b");
                break;
        }
    }
    
    • 反编译代码:
    public static void main(String[] args) {
        String str = "a";
        byte var3 = -1;
        // 先判断hashcode的值,然后判断equals是否相等,如果相等,则赋值int变量
        switch(str.hashCode()) {
            case 97:  // ‘a’的hashcode计算出来为97
                if (str.equals("a")) {
                    var3 = 0;
                }
                break;
            case 98:
                if (str.equals("b")) {
                    var3 = 1;
                }
        }
    
        // 还是需要通过int的switch进行判断
        switch(var3) {
            case 0:
                System.out.println("a");
                break;
            case 1:
                System.out.println("b");
        }
    
    }
    

try-with-resource

  • 先看看try-catch-finally在操作流时的代码

  • 示例:读取文件并正确关闭流的操作

    public static void main(String[] args) {
        InputStream in = null;
        try {
            in = new FileInputStream("D://file.txt");
            int read = in.read();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    啊这~ 读取文件就一行代码,但是保证流安全的代码竟然这么多

  • try-with-resource示例

    public static void main(String[] args) {
        try(InputStream in = new FileInputStream("D://file.txt")) {
            int read = in.read();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

    将读取代码操作放到try语句块中,自动帮我们安全的关闭流,如何做到的呢?其实和上面的原始的try-catch-finally基本是一样的,只不过这些繁琐的操作由JVM来搞定了而已

    • 反编译代码
    public static void main(String[] args) {
        try {
            InputStream in = new FileInputStream("D://file.txt");
            Throwable var2 = null;
    
            try {
                int var3 = in.read();
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                if (in != null) {
                    if (var2 != null) {
                        try {
                            in.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        in.close();
                    }
                }
    
            }
        } catch (Exception var14) {
            var14.printStackTrace();
        }
    
    }
    

    和上面的自己处理异常基本是一致的,不过JVM处理的更加完善,并且提供了甜甜的糖衣来使用

变长参数

  • 当给方法传递参数的时候,不确定传递多少个的时候,就可以使用变长参数来作为方法的参数,不过变长参数实质上就是数组,所以可以接收多个参数,在方法中处理的时候,需要将变长参数作为数组进行处理

  • 示例

    • 变长参数的类型:
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = 3;
        handle(a, b, c);
    }
    
    
    private static void handle(int a, int... arr) {
        System.out.println(arr.getClass().getSimpleName()); // int[]
    }
    

    可以看到变长参数的类型为int[],可以说明虽然传递了多个参数,但是将这些参数都保存到数组中了,需要注意的是,变长参数只能放到参数列表的最后一个,并且一个方法中只能有一个变长参数

枚举

  • 你知道枚举的实质是class吗?其实枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。每个枚举类型都继承自java.lang.Enum,并自动添加了values和valueOf方法

    我们来一起看看枚举反编译之后的样子吧

  • 源代码:

    public enum Skip {
        ADD,
        SUBTRACT,
        MULTIPLY,
        DIVIDE
    }
    
  • 反编译代码:

    • 第一步编译javac Skip.java
    • 第二步反编译javap -c -v Skip.class
    Compiled from "Skip.java"
    public final class Skip extends java.lang.Enum<Skip>
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
    Constant pool:
       #1 = Fieldref           #4.#38         // Skip.$VALUES:[LSkip;
       #2 = Methodref          #39.#40        // "[LSkip;".clone:()Ljava/lang/Object;
       #3 = Class              #23            // "[LSkip;"
       #4 = Class              #41            // Skip
       #5 = Methodref          #16.#42        // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       #6 = Methodref          #16.#43        // java/lang/Enum."<init>":(Ljava/lang/String;I)V
       #7 = String             #17            // ADD
       #8 = Methodref          #4.#43         // Skip."<init>":(Ljava/lang/String;I)V
       #9 = Fieldref           #4.#44         // Skip.ADD:LSkip;
      #10 = String             #19            // SUBTRACT
      #11 = Fieldref           #4.#45         // Skip.SUBTRACT:LSkip;
      #12 = String             #20            // MULTIPLY
      #13 = Fieldref           #4.#46         // Skip.MULTIPLY:LSkip;
      #14 = String             #21            // DIVIDE
      #15 = Fieldref           #4.#47         // Skip.DIVIDE:LSkip;
      #16 = Class              #48            // java/lang/Enum
      #17 = Utf8               ADD
      #18 = Utf8               LSkip;
      #19 = Utf8               SUBTRACT
      #20 = Utf8               MULTIPLY
      #21 = Utf8               DIVIDE
      #22 = Utf8               $VALUES
      #23 = Utf8               [LSkip;
      #24 = Utf8               values
      #25 = Utf8               ()[LSkip;
      #26 = Utf8               Code
      #27 = Utf8               LineNumberTable
      #28 = Utf8               valueOf
      #29 = Utf8               (Ljava/lang/String;)LSkip;
      #30 = Utf8               <init>
      #31 = Utf8               (Ljava/lang/String;I)V
      #32 = Utf8               Signature
      #33 = Utf8               ()V
      #34 = Utf8               <clinit>
      #35 = Utf8               Ljava/lang/Enum<LSkip;>;
      #36 = Utf8               SourceFile
      #37 = Utf8               Skip.java
      #38 = NameAndType        #22:#23        // $VALUES:[LSkip;
      #39 = Class              #23            // "[LSkip;"
      #40 = NameAndType        #49:#50        // clone:()Ljava/lang/Object;
      #41 = Utf8               Skip
      #42 = NameAndType        #28:#51        // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
      #43 = NameAndType        #30:#31        // "<init>":(Ljava/lang/String;I)V
      #44 = NameAndType        #17:#18        // ADD:LSkip;
      #45 = NameAndType        #19:#18        // SUBTRACT:LSkip;
      #46 = NameAndType        #20:#18        // MULTIPLY:LSkip;
      #47 = NameAndType        #21:#18        // DIVIDE:LSkip;
      #48 = Utf8               java/lang/Enum
      #49 = Utf8               clone
      #50 = Utf8               ()Ljava/lang/Object;
      #51 = Utf8               (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
    {
      public static final Skip ADD;
        descriptor: LSkip;
        flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
    
      public static final Skip SUBTRACT;
        descriptor: LSkip;
        flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
    
      public static final Skip MULTIPLY;
        descriptor: LSkip;
        flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
    
      public static final Skip DIVIDE;
        descriptor: LSkip;
        flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
    
      public static Skip[] values();
        descriptor: ()[LSkip;
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=1, locals=0, args_size=0
             0: getstatic     #1                  // Field $VALUES:[LSkip;
             3: invokevirtual #2                  // Method "[LSkip;".clone:()Ljava/lang/Object;
             6: checkcast     #3                  // class "[LSkip;"
             9: areturn
          LineNumberTable:
            line 5: 0
    
      public static Skip valueOf(java.lang.String);
        descriptor: (Ljava/lang/String;)LSkip;
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: ldc           #4                  // class Skip
             2: aload_0
             3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
             6: checkcast     #4                  // class Skip
             9: areturn
          LineNumberTable:
            line 5: 0
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=4, locals=0, args_size=0
             0: new           #4                  // class Skip
             3: dup
             4: ldc           #7                  // String ADD
             6: iconst_0
             7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
            10: putstatic     #9                  // Field ADD:LSkip;
            13: new           #4                  // class Skip
            16: dup
            17: ldc           #10                 // String SUBTRACT
            19: iconst_1
            20: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
            23: putstatic     #11                 // Field SUBTRACT:LSkip;
            26: new           #4                  // class Skip
            29: dup
            30: ldc           #12                 // String MULTIPLY
            32: iconst_2
            33: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
            36: putstatic     #13                 // Field MULTIPLY:LSkip;
            39: new           #4                  // class Skip
            42: dup
            43: ldc           #14                 // String DIVIDE
            45: iconst_3
            46: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
            49: putstatic     #15                 // Field DIVIDE:LSkip;
            52: iconst_4
            53: anewarray     #4                  // class Skip
            56: dup
            57: iconst_0
            58: getstatic     #9                  // Field ADD:LSkip;
            61: aastore
            62: dup
            63: iconst_1
            64: getstatic     #11                 // Field SUBTRACT:LSkip;
            67: aastore
            68: dup
            69: iconst_2
            70: getstatic     #13                 // Field MULTIPLY:LSkip;
            73: aastore
            74: dup
            75: iconst_3
            76: getstatic     #15                 // Field DIVIDE:LSkip;
            79: aastore
            80: putstatic     #1                  // Field $VALUES:[LSkip;
            83: return
          LineNumberTable:
            line 6: 0
            line 7: 13
            line 8: 26
            line 9: 39
            line 5: 52
    }
    Signature: #35                          // Ljava/lang/Enum<LSkip;>;
    SourceFile: "Skip.java"
    

    通过反编译的代码可以看到,Skip extends java.lang.Enum 定义的枚举类就是一个Class对象,并且继承java.lang.Enum类,自动生成了values方法和valueOf方法。

    每个枚举常量是一个静态常量字段,使用内部类实现,该内部类继承了枚举类。所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化。另外通过把clone、readObject、writeObject这三个方法定义为final的,同时实现是抛出相应的异常。这样保证了每个枚举类型及枚举常量都是不可变的。可以利用枚举的这两个特性来实现线程安全的单例。

说了这么多关于语法糖,那你知道解语法糖是在javac编译的哪个阶段解除糖衣的吗?没错就是在分析与字节码生成阶段,在这个阶段中javac编译器会专门对语法糖进行解除,将糖衣还原为原始代码

下次在吃语法糖的时候,不要忘记在每个糖衣下面的源代码哦~

在这里插入图片描述

微信公众号「指尖上的代码」,欢迎关注~

原创不易, 点个赞再走呗~ 欢迎关注,给你带来更精彩的文章!

你的点赞和关注是写文章最大的动力~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值