当前位置: 首页 > 科技观察

Java中的语法糖,太甜了

时间:2023-03-12 03:33:47 科技观察

我们在日常开发中经常会用到泛型、自动拆箱装箱、内部类、增强for循环、try-with-resources语法、lambda表达式等。因为这些特性可以帮助我们减少开发工作量;但是我们从来没有认真研究过这些特性的本质,所以在这篇文章中,cxuan将为您揭开这些特性背后的真相。在讲语法糖之前,我们需要了解一下语法糖的概念:语法糖(Syntacticsugar),又称糖衣语法,是英国科学家发明的一个名词。一般来说,使用语法糖可以增加程序的可读性,从而减少程序代码出错的几率,真是甜甜的。句法糖是指添加到计算机语言中的某种语法。这种语法对语言的功能没有影响,反而更方便程序员使用。因为Java代码需要运行在JVM中,JVM不支持语法糖,语法糖会在程序编译阶段还原为简单的基本语法结构。这个过程就是语法糖的解。所以在Java中,真正支持语法糖的是Java编译器。.....下面我们来看看Java中的这些语法糖泛型泛型是语法糖的一种。在JDK1.5中引入了泛型机制,但泛型机制本身是通过类型擦除实现的。JVM中没有泛型,只有普通的类型和普通的方法。泛型类的类型参数编译后会被擦除。泛型没有自己独特的类类型。ListaList=newArrayList();ListbList=newArrayList();System.out.println(aList.getClass()==bList.getClass());List和List被认为是不同的类型,但是输出具有相同的结果。这是因为通用信息只存在于代码编译阶段。在进入JVM之前,泛型相关的信息会被抹掉。技术术语称为类型擦除。但是,不允许将Integer类型的数据放入List或将String类型的数据放入List。如下图所示,Integer类型的数据放入List中失败,与String类型的数据放入List中失败是一样的,都会编译失败。自动拆箱和自动装箱自动拆箱和自动装箱是一种语法糖,表示八种原始数据类型的包装类与其原始数据类型之间的自动转换。简单来说,装箱就是将基本数据类型自动转换为包装类型;拆箱是自动将包装器类型转换为基本数据类型。我们先来了解一下基本数据类型的包装类有哪些。也就是说,上面的基本数据类型和包装类在转换过程中都会自动装箱/拆箱,例如下面的代码Integerinteger=66;//自动拆箱inti1=integer;//上面的整型对象自动装箱的代码会使用基本数据类型进行赋值,但是基本数据类型i1将其赋值给一个对象类型,一般情况下是做不到的,但是编译器允许我们这样做,这其实是一种语法糖。这种语法糖方便我们进行数值运算。如果没有语法糖,在进行数值运算时需要将对象转换为基本数据类型。基本数据类型还需要转换为包装类型才能使用其内置方法,这无疑增加了代码冗余。那么自动拆箱和自动装箱是如何实现的呢?其实这背后的原理是编译器做了优化。将基本类型赋值给包装类实际上是调用包装类的valueOf()方法创建一个包装类,然后将其赋值给基本类型。inti1=Integer.valueOf(1);而包装类对基本类型的赋值就是调用包装类的xxxValue()方法获取基本数据类型,然后进行赋值。Integeri1=newInteger(1).intValue();我们使用javap-c反编译上面的自动装箱和自动拆箱验证可以看到代码2处调用invokestatic时相当于编译器自动添加了Integer.valueOf方法将原始数据类型转换为wrapper类型。代码7处调用invokevirtual时,相当于编译器为我们添加了Integer.intValue()方法,将Integer的值转换为基本数据类型。枚举我们在日常开发中经常会用到enum和publicstaticfinal...语法。那么什么时候使用enum或publicstaticfinal等常量呢?好像还行。但是在Java字节码结构中,并没有枚举类型。枚举只是一种语法糖。编译后会编译成一个普通的类,这个类也是用Class装饰的。该类继承自java.lang.Enum并使用final关键字修饰。我们举个例子看看publicenumSchool{STUDENT,TEACHER;}这是一个School的枚举,里面有两个字段,一个是STUDENT,一个是TEACHER,别的什么都没有。接下来,我们使用javap反编译School.class。反编译完成后的结果如下。从图中我们可以看出,枚举其实是继承自java.lang.Enum类的一个类。里面的属性STUDENT和TEACHER本质上都是publicstaticfinal修饰的字段。这实际上是一个编译器优化。毕竟STUDENT在美观和简洁上要比publicstaticfinalSchoolSTUDENT好很多。另外,编译器会为我们生成两个方法,values()方法和valueOf方法。这两个方法是编译器为我们添加的。通过使用values()方法,您可以获得所有Enum属性值,而valueOf方法用于获取单个属性值。需要注意的是Enum的values()方法并不是JDKAPI的一部分,在Java源码中,也没有values()方法的相关注解。用法如下System.out.println(School.STUDENT.getName());School[]values=School.values();for(Schoolschool:values){System.out.println("name="+school.getName());}}}内部类内部类是Java的一个小众特性。之所以说小众,并不是说内部类没有用,而是在我们日常开发中很少用到,但是查看JDK源码发现,很多源码都是有内部类结构的。比如在常见的ArrayList源码中,有一个继承自Iterator类的Itr内部类;再比如,在HashMap中,构造一个Node继承自Map.Entry来表示HashMap的每一个节点。Java语言之所以引入内部类,是因为有时候一个类只想在一个类中有用,不想在其他地方用到,也就是对外部隐藏内部细节。内部类其实是一个语法糖,因为它只是一个编译期的概念。一旦编译完成,编译器会为内部类生成一个单独的class文件,命名为outer$innter.class。让我们用一个例子来验证它。publicclassOuterClass{privateStringlabel;classInnerClass{publicStringlinkOuter(){returnlabel="inner";}}publicstaticvoidmain(String[]args){OuterClassouterClass=newOuterClass();InnerClassinnerClass=outerClass.newInnerClass();System.out.println(innerClass.linkOuter()));}}编译完上面这段,会生成两个class文件,一个是OuterClass.class,一个是OuterClass$InnerClass.class,说明外部类可以链接到内部类,而内部类class可以修改外部类的属性等,我们来看看内部类的编译结果。如上图所示,编译后的内部类的linkOuter()方法会生成一个指向外部类的this引用,这是连接外部类和内部类的引用。变长参数变长参数也是比较少见的用法。所谓可变长度参数是指该方法可以接受长度不确定的参数。一般我们在开发中不会使用变长参数,也不推荐使用变长参数,它会让我们的程序难以处理。但是我们有必要了解变长参数的特点。它的基本用法如下printMessage("l","am","cxuan");}}变长参数也是一种语法糖,那么它是如何实现的呢?我们可以猜测它的内部应该是由数组组成的,否则不能接受多个值,那么我们反编译看它是不是数组实现的。如您所见,printMessage()的参数是使用数组接收的,所以不要被变长参数所迷惑!JDK1.5中引入了可变长度参数功能。使用可变长度参数有两个条件。变长参数具有相同的类型,并且变长参数必须位于方法参数列表的末尾。增强的for循环为什么在普通的for循环之后会有一个增强的for循环?想一想,普通的for循环不需要知道遍历次数吗?您还需要每次都知道数组的索引。这种写法显然有点繁琐。与普通的for循环相比,增强的for循环功能更强,代码更简洁。您不需要知道遍历次数和要遍历的数组的索引。增强for循环的对象要么是数组,要么实现了Iterable接口。这个语法糖主要用来遍历数组或者集合,在循环过程中不能改变集合的大小。publicstaticvoidmain(String[]args){String[]params=newString[]{"hello","world"};//将for循环对象增强为数组for(Stringstr:params){System.out.println(海峡);}Listlists=Arrays.asList("hello","world");//增强for循环对象实现Iterable接口for(Stringstr:lists){System.out.println(str);}}编译后class文件如下形式增强for退化为普通forfor(intstr=0;str