本文从Java编译原理的角度,深入字节码和类文件java中->,了解Java语法糖的原理和用法。 它将帮助您在学习如何使用 Java 语法糖的同时了解这些语法糖背后的原理。

语法糖

Syntactic Sugar,又称糖衣语法,是英国计算机科学家Peter.J.Landin发明的术语。 它是指添加到计算机语言中的某种语法。 这种语法对语言的功能没有影响。 但是程序员使用起来更方便。 简而言之,语法糖使程序更简洁、更易读。

有意思的是,在编程领域,除了语法糖之外,还有语法盐、语法糖精等术语,限于篇幅就不展开了。

我们所知道的几乎所有编程语言都有语法糖。 笔者认为,语法糖的多少是判断一门语言是否足够强大的标准之一。

很多人说Java是“低糖语言”。 其实从Java 7开始,在Java语言层面就加入了各种糖,主要是在“Project Coin”项目下开发的。 虽然Java中仍有部分人认为现在的Java是低糖的,但未来会继续往“高糖”的方向发展。

非句法糖

前面说过,语法糖的存在主要是为了方便开发者。 但实际上,Java虚拟机并不支持这些语法糖。 这些语法糖在编译阶段会被还原为简单的基本语法结构,这个过程就是语法糖的解。

说到编译,想必大家都知道,在Java语言中,javac命令可以将一个后缀为.java的源文件编译成可以在Java虚拟机上运行的后缀为.class的字节码。

如果你看一下com.sun.tools.javac.main.JavaCompiler的源码,你会发现在compile()中有一个步骤是调用desugar(),它负责语法糖解的实现。

Java中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆箱、内部类等,本文主要分析这些语法糖背后的原理。 一步一步地剥开糖霜,看看它是什么。

Candy 1.switch支持String和枚举

前面说过,从Java 7开始,Java语言中的语法糖逐渐丰富起来。 其中比较重要的一个是 Java 7 中的 switch 开始支持 String。

在开始编码之前,我们先科普一下。 Java中的switch本身就支持基本类型。 如int、char等。

对于int类型,直接比较数值。 对于char类型,比较它的ascii码。

因此,对于编译器来说,switch中只能使用整数,任何类型的比较都必须转换为整数。 例如字节。 short、char(ackii 代码是整数)和 int。

然后我们看看switch对String的支持,代码如下:

_java中substring用法’/>

反编译后内容如下:

‘/>

看到这段代码就知道原来字符串的切换是通过equals()和hashCode()方法实现的。 幸运的是,hashCode() 方法返回一个 int,而不是 long。

仔细看可以发现实际的switch是hash值,然后通过equals方法比较进行安全检查。 此检查是必要的,因为散列可能会发生冲突。 所以它的性能不如使用枚举切换或使用普通整数常量,但也不错。

糖果 2. 泛型

我们都知道很多语言都支持泛型,但是很多人不知道的是不同的编译器对泛型的处理方式不同。

通常,编译器以两种方式处理泛型:代码专业化和代码共享。

C++和C#采用Code specialization的处理机制,而Java采用Code sharing的机制。

代码共享方法为每个通用类型创建一个唯一的字节码表示,并将通用类型的实例映射到这个唯一的字节码表示。 多个通用类型实例到唯一字节码表示的映射是通过类型擦除完成的。

也就是说,对于Java虚拟机来说,他根本不知道Map映射的语法。 有必要在编译阶段通过类型擦除来去除句法糖。

类型擦除的主要过程如下:

以下代码:

_java中substring用法’/>

语法糖脱糖后会变成:

_http请求中java中302’/>

以下代码:

_http请求中java中302_java中substring用法’/>

类型擦除后变成:

‘/>

虚拟机中没有泛型,只有普通的类和普通的方法。 所有泛型类的类型参数在编译时都会被抹掉,泛型类没有自己唯一的Class对象。 比如没有List.class或者List.class,只有List.class。

糖果3.自动装箱和拆箱

自动装箱是指Java自动将原始类型的值转换成对应的对象,比如将一个int变量转换成一个Integer对象。 这个过程称为装箱。 反之,将一个Integer对象转换成int类型的值,就叫做拆箱。参考:一篇文章了解什么是Java自动拆箱

因为这里的装箱和拆箱是自动的非人工转换,所以叫自动装箱和拆箱。

byte、short、char、int、long、float、double、boolean基本类型对应的封装类有Byte、Short、Character、Integer、Long、Float、Double、Boolean。

我们先看一下自动装箱的代码:

‘/>

反编译代码如下:

_java中substring用法’/>

我们看一下自动拆箱的代码:

‘/>

反编译代码如下:

‘/>

从反编译的内容可以看出,装箱时会自动调用Integer的valueOf(int)方法。 拆箱时会自动调用 Integer 的 intValue 方法。

因此,装箱过程是通过调用wrapper的valueOf方法实现的,拆箱过程是通过调用wrapper的xxxValue方法实现的。

