diff --git a/README.md b/README.md index fbcbd297..c4504e8d 100755 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ - [BigDecimal 详解](./docs/java/basis/bigdecimal.md) - [Java 魔法类 Unsafe 详解](./docs/java/basis/unsafe.md) - [Java SPI 机制详解](./docs/java/basis/spi.md) +- [Java 语法糖详解](./docs/java/basis/syntactic-sugar.md) ### 集合 diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts index 44f38df9..7df4511b 100644 --- a/docs/.vuepress/sidebar.ts +++ b/docs/.vuepress/sidebar.ts @@ -57,6 +57,7 @@ export const sidebarConfig = defineSidebarConfig({ "bigdecimal", "unsafe", "spi", + "syntactic-sugar", ], }, ], diff --git a/docs/home.md b/docs/home.md index 90fa3186..9decf2a9 100644 --- a/docs/home.md +++ b/docs/home.md @@ -66,6 +66,7 @@ title: JavaGuide(Java学习&&面试指南) - [BigDecimal 详解](./java/basis/bigdecimal.md) - [Java 魔法类 Unsafe 详解](./java/basis/unsafe.md) - [Java SPI 机制详解](./java/basis/spi.md) +- [Java 语法糖详解](./java/basis/syntactic-sugar.md) ### 集合 diff --git a/docs/java/basis/java-basic-questions-03.md b/docs/java/basis/java-basic-questions-03.md index 96099383..01ca5e9c 100644 --- a/docs/java/basis/java-basic-questions-03.md +++ b/docs/java/basis/java-basic-questions-03.md @@ -502,3 +502,31 @@ Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来 ### BIO、NIO 和 AIO 的区别? [Java IO 模型详解](../io/io-model.md) + +## 语法糖 + +### 什么是语法糖? + +**语法糖(Syntactic sugar)** 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。 + +举个例子,Java 中的 `for-each` 就是一个常用的语法糖,其原理其实就是基于普通的 for 循环和迭代器。 + +```java +String[] strs = {"JavaGuide", "公众号:JavaGuide", "博客:https://javaguide.cn/"}; +for (String s : strs) { + System.out.println(s); +} +``` + +不过,JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法。这也侧面说明,Java 中真正支持语法糖的是 Java 编译器而不是 JVM。如果你去看`com.sun.tools.javac.main.JavaCompiler`的源码,你会发现在`compile()`中有一个步骤就是调用`desugar()`,这个方法就是负责解语法糖的实现的。 + +### Java 中有哪些常见的语法糖? + +Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。 + +关于这些语法糖的详细解读,请看这篇文章 [Java 语法糖详解](./syntactic-sugar.md) 。 + + + + + diff --git a/docs/java/basis/syntactic-sugar.md b/docs/java/basis/syntactic-sugar.md new file mode 100644 index 00000000..653f025c --- /dev/null +++ b/docs/java/basis/syntactic-sugar.md @@ -0,0 +1,813 @@ +--- +title: Java 语法糖详解 +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: Java 语法糖 + - - meta + - name: description + content: 这篇文章介绍了 12 种 Java 中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成 JVM 认识的语法。当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。有了这些语法糖,我们在日常开发的时候可以大大提升效率,但是同时也要避免过渡使用。使用之前最好了解下原理,避免掉坑。 +--- + +> 作者:Hollis +> +> 原文:https://mp.weixin.qq.com/s/o4XdEMq1DL-nBS-f8Za5Aw +> + +语法糖是大厂 Java 面试常问的一个知识点。 + +本文从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,了解这些语法糖背后的原理。 + +## 什么是语法糖? + +**语法糖(Syntactic Sugar)** 也称糖衣语法,是英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/syntactic-sugar/image-20220818175953954.png) + +> 有意思的是,在编程领域,除了语法糖,还有语法盐和语法糖精的说法,篇幅有限这里不做扩展了。 + +我们所熟知的编程语言中几乎都有语法糖。作者认为,语法糖的多少是评判一个语言够不够牛逼的标准之一。很多人说 Java 是一个“低糖语言”,其实从 Java 7 开始 Java 语言层面上一直在添加各种糖,主要是在“Project Coin”项目下研发。尽管现在 Java 有人还是认为现在的 Java 是低糖,未来还会持续向着“高糖”的方向发展。 + +## Java 中有哪些常见的语法糖? + +前面提到过,语法糖的存在主要是方便开发人员使用。但其实, **Java 虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。** + +说到编译,大家肯定都知道,Java 语言中,`javac`命令可以将后缀名为`.java`的源文件编译为后缀名为`.class`的可以运行于 Java 虚拟机的字节码。如果你去看`com.sun.tools.javac.main.JavaCompiler`的源码,你会发现在`compile()`中有一个步骤就是调用`desugar()`,这个方法就是负责解语法糖的实现的。 + +Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。本文主要来分析下这些语法糖背后的原理。一步一步剥去糖衣,看看其本质。 + +我们这里会用到[反编译](https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650120609&idx=1&sn=5659f96310963ad57d55b48cee63c788&chksm=f36bbc80c41c3596a1e4bf9501c6280481f1b9e06d07af354474e6f3ed366fef016df673a7ba&scene=21#wechat_redirect),你可以通过 [Decompilers online](http://www.javadecompilers.com/) 对 Class 文件进行在线反编译。 + +### switch 支持 String 与枚举 + +前面提到过,从 Java 7 开始,Java 语言中的语法糖在逐渐丰富,其中一个比较重要的就是 Java 7 中`switch`开始支持`String`。 + +在开始之前先科普下,Java 中的`switch`自身原本就支持基本类型。比如`int`、`char`等。对于`int`类型,直接进行数值的比较。对于`char`类型则是比较其 ascii 码。所以,对于编译器来说,`switch`中其实只能使用整型,任何类型的比较都要转换成整型。比如`byte`。`short`,`char`(ackii 码是整型)以及`int`。 + +那么接下来看下`switch`对`String`得支持,有以下代码: + +```java +public class switchDemoString { + public static void main(String[] args) { + String str = "world"; + switch (str) { + case "hello": + System.out.println("hello"); + break; + case "world": + System.out.println("world"); + break; + default: + break; + } + } +} +``` + +反编译后内容如下: + +```java +public class switchDemoString +{ + public switchDemoString() + { + } + public static void main(String args[]) + { + String str = "world"; + String s; + switch((s = str).hashCode()) + { + default: + break; + case 99162322: + if(s.equals("hello")) + System.out.println("hello"); + break; + case 113318802: + if(s.equals("world")) + System.out.println("world"); + break; + } + } +} +``` + +看到这个代码,你知道原来 **字符串的 switch 是通过`equals()`和`hashCode()`方法来实现的。** 还好`hashCode()`方法返回的是`int`,而不是`long`。 + +仔细看下可以发现,进行`switch`的实际是哈希值,然后通过使用`equals`方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行 `switch` 或者使用纯整数常量,但这也不是很差。 + +### 泛型 + +我们都知道,很多语言都是支持泛型的,但是很多人不知道的是,不同的编译器对于泛型的处理方式是不同的,通常情况下,一个编译器处理泛型有两种方式:`Code specialization`和`Code sharing`。C++和 C#是使用`Code specialization`的处理机制,而 Java 使用的是`Code sharing`的机制。 + +> Code sharing 方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(`type erasue`)实现的。 + +也就是说,**对于 Java 虚拟机来说,他根本不认识`Map map`这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。** + +类型擦除的主要过程如下: 1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。 2.移除所有的类型参数。 + +以下代码: + +```java +Map map = new HashMap(); +map.put("name", "hollis"); +map.put("wechat", "Hollis"); +map.put("blog", "www.hollischuang.com"); +``` + +解语法糖之后会变成: + +```java +Map map = new HashMap(); +map.put("name", "hollis"); +map.put("wechat", "Hollis"); +map.put("blog", "www.hollischuang.com"); +``` + +以下代码: + +```java +public static > A max(Collection xs) { + Iterator xi = xs.iterator(); + A w = xi.next(); + while (xi.hasNext()) { + A x = xi.next(); + if (w.compareTo(x) < 0) + w = x; + } + return w; +} +``` + +类型擦除后会变成: + +```java + public static Comparable max(Collection xs){ + Iterator xi = xs.iterator(); + Comparable w = (Comparable)xi.next(); + while(xi.hasNext()) + { + Comparable x = (Comparable)xi.next(); + if(w.compareTo(x) < 0) + w = x; + } + return w; +} +``` + +**虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的`Class`类对象。比如并不存在`List.class`或是`List.class`,而只有`List.class`。** + +### 自动装箱与拆箱 + +自动装箱就是 Java 自动将原始类型值转换成对应的对象,比如将 int 的变量转换成 Integer 对象,这个过程叫做装箱,反之将 Integer 对象转换成 int 类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。原始类型 byte, short, char, int, long, float, double 和 boolean 对应的封装类为 Byte, Short, Character, Integer, Long, Float, Double, Boolean。 + +先来看个自动装箱的代码: + +```java + public static void main(String[] args) { + int i = 10; + Integer n = i; +} +``` + +反编译后代码如下: + +```java +public static void main(String args[]) +{ + int i = 10; + Integer n = Integer.valueOf(i); +} +``` + +再来看个自动拆箱的代码: + +```java +public static void main(String[] args) { + + Integer i = 10; + int n = i; +} +``` + +反编译后代码如下: + +```java +public static void main(String args[]) +{ + Integer i = Integer.valueOf(10); + int n = i.intValue(); +} +``` + +从反编译得到内容可以看出,在装箱的时候自动调用的是`Integer`的`valueOf(int)`方法。而在拆箱的时候自动调用的是`Integer`的`intValue`方法。 + +所以,**装箱过程是通过调用包装器的 valueOf 方法实现的,而拆箱过程是通过调用包装器的 xxxValue 方法实现的。** + +### 可变长参数 + +可变参数(`variable arguments`)是在 Java 1.5 中引入的一个特性。它允许一个方法把任意数量的值作为参数。 + +看下以下可变参数代码,其中 `print` 方法接收可变参数: + +```java +public static void main(String[] args) + { + print("Holis", "公众号:Hollis", "博客:www.hollischuang.com", "QQ:907607222"); + } + +public static void print(String... strs) +{ + for (int i = 0; i < strs.length; i++) + { + System.out.println(strs[i]); + } +} +``` + +反编译后代码: + +```java + public static void main(String args[]) +{ + print(new String[] { + "Holis", "\u516C\u4F17\u53F7:Hollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com", "QQ\uFF1A907607222" + }); +} + +public static transient void print(String strs[]) +{ + for(int i = 0; i < strs.length; i++) + System.out.println(strs[i]); + +} +``` + +从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。 + +### 枚举 + +Java SE5 提供了一种新的类型-Java 的枚举类型,关键字`enum`可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。 + +要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是`enum`吗?答案很明显不是,`enum`就和`class`一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单的写一个枚举: + +```java +public enum t { + SPRING,SUMMER; +} +``` + +然后我们使用反编译,看看这段代码到底是怎么实现的,反编译后代码内容如下: + +```java +public final class T extends Enum +{ + private T(String s, int i) + { + super(s, i); + } + public static T[] values() + { + T at[]; + int i; + T at1[]; + System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i); + return at1; + } + + public static T valueOf(String s) + { + return (T)Enum.valueOf(demo/T, s); + } + + public static final T SPRING; + public static final T SUMMER; + private static final T ENUM$VALUES[]; + static + { + SPRING = new T("SPRING", 0); + SUMMER = new T("SUMMER", 1); + ENUM$VALUES = (new T[] { + SPRING, SUMMER + }); + } +} +``` + +通过反编译后代码我们可以看到,`public final class T extends Enum`,说明,该类是继承了`Enum`类的,同时`final`关键字告诉我们,这个类也是不能被继承的。 + +**当我们使用`enum`来定义一个枚举类型的时候,编译器会自动帮我们创建一个`final`类型的类继承`Enum`类,所以枚举类型不能被继承。** + +### 内部类 + +内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。 + +**内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,`outer.java`里面定义了一个内部类`inner`,一旦编译成功,就会生成两个完全不同的`.class`文件了,分别是`outer.class`和`outer$inner.class`。所以内部类的名字完全可以和它的外部类名字相同。** + +```java +public class OutterClass { + private String userName; + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public static void main(String[] args) { + + } + + class InnerClass{ + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} +``` + +以上代码编译后会生成两个 class 文件:`OutterClass$InnerClass.class` 、`OutterClass.class` 。当我们尝试对`OutterClass.class`文件进行反编译的时候,命令行会打印以下内容:`Parsing OutterClass.class...Parsing inner class OutterClass$InnerClass.class... Generating OutterClass.jad` 。他会把两个文件全部进行反编译,然后一起生成一个`OutterClass.jad`文件。文件内容如下: + +```java +public class OutterClass +{ + class InnerClass + { + public String getName() + { + return name; + } + public void setName(String name) + { + this.name = name; + } + private String name; + final OutterClass this$0; + + InnerClass() + { + this.this$0 = OutterClass.this; + super(); + } + } + + public OutterClass() + { + } + public String getUserName() + { + return userName; + } + public void setUserName(String userName){ + this.userName = userName; + } + public static void main(String args1[]) + { + } + private String userName; +} +``` + +### 条件编译 + +—般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。 + +如在 C 或 CPP 中,可以通过预处理语句来实现条件编译。其实在 Java 中也可实现条件编译。我们先来看一段代码: + +```java +public class ConditionalCompilation { + public static void main(String[] args) { + final boolean DEBUG = true; + if(DEBUG) { + System.out.println("Hello, DEBUG!"); + } + + final boolean ONLINE = false; + + if(ONLINE){ + System.out.println("Hello, ONLINE!"); + } + } +} +``` + +反编译后代码如下: + +```java +public class ConditionalCompilation +{ + + public ConditionalCompilation() + { + } + + public static void main(String args[]) + { + boolean DEBUG = true; + System.out.println("Hello, DEBUG!"); + boolean ONLINE = false; + } +} +``` + +首先,我们发现,在反编译后的代码中没有`System.out.println("Hello, ONLINE!");`,这其实就是条件编译。当`if(ONLINE)`为 false 的时候,编译器就没有对其内的代码进行编译。 + +所以,**Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假,编译器直接把分支为 false 的代码块消除。通过该方式实现的条件编译,必须在方法体内实现,而无法在正整个 Java 类的结构或者类的属性上进行条件编译,这与 C/C++的条件编译相比,确实更有局限性。在 Java 语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。** + +### 断言 + +在 Java 中,`assert`关键字是从 JAVA SE 1.4 引入的,为了避免和老版本的 Java 代码中使用了`assert`关键字导致错误,Java 在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!),如果要开启断言检查,则需要用开关`-enableassertions`或`-ea`来开启。 + +看一段包含断言的代码: + +```java +public class AssertTest { + public static void main(String args[]) { + int a = 1; + int b = 1; + assert a == b; + System.out.println("公众号:Hollis"); + assert a != b : "Hollis"; + System.out.println("博客:www.hollischuang.com"); + } +} +``` + +反编译后代码如下: + +```java +public class AssertTest { + public AssertTest() + { + } + public static void main(String args[]) +{ + int a = 1; + int b = 1; + if(!$assertionsDisabled && a != b) + throw new AssertionError(); + System.out.println("\u516C\u4F17\u53F7\uFF1AHollis"); + if(!$assertionsDisabled && a == b) + { + throw new AssertionError("Hollis"); + } else + { + System.out.println("\u535A\u5BA2\uFF1Awww.hollischuang.com"); + return; + } +} + +static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus(); + +} +``` + +很明显,反编译之后的代码要比我们自己的代码复杂的多。所以,使用了 assert 这个语法糖我们节省了很多代码。**其实断言的底层实现就是 if 语言,如果断言结果为 true,则什么都不做,程序继续执行,如果断言结果为 false,则程序抛出 AssertError 来打断程序的执行。**`-enableassertions`会设置\$assertionsDisabled 字段的值。 + +### 数值字面量 + +在 java 7 中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。 + +比如: + +```java +public class Test { + public static void main(String... args) { + int i = 10_000; + System.out.println(i); + } +} +``` + +反编译后: + +```java +public class Test +{ + public static void main(String[] args) + { + int i = 10000; + System.out.println(i); + } +} +``` + +反编译后就是把`_`删除了。也就是说 **编译器并不认识在数字字面量中的`_`,需要在编译阶段把他去掉。** + +### for-each + +增强 for 循环(`for-each`)相信大家都不陌生,日常开发经常会用到的,他会比 for 循环要少写很多代码,那么这个语法糖背后是如何实现的呢? + +```java +public static void main(String... args) { + String[] strs = {"Hollis", "公众号:Hollis", "博客:www.hollischuang.com"}; + for (String s : strs) { + System.out.println(s); + } + List strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com"); + for (String s : strList) { + System.out.println(s); + } +} +``` + +反编译后代码如下: + +```java +public static transient void main(String args[]) +{ + String strs[] = { + "Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com" + }; + String args1[] = strs; + int i = args1.length; + for(int j = 0; j < i; j++) + { + String s = args1[j]; + System.out.println(s); + } + + List strList = ImmutableList.of("Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com"); + String s; + for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s)) + s = (String)iterator.next(); + +} +``` + +代码很简单,**for-each 的实现原理其实就是使用了普通的 for 循环和迭代器。** + +### try-with-resource + +Java 里,对于文件操作 IO 流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过 close 方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。 + +关闭资源的常用方式就是在`finally`块里是释放,即调用`close`方法。比如,我们经常会写这样的代码: + +```java +public static void main(String[] args) { + BufferedReader br = null; + try { + String line; + br = new BufferedReader(new FileReader("d:\\hollischuang.xml")); + while ((line = br.readLine()) != null) { + System.out.println(line); + } + } catch (IOException e) { + // handle exception + } finally { + try { + if (br != null) { + br.close(); + } + } catch (IOException ex) { + // handle exception + } + } +} +``` + +从 Java 7 开始,jdk 提供了一种更好的方式关闭资源,使用`try-with-resources`语句,改写一下上面的代码,效果如下: + +```java +public static void main(String... args) { + try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) { + String line; + while ((line = br.readLine()) != null) { + System.out.println(line); + } + } catch (IOException e) { + // handle exception + } +} +``` + +看,这简直是一大福音啊,虽然我之前一般使用`IOUtils`去关闭流,并不会使用在`finally`中写很多代码的方式,但是这种新的语法糖看上去好像优雅很多呢。看下他的背后: + +```java +public static transient void main(String args[]) + { + BufferedReader br; + Throwable throwable; + br = new BufferedReader(new FileReader("d:\\ hollischuang.xml")); + throwable = null; + String line; + try + { + while((line = br.readLine()) != null) + System.out.println(line); + } + catch(Throwable throwable2) + { + throwable = throwable2; + throw throwable2; + } + if(br != null) + if(throwable != null) + try + { + br.close(); + } + catch(Throwable throwable1) + { + throwable.addSuppressed(throwable1); + } + else + br.close(); + break MISSING_BLOCK_LABEL_113; + Exception exception; + exception; + if(br != null) + if(throwable != null) + try + { + br.close(); + } + catch(Throwable throwable3) + { + throwable.addSuppressed(throwable3); + } + else + br.close(); + throw exception; + IOException ioexception; + ioexception; + } +} +``` + +**其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。** + +### Lambda 表达式 + +关于 lambda 表达式,有人可能会有质疑,因为网上有人说他并不是语法糖。其实我想纠正下这个说法。**Labmda 表达式不是匿名内部类的语法糖,但是他也是一个语法糖。实现方式其实是依赖了几个 JVM 底层提供的 lambda 相关 api。** + +先来看一个简单的 lambda 表达式。遍历一个 list: + +```java +public static void main(String... args) { + List strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com"); + + strList.forEach( s -> { System.out.println(s); } ); +} +``` + +为啥说他并不是内部类的语法糖呢,前面讲内部类我们说过,内部类在编译之后会有两个 class 文件,但是,包含 lambda 表达式的类编译后只有一个文件。 + +反编译后代码如下: + +```java +public static /* varargs */ void main(String ... args) { + ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com"); + strList.forEach((Consumer)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)()); +} + +private static /* synthetic */ void lambda$main$0(String s) { + System.out.println(s); +} +``` + +可以看到,在`forEach`方法中,其实是调用了`java.lang.invoke.LambdaMetafactory#metafactory`方法,该方法的第四个参数 `implMethod` 指定了方法实现。可以看到这里其实是调用了一个`lambda$main$0`方法进行了输出。 + +再来看一个稍微复杂一点的,先对 List 进行过滤,然后再输出: + +```java +public static void main(String... args) { + List strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com"); + + List HollisList = strList.stream().filter(string -> string.contains("Hollis")).collect(Collectors.toList()); + + HollisList.forEach( s -> { System.out.println(s); } ); +} +``` + +反编译后代码如下: + +```java +public static /* varargs */ void main(String ... args) { + ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com"); + List HollisList = strList.stream().filter((Predicate)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList()); + HollisList.forEach((Consumer)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$1(java.lang.Object ), (Ljava/lang/Object;)V)()); +} + +private static /* synthetic */ void lambda$main$1(Object s) { + System.out.println(s); +} + +private static /* synthetic */ boolean lambda$main$0(String string) { + return string.contains("Hollis"); +} +``` + +两个 lambda 表达式分别调用了`lambda$main$1`和`lambda$main$0`两个方法。 + +**所以,lambda 表达式的实现其实是依赖了一些底层的 api,在编译阶段,编译器会把 lambda 表达式进行解糖,转换成调用内部 api 的方式。** + +## 可能遇到的坑 + +### 泛型 + +**一、当泛型遇到重载** + +```java +public class GenericTypes { + + public static void method(List list) { + System.out.println("invoke method(List list)"); + } + + public static void method(List list) { + System.out.println("invoke method(List list)"); + } +} +``` + +上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是`List`另一个是`List` ,但是,这段代码是编译通不过的。因为我们前面讲过,参数`List`和`List`编译之后都被擦除了,变成了一样的原生类型 List,擦除动作导致这两个方法的特征签名变得一模一样。 + +**二、当泛型遇到 catch** + +泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型`MyException`和`MyException`的 + +**三、当泛型内包含静态变量** + +```java +public class StaticTest{ + public static void main(String[] args){ + GT gti = new GT(); + gti.var=1; + GT gts = new GT(); + gts.var=2; + System.out.println(gti.var); + } +} +class GT{ + public static int var=0; + public void nothing(T x){} +} +``` + +以上代码输出结果为:2! + +由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。 + +### 自动装箱与拆箱 + +**对象相等比较** + +```java +public static void main(String[] args) { + Integer a = 1000; + Integer b = 1000; + Integer c = 100; + Integer d = 100; + System.out.println("a == b is " + (a == b)); + System.out.println(("c == d is " + (c == d))); +} +``` + +输出结果: + +``` +a == b is false +c == d is true +``` + +在 Java 5 中,在 Integer 的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。 + +> 适用于整数值区间-128 至 +127。 +> +> 只适用于自动装箱。使用构造函数创建对象不适用。 + +### 增强 for 循环 + +```java +for (Student stu : students) { + if (stu.getId() == 2) + students.remove(stu); +} +``` + +会抛出`ConcurrentModificationException`异常。 + +Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出`java.util.ConcurrentModificationException`异常。 + +所以 `Iterator` 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 `Iterator` 本身的方法`remove()`来删除对象,`Iterator.remove()` 方法会在删除当前迭代对象的同时维护索引的一致性。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/syntactic-sugar/image-20220818175912313.png) + +## 总结 + +前面介绍了 12 种 Java 中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成 JVM 认识的语法。当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。 + +有了这些语法糖,我们在日常开发的时候可以大大提升效率,但是同时也要避免过渡使用。使用之前最好了解下原理,避免掉坑。 \ No newline at end of file