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)

其中局部类在方法体内声明的内部类

匿名类在方法体内声明且未命名的内部类,即没有命名的局部类

关于匿名类,我们还必须要了解以下三点:

  1. 能够在声明一个类时,同时实例化出该类的一个对象
  2. 在只想使用一次局部类的情况下,可以使用匿名类
  3. 虽然局部类是类声明,但匿名类是表达式,这意味着我们可以在另一个表达式中使用匿名类来定义一个类,且匿名类必须是语句的一部分

对内部类、局部类和匿名类还不是很清楚的童鞋,可以直接点击我在文末给出的链接:

初始化块(代码块)

在 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

嗯!亲,不用再解释了,我完全明白为什么说“双括号初始化”其实就是组合使用了匿名类(匿名内部类)实例初始化块(构造代码块)

扩展阅读

聪明而好学的你,如果想要更详细地了解本文所涉及的内容点,可点击以下链接进行自我挖掘:

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值