方糖4.方法变长参数

可变参数(variable arguments)是Java 1.5引入的一个特性。 它允许一个方法接受任意数量的值作为参数。

看下面可变参数代码,其中print方法接收可变参数:

‘/>

反编译代码:

_java中substring用法_http请求中java中302’/>

从反编译代码可以看出,当使用可变参数时,首先创建一个长度为调用方法传递的实际参数个数的数组,然后将所有的参数值放入这个数组中,然后将此数组作为参数传递给被调用的方法。

糖果五、枚举

Java SE5 提供了一种新的类型——Java 的枚举类型。 关键字 enum 可以创建一组有限的命名值作为新类型,这些命名值可以用作常规程序组件。 这是一个非常有用的功能。

要想看源码,首先得有一个类,那么枚举类型到底是一个什么样的类呢? 它是枚举吗?

答案显然不是,枚举就像类一样,只是一个关键字,不是类。

那么枚举是由什么类维护的呢? 我们简单地写一个枚举:

public enum t {
 SPRING,SUMMER;
}

然后我们通过反编译看看这段代码是如何实现的。 反编译后代码内容如下:

_java中substring用法’/>

_http请求中java中302_java中substring用法’/>

反编译代码后我们可以看到public final class T extends Enum表示这个类继承了Enum类,final关键字告诉我们这个类不能被继承。

当我们使用enmu来定义枚举类型时,编译器会自动为我们创建一个final类型类来继承Enum类,所以枚举类型是无法被继承的。

糖果六,内部类

内部类也叫嵌套类,内部类可以理解为外部类的一个普通成员。

内部类之所以也是语法糖,是因为它只是一个编译时概念。

在 outer.java 中定义了一个内部类 inner。 编译成功后会生成两个完全不同的.class文件,分别是outer.class和outer$inner.class。 所以内部类的名字可以和它的外部类名一样。

_http请求中java中302’/>

以上代码编译后会生成两个class文件:OutterClass$InnerClass.class和OutterClass.class。

当我们尝试使用jad反编译OutterClass.class文件时,命令行会打印出如下内容:

Parsing OutterClass.class...
Parsing inner class OutterClass$InnerClass.class...
Generating OutterClass.jad

他会把这两个文件全部反编译,然后一起生成一个OutterClass.jad文件。 文件内容如下:

_java中substring用法’/>

_http请求中java中302_java中substring用法’/>

糖果七、条件编译

—正常情况下,程序中的每一行代码都必须参与编译。 但有时为了程序代码优化,希望只编译部分内容。 这时候就需要给程序加上条件,让编译器只编译满足条件的代码,不编译不满足条件的代码。 放弃了,这是条件编译。

与在 C 或 CPP 中一样,条件编译可以通过准备好的语句来实现。 其实条件编译也可以用Java来实现。 我们先来看一段代码:

_java中substring用法_http请求中java中302’/>

反编译代码如下:

‘/>

首先我们发现没有System.out.println("Hello, ONLINE!"); 在反编译代码中,其实就是条件编译。

当if(ONLINE)为false时,编译器不编译其中的代码。

因此,Java语法的条件编译是通过判断条件不变的if语句来实现的。 编译器根据if条件的真假java中->,直接剔除分支为假的代码块。 该方法实现的条件编译必须在方法体中实现,不能对整个Java类的结构或类的属性进行条件编译。

这确实比C/C++的条件编译更受限制。 Java语言设计之初并没有引入条件编译的功能。 虽然有局限性,但聊胜于无。

甜面包 8.断言

在 Java 中,assert 关键字是从 JAVA SE 1.4 引入的。 为了避免在旧版本Java代码中使用assert关键字导致的错误,Java在执行时默认不开启断言检查(此时所有的Assertion语句都被忽略!)。

如果要启用断言检查,则需要使用开关 -enableassertions 或 -ea 来启用它。

考虑一段包含断言的代码:

_java中substring用法_http请求中java中302’/>

反编译代码如下:

_http请求中java中302_java中substring用法’/>

显然,反编译后的代码比我们自己的代码要复杂得多。 因此,我们通过使用assert这个语法糖,节省了大量的代码。

其实断言的底层实现是if语言。 如果断言结果为真,什么都不做,程序继续执行。 如果断言结果为假,程序将抛出一个AssertError来中断程序的执行。

-enableassertions 将设置 $assertionsDisabled 字段的值。

糖果九,数字文字

在 Java 7 中,数字文字,无论是整数还是浮点数,都允许在数字之间插入任意数量的下划线。 这些下划线不会影响字面值,目的是方便阅读。

例如:

‘/>

反编译后:

_http请求中java中302’/>

反编译后,_被删除。 也就是说,编译器无法识别数字字面量中的_,需要在编译阶段将其去除。

十颗糖果,每颗

增强的for循环(for-each)相信大家都不陌生。 日常开发中经常用到。 它将比 for 循环编写更少的代码。 那么这个语法糖是如何实现的呢?

