Java 隐藏特性:双括号初始化(Double Brace Initialization)
引入双括号初始化
Java 中的“双括号初始化”常被人以隐藏特性的方式所提及,那何谓“双括号初始化”呢?我们又在哪里有应用到“双括号初始化”呢?
首先,我们先来观察一段典型的“双括号初始”示例代码片段:
Map<String, String> map = new HashMap<String, String>() {{
put("name", "吴仙杰");
put("englishName", "Jason Wu");
}};
嗯,等等……这种写法……怎么有种似曾相识的感觉……
没错,我们并不陌生这种写法,因为在 MyBatis 3 The SQL Builder Class 中就使用了该写法。
以下为我从 MyBatis 3 The SQL Builder Class 中摘录的代码片段:
private String selectPersonSql() {
return new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
FROM("PERSON P");
FROM("ACCOUNT A");
INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
WHERE("P.ID = A.ID");
WHERE("P.FIRST_NAME like ?");
OR();
WHERE("P.LAST_NAME like ?");
GROUP_BY("P.ID");
HAVING("P.LAST_NAME like ?");
OR();
HAVING("P.FIRST_NAME like ?");
ORDER_BY("P.ID");
ORDER_BY("P.FULL_NAME");
}}.toString();
}
理解双括号初始化
为了讲解时思路的清晰性,这里,我直接先给出“双括号初始化”的原理:Java 中所谓的“双括号初始化”其实就是利用了匿名类(匿名内部类)和实例初始化块(构造代码块)。
故如果想到完全理解“双括号初始化”,则必须要先明白什么是匿名类和实例初始化块。
内部类(Inner Class)
Java 中所谓的内部类,可以简单地理解为是定义在其它类内部中的类。
内部类又有两种额外类型,分别是局部类(Local Class,本地类)和匿名类(Anonymous Class)。
其中局部类是在方法体内声明的内部类。
而匿名类是在方法体内声明且未命名的内部类,即没有命名的局部类。
关于匿名类,我们还必须要了解以下三点:
- 能够在声明一个类时,同时实例化出该类的一个对象
- 在只想使用一次局部类的情况下,可以使用匿名类
- 虽然局部类是类声明,但匿名类是表达式,这意味着我们可以在另一个表达式中使用匿名类来定义一个类,且匿名类必须是语句的一部分
对内部类、局部类和匿名类还不是很清楚的童鞋,可以直接点击我在文末给出的链接:
初始化块(代码块)
在 Java 中一直都存在块级作用域,这与 ECMAScript 2015(ECMAScript 6、ES6)之前的 JavaScript 存在明显的不同(在 ECMAScript 2015 之前的 JS 是不存在块级作用域的)。
在 Java 中有两种初始化块(代码块)是我们比较常用的,分别是静态初始化块(静态代码块)和实例初始化块(构造代码块)。
静态初始化块的语法如下:
static {
// 这里可以写任何初始化代码
}
一个类可以有任意数量的静态初始化块,它们可以出现在类体中的任何位置。当存在多个静态初始化块时,运行时系统会保证按照它们在源代码中出现的顺序调用。
而静态初始化块语法去掉 static
关键字就是实例初始化块了:
{
// 这里可以写任何初始化代码
}
Java 编译器会将实例初始化块复制到每个构造函数中,故实例初始化块可用于在多个构造函数之间共享代码块。
本着认真负责的态度,我在这里就粗略总结下关于静态初始化块、实例初始化块和构造函数三者的比较:
- 执行顺序:静态初始化块 → 实例初始化块 → 构造函数
- 执行阶段:静态初始化块在类初始化阶段执行。实例初始化块和构造函数在对象实例化阶段执行
- 执行次数:不论实例化多少个对象,静态初始化块都只执行一次。实例化几个对象,就执行几次实例初始化块和构造函数
对静态初始化块、实例初始化块和构造函数还不是很清楚的童鞋,可以直接点击我在文末给出的链接:The Java™ Tutorials - Initializing Fields。
剖析双括号初始化
首先,还是以我们前面提到的代码为例:
package com.wuxianjiezh.test;
import java.util.HashMap;
import java.util.Map;
public class MainTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>() {{
put("name", "吴仙杰");
put("englishName", "Jason Wu");
}};
System.out.println(map);
}
}
我们通过 javac
编译上面的代码后,会生成以下两个 class 文件:
MainTest.class
MainTest$1.class
发现没?有个匿名类的 class 文件生成了!所以毫无疑问,“双括号初始化”的的确确使用了匿名类。
接下来,我们再对上面的第代码略作修改,并加上一些注释:
package com.wuxianjiezh.test;
import java.util.HashMap;
import java.util.Map;
public class MainTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>() {
// 实例初始化块
{
put("name", "吴仙杰");
put("englishName", "Jason Wu");
}
}; // 因为匿名类是表达式,故它必须是语句的一部分,所以在闭合的大括号后会有一个分号
System.out.println(map);
}
}
这次的代码与上次的代码完全一样,只是增加换行和注释,但我们发现了一点:“双括号初始化”使用了实例初始化块是没得跑了。
嗯!现在的你是否对“双括号号初始化”即是匿名类和实例初始化块的应用有一丢丢的感觉了,但是不是又有一点说不出来的迷惑呢?迷惑点是不是还是匿名类这一块呢?
我们再进一步对上面代码作个测试,当我们在 new HashMap<String, String>() {}
的大括号内按下 IntelliJ IDEA 的快捷键(Ctrl-O
),发现了什么?咦,我们可以重写 HashMap
中的方法哎!这不就是说,这个大括号继承了类(针对上面的例子,因为 HashMap
是一个类)或实现了接口(如果我们 new
的是一个接口,如 Map
)嘛!
嗯,很好!那如果我们如下重写 HashMap#put
方法,那此时的 HashMap
还是我们所想当然的 HashMap
吗?
package com.wuxianjiezh.test;
import java.util.HashMap;
import java.util.Map;
public class MainTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>() {
// 实例初始化块
{
put("name", "吴仙杰");
put("englishName", "Jason Wu");
}
@Override
public String put(String key, String value) {
return "";
}
}; // 因为匿名类是表达式,故它必须是语句的一部分,所以在闭合的大括号后会有一个分号
System.out.println(map);
}
}
针对上述的代码,显然我们的 HashMap
已经不再是我们所想要的那个 HashMap
了。
OK!现在我们知道了“双括号初始化”确实是使用了匿名类,并且这个匿名类还继承或实现了 new
后面的对象或接口。接下来,我们再进一步改造得到如下代码:
package com.wuxianjiezh.test;
import java.util.HashMap;
import java.util.Map;
public class MainTest {
public static void main(String[] args) {
// 局部类
class LocalClass extends HashMap<String, String> {
// 实例初始化块
{
put("name", "吴仙杰");
put("englishName", "Jason Wu");
}
}
Map<String, String> map = new LocalClass();
System.out.println(map);
}
}
我们通过 javac
编译上面的代码后,会生成以下两个 class 文件:
MainTest.class
MainTest$1LocalClass.class
嗯!亲,不用再解释了,我完全明白为什么说“双括号初始化”其实就是组合使用了匿名类(匿名内部类)和实例初始化块(构造代码块)。
扩展阅读
聪明而好学的你,如果想要更详细地了解本文所涉及的内容点,可点击以下链接进行自我挖掘: