• JVM系列之语法糖的味道


    随笔

    最近一直在研究“微服务、云原生”的相关的内容,收获颇丰,导致文章晚了一周才发布,以后的博主的文章大概率是“微服务、云原生相关的内容,如果感兴趣的可以关注一下博主的动态~~

    在这里插入图片描述

    引言

    参考书籍:“深入理解Java虚拟机

    个人java知识分享项目——gitee地址

    个人java知识分享项目——github地址

    java语法

    文章主要从虚拟机的角度去了解一下我们在日常开发中常用的java语法糖,如:泛型、自动拆封箱、循环遍历、条件编译、变长参数、内部类、枚举类、断言语句、对枚举和字符串(在 JDK 1.7中支持)的switch支持、try语句中定义和关闭资源。

    泛型

    泛型是JDK 1.5的一项新增特性,它的本质是参数化类型(Parametersized Type)的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

    泛型的作用

    泛型思想早在C++语言的模板(Template)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。例如,如果没有通过泛型指定List集合中存的元素的类型,在List集合的存取时,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于java.lang.Object,所以Object转型成任何对象都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的 风险就会转嫁到程序运行期之中。

    public static void main(String[] args) {
            demo(Lists.newArrayList(new User("disaster", 1), new User("disaster1", 2)));
            demo1(Lists.newArrayList(new User("disaster", 1), new User("disaster1", 2), "disaster"));
        }
    
        public static void demo(List<User> users) {
            for (User user : users) {
                System.out.println(user.getName());
            }
        }
    
        public static void demo1(List users) {
            for (Object user : users) {
                User user1 = (User) user;
                System.out.println(user1.getName());
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    上面的这个案例就很好的说明了这个问题,在运行demo1方法时程序抛出ClassCastException异常。

    泛型的原理

    泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的CLR中,都是切实存在的,List与List就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。

    Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList< int >与ArrayList< String >就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

    案例:

    public static void method(List<String> list) {
            System.out.println("invoke method(Listlist)");
        }
    public static void method(List<Integer> list) {
            System.out.println("invoke method(Listlist)");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这两个method是无法通过编译的,原因在泛型的原理已经知道了

    但是还有一个比较特殊的情况需要稍微多提一嘴:

    public static String method(List<String>list){
            System.out.println("invoke method(Listlist)"); return"";
        }
    public static int method(List<Integer>list){ System.out.println("invoke method(Listlist)"); return 1;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    大家可以使用jdk1.6的版本去编译这个案例,我们可以很神奇的发现,这个两个方法可以存在同一个类中,我们知道方法重载的返回值不参与重载选择,但是上面这两个方法的参数都是List对象,只有返回值不同,那正常按道理来说是不能编译通过才对。再强调一下重载不是根据返回值来确定,至于这里为什么两个相同method方法能够编译执行是因为在jdk1.6的版本的Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说,两个方法 如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个Class文件中的。

    但是这种奇特的现象与“返回值不参与重载选择”的基本认知相悖,因此JCP组织对虚拟机规范做出了相应的修改,引入了诸如Signature、LocalVariableTypeTable等新的属性用 于解决伴随泛型而来的参数类型的识别问题,Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。

    自动拆封箱、循环遍历、变长参数

    案例:

    public static void demo3(Integer... args) {
            List<Integer> list = Arrays.asList(args);
            Integer sum = 0;
            for (Integer i : list) {
                sum += i;
                System.out.println(i + sum);
            }
            System.out.println(sum);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    编译后的字节码:

     0 aload_0
     1 invokestatic #27 <java/util/Arrays.asList : ([Ljava/lang/Object;)Ljava/util/List;>
     4 astore_1
     5 iconst_0
     6 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
     9 astore_2
    10 aload_1
    11 invokeinterface #12 <java/util/List.iterator : ()Ljava/util/Iterator;> count 1
    16 astore_3
    17 aload_3
    18 invokeinterface #13 <java/util/Iterator.hasNext : ()Z> count 1
    23 ifeq 70 (+47)
    26 aload_3
    27 invokeinterface #14 <java/util/Iterator.next : ()Ljava/lang/Object;> count 1
    32 checkcast #28 <java/lang/Integer>
    35 astore 4
    37 aload_2
    38 invokevirtual #29 <java/lang/Integer.intValue : ()I>
    41 aload 4
    43 invokevirtual #29 <java/lang/Integer.intValue : ()I>
    46 iadd
    47 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
    50 astore_2
    51 getstatic #15 <java/lang/System.out : Ljava/io/PrintStream;>
    54 aload 4
    56 invokevirtual #29 <java/lang/Integer.intValue : ()I>
    59 aload_2
    60 invokevirtual #29 <java/lang/Integer.intValue : ()I>
    63 iadd
    64 invokevirtual #30 <java/io/PrintStream.println : (I)V>
    67 goto 17 (-50)
    70 getstatic #15 <java/lang/System.out : Ljava/io/PrintStream;>
    73 aload_2
    74 invokevirtual #31 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
    77 return
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    上面这个案例中包含了自动装箱、拆箱、遍历循环与变长参数、泛型这几种语法糖,我们通过编译后的字节码去看看这些语法糖的实际面目,泛型就不必说了,自动装箱、拆箱在编译之后被转化成了对应的包装和还原方法,如本例中的Integer.valueOf()与 Integer.intValue()方法,而遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。最后再看看变长参数,看到字节码的第一行就调用了“”方法,那我们就去这个方法的内部去看看:

    public static <T> List<T> asList(T... a) {
            return new ArrayList<>(a);
        }
    
    private static class ArrayList<E> extends AbstractList<E>
            implements RandomAccess, java.io.Serializable
    {
    		private static final long serialVersionUID = -2764017481108945198L;
            private final E[] a;
    
            ArrayList(E[] array) {
                a = Objects.requireNonNull(array);
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    它在调用的时候变成了一个数组类型的参数,在变长参数出现之前,程序员就是使用数组来完成类似功能的。

    虽然语法糖给我开发人员带来了便利,但是有些坑也是需要我们去踩一踩的,拿个面试碰到过的案例来说:

    public static void main(String[]args){ Integer a=1;
    	Integer b=2;
    	Integer c=3;
    	Integer d=3;
    	Integer e=321;
    	Integer f=321;
    	Long g=3L; 
    	System.out.println(c==d); 	
    	System.out.println(e==f); 
    	System.out.println(c==(a+b)); 
    	System.out.println(c.equals(a+b)); 
    	System.out.println(g==(a+b));
    	System.out.println(g.equals(a+b));
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这个案例大家可以去自己的机器上去运行看看结果(还有类似String的题目大家也可以感受一下)。

    条件编译

    案例:

    public static void demo5() {
            if (true) {
                System.out.println("block 1");
            } else {
                System.out.println("block 2");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    字节码:

    0 getstatic #15 <java/lang/System.out : Ljava/io/PrintStream;>
    3 ldc #32 <block 1>
    5 invokevirtual #17 <java/io/PrintStream.println : (Ljava/lang/String;)V>
    8 return
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    看看编译后的字节码,我们发现demo5方法编译之后只有打印“block 1”字符串的代码,这其实就是条件编译之后的之后。同样的道理如果if里面是false编译之后久只有“block 2”字符串的代码.

    还有几个语法糖这篇文章没有去说,但是大家可以根据我上面说的这几个语法糖案例的思路去查看它的原理即可~~~~

  • 相关阅读:
    学生python编程----飞机训练
    毕业设计:基于Springboot+Vue+ElementUI实现疫情社区管理系统
    LeetCode·429.N叉树的层次遍历·层次遍历
    OPENCV--调用GrabCut实现图像分割
    Grafana Prometheus 监控JVM进程
    Mallox勒索病毒:最新变种malloxx袭击了您的计算机?
    16-自动化测试——selenium介绍
    如何快速上手短视频创作,有什么建议吗?
    【Web前端基础进阶学习】HTML详解(下篇)
    大数据开源平台好在哪里?
  • 原文地址:https://blog.csdn.net/a_ittle_pan/article/details/126567959