_http请求中java中302’/>

反编译代码如下:

_http请求中java中302_java中substring用法’/>

代码很简单,for-each的原理其实就是用普通的for循环和迭代器。

糖果十一号,尝试资源

在Java中,对于文件操作、IO流、数据库连接等开销非常大的资源,使用后必须及时通过close方法关闭,否则资源会一直打开,可能会导致内存泄漏等问题.

关闭资源的常用方式是在finally块中释放,即调用close方法。 比如我们经常写这样的代码:

_http请求中java中302_java中substring用法’/>

从Java 7开始,jdk提供了更好的资源关闭方式。 使用 try-with-resources 语句重写上面的代码。 效果如下:

_java中substring用法_http请求中java中302’/>

看,这是一个很大的恩惠。 虽然之前我一般都是用IOUtils来关闭流,并没有使用finally里面写一大堆代码的方式,但是这个新的语法糖好像优雅多了。

反编译上面的代码看看背后的原理:

_java中substring用法’/>

_http请求中java中302_java中substring用法’/>

其实背后的原理也很简单。 编译器已经帮我们完成了我们没有做的关闭资源的操作。

所以,再次印证了句法糖的作用是方便程序员使用,但最终还是要转换成编译器看得懂的语言。

糖果十二,Lambda表达式

关于lambda表达式,可能有人会有疑惑,因为网上有人说它不是语法糖。 其实我想更正这个说法。

Labmda 表达式不是匿名内部类的语法糖,但它们也是语法糖。 其实现方式实际上依赖于JVM底层提供的几个lambda相关的API。

我们先来看一个简单的 lambda 表达式。 遍历列表:

_http请求中java中302’/>

为什么说它不是内部类的语法糖呢? 前面我们说过,内部类编译后会有两个class文件,但是包含lambda表达式的类编译后只有一个文件。

反编译代码如下:

_java中substring用法’/>

可以看出,在forEach方法中,实际调用的是java.lang.invoke.LambdaMetafactory#metafactory方法,方法的第四个参数implMethod指定了方法实现。 可以看出这里其实调用了一个lambda$main$0方法进行输出。

我们看一个稍微复杂一点的,先过滤List,然后输出:

_http请求中java中302’/>

反编译代码如下:

‘/>

这两个 lambda 表达式分别调用了 lambda$main$1 和 lambda$main$0 方法。

因此,lambda表达式的实现实际上依赖于一些底层API。 在编译阶段,编译器对 lambda 表达式进行脱糖处理,将其转化为调用内部 API 的方式。

可能遇到的坑

泛型——当泛型遇到重载时

_http请求中java中302_java中substring用法’/>

上面代码有两个重载函数,因为它们的参数类型不同,一个是List,一个是List。 但是,无法编译此代码。 前面我们说过,参数List和List在编译后被擦除,成为同一个原生类型List。 擦除操作导致这两种方法的特征签名变得完全相同。

泛型——当泛型遇到catch

Java 异常处理的 catch 语句中不能使用泛型类型参数。 因为异常处理是由JVM在运行时进行的。由于擦除了类型信息,所以JVM无法区分MyException和MyException这两种异常类型

泛型——当泛型包含静态变量时

‘/>

上面代码的输出是:2! 由于类型擦除,所有泛型类实例都与相同的字节码相关联,并且共享泛型类的所有静态变量。

自动装箱和拆箱——对象相等比较

_java中substring用法’/>

输出结果:

a == b is false
c == d is true

在 Java 5 中,对 Integer 的操作引入了一项新功能,以节省内存并提高性能。 整数对象通过使用相同的对象引用启用缓存和重用。

适用于 -128 到 +127 范围内的整数值。

仅适用于自动装箱。 使用构造函数创建对象不适用。

增强for循环

_http请求中java中302’/>

将抛出 ConcurrentModificationException。

迭代器在一个独立的线程中工作,并拥有一个互斥锁。 Iterator创建后,会创建一个指向原始对象的单链索引表。 当原始对象的个数发生变化时,索引表的内容不会同步变化,所以当索引指针向后移动时,也找不到进行迭代。 对象,所以根据 fail-fast 原则,Iterator 会立即抛出 java.util.ConcurrentModificationException。

所以迭代器在工作的时候不允许被迭代的对象被改变。 但是可以使用Iterator自身的remove()方法来删除对象,Iterator.remove()方法会在删除当前迭代对象的同时保持索引的一致性。

总结

上一节介绍了Java中常用的12种语法糖。 所谓语法糖,就是提供给开发者方便开发的一种语法。

但是这种语法只有开发人员知道。 要执行,它需要被脱糖,即转换成JVM 可以识别的语法。

当我们对语法进行脱糖时,你会发现我们日常使用的方便的语法其实是由其他更简单的语法组成的。

有了这些语法糖,我们在日常开发中可以大大提高效率,同时又能避免过度使用。 最好先了解原理再使用,以免掉坑里。