前言
语法糖(Syntactic Sugar), 又称糖衣语法, 是由英国计算机学家 Peter.J.Landin 发明的一个术语, 指在计算机语言中添加的某种语法. 这种语法对语言的功能并没有影响, 但往往能让程序更加简洁, 并有更高的可读性, 从而方便程序员使用, 减少代码出错的机会, 并提升开发效率. 简单来说, 语法糖就是对现有语法的一种包装.
很多编程语言中都有语法糖, Java 也是如此. 要明白, 语法糖仅存在于编译时, JVM 并不认识这些语法糖. 因此, 在编译阶段, Java 编译器会将语法糖还原为基础的语法结构, 有些文章将这个过程称为 "脱糖", 也有称 "解语法糖"(Reference 2). 在 com.sun.tools.javac.main.JavaCompiler 的 compile 方法中, 有一个步骤 (compile2) 会调用它的 desugar 方法, 这个方法就是用来实现脱糖处理的.
顺便提下, 从 Java 6 开始, Java 提供了编译器 API, 使得开发者们可以更灵活地使用编译. 如果你有兴趣, 它的入口在 javax.tools.JavaCompiler 接口 (从父接口继承来) 的 run 方法. com.sun.tools.javac.API.JavacTool 是它的一个实现, 它最终会将编译委托给 com.sun.tools.javac.main.JavaCompiler 的 compile 方法.
从本文开始, 笔者将试图总结和介绍 Java 语言中的常见语法糖, 并将尽量按照由简到繁, 并将相似或相关内容放在一起的顺序来组织. 对于涉及到的关联知识, 也会稍做介绍, 但可能不会深究太多细节. 欢迎有兴趣的同学一起探讨, 如有不足, 还请指正.
这个系列的目录如下(为了不剧透, 先只列出部分):
(一): 用于字符串拼接的 "+" 运算符 https://www.atatech.org/articles/131182
(二): 条件编译, 断言语句 https://www.atatech.org/articles/131600
(三): 可变元参数, foreach 循环 https://www.atatech.org/articles/133313
注: 如果没有特别指明, 所有的介绍及示例均基于 Java 8.
用于字符串拼接的 "+" 运算符
在 Java 中, 字符串是最常使用的类型, 字符串的拼接又是字符串最常用的操作. 有些语言(如: C++) 允许程序员对运算符做重载, 从而实现定制化的操作. Java 不支持运算符重载, 但它为程序员简化了字符串拼接操作, 允许通过二元操作符 "+" 完成字符串拼接.
更进一步地, 根据 Java 语言规范,"+" 操作符的运算规则如下:
如果有一个操作数是 String, 则将另一个操作数也转为 String.
否则,(如有必要, 插入拆箱转换), 如果任意一个操作数是 double, 则将另一个转为 double.
否则, 如果任意一个操作数是 float, 则将另一个转为 float.
否则, 如果任意一个操作数是 long, 则将另一个转为 long.
否则, 将两个操作数都转为 int.
这条规则的最后一句也就是两个 byte 相加, 结果是 int 的原因.
回到正题, 先看一个字符串拼接的例子:
- String t = "b";
- String s = "a" + t + "c";
反编译这段代码, 得到:
- 0: ldc #2 // String b
- 2: astore_1
- 3: new #3 // class java/lang/StringBuilder
- 6: dup
- 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
- 10: ldc #5 // String a
- 12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- 15: aload_1
- 16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- 19: ldc #7 // String c
- 21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
- 27: astore_2
简单解释, 首先将字符串 "b" 存入第一个变量 (即: t) 中. 接着, 构造一个 StringBuilder, 并通过其 append 方法存入字符串 "a". 然后, 将第一个变量(t) 中的字符串 ("b") 取出, 并再次通过在刚才的 StringBuilder 对象上调用 append 方法存入. 接着, 以类似的方法再存入字符串 "c". 最后, 通过在 StringBuilder 对象上调用 toString 方法得到最后的结果, 存入第二个变量 (即: s) 中.
可见, 在编译过后, Java 的字符串拼接其实是通过构造 StringBuilder 对象, 并不断调用其 append 方法将字符串放入, 再通过 toString 得到最终结果. 这样的做的好处在于可以避免产生很多无用的中间字符串对象.
事实上, Java 编译器做的还不止于此, 如果字符串是常量值, 那么在编译期, Java 会直接将常量字符串替换为其字面值. 考虑如下代码:
- final String t = "b";
- String s = "a" + t + "c";
与前述例子相比, 这里仅仅是将 t 声明为了 final. 反编译后的代码如下:
- 0: ldc #2 // String b
- 2: astore_1
- 3: ldc #3 // String abc
- 5: astore_2
看, 由于所有的字符串都是常量值, Java 可以在编译阶段就直接计算出结果.
不过, 需要指出, Java 编译器也聪明得有限. 考虑下面字符串连续拼接的场景:
- String s = "a";
- s += "b";
- s += "c";
反编译后得到如下代码:
- 0: ldc #2 // String a
- 2: astore_1
- 3: new #3 // class java/lang/StringBuilder
- 6: dup
- 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
- 10: aload_1
- 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- 14: ldc #6 // String b
- 16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- 19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
- 22: astore_1
- 23: new #3 // class java/lang/StringBuilder
- 26: dup
- 27: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
- 30: aload_1
- 31: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- 34: ldc #8 // String c
- 36: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- 39: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
- 42: astore_1
可以看出, 相比之前的例子, 这个过程多了很多中间计算. 首先将字符串 "a" 存入变量 1(即: s)中. 接着, 在拼接字符串 "b" 时, 构造了一个 StringBuilder, 依次放入变量 1 中的值("a"), 字符串 "b". 然后通过 StringBuilder#toString 得到结果并再次存入变量 1 中. 此时变量 1 中存的值是 "ab". 接着, 再次构造一个 StringBuilder, 依次放入变量 1 的值("ab"), 字符串 "c". 然后又一次调用 StringBuilder#toString, 得到最终结果 "abc".
也就是说, 每执行一条拼接语句, 就会构造一个 StringBuilder, 并对之前的结果和当前要拼入的后续字符串依次调用 append 放入, 然后再用 toString 得到该语句的结果. 如果你有类似于下面这样的拼接, 那么可能会产生极大的浪费:
- String result = ...;
- List<String> strsToAppend = ...;
- for (String s : strsToAppend) {
- result += s;
- }
总结一下, 如果我们要在一条语句中进行字符串拼接, 那么可以直接使用 "+" 运算符, 这样做的代码十分简洁, 同时也具有很高的可读性. 但如果拼接涉及多条语句, 那么就需要考虑使用类似于 StringBuilder 的技术来避免或减少先创建然后又丢弃中间对象的事情发生, 以提高拼接的性能.
References
Java 语言规范(Java SE 8)
来源: https://juejin.im/post/5c74b664f265da2dcd79ed6c