虽然今天遇到的不是一个有什么技术含量的问题,但是国内做这块的人太少了,觉得还是需要多分享分享,至少可以帮助新人走一些弯路,^_^

前几天手贱升级了 macOS Sierra,本来看到 Intellij IDEA 在 macOS Sierra 下面只有一个触摸板异常灵活的问题,觉得自己触摸板用地比较少就直接升级了,哪知道升级以后,在开发我们自己的 Intellij IDEA 插件的时候,启动的 IDEA 一会儿就出现了 Crash 的问题(Crash 的是通过 Intellij IDEA 启动起来的用来测试插件的 Intellij IDEA)。具体的 Crash 截图如下:

intellij_idea_crash.png

本来想是不是要直接回到 OS X Yosemite,但是不甘心啊,觉得既然都已经升级了,那遇到问题就解决吧,幸好在 Jetbrains 官方的问题跟踪平台上看到了有了提了类似的问题:https://youtrack.jetbrains.com/oauth?state=%2Fissue%2FJRE-3

看这个帖子的意思是这个问题是 OpenJDK 的 Bug,于是顺藤摸瓜找到了 Jetbrains 在自己维护的 JDK 上对这个问题 Fix 的 Commit:

https://github.com/JetBrains/jdk8u_jdk/commit/02f9a5fbb4924ff67c8a04c15e490acfcc750003

如果把运行插件 SDK 的 JDK 换成 Jetbrains 自己的 JDK,应该就可以解决问题。当然,要使用 Jetbrains 自家的 JDK,不用拿着源代码自己 Build,可以直接从这里下载对应的 Build:https://bintray.com/jetbrains/intellij-jdk

下载过来以后在插件工程的「Project Structure」配置界面进行如下配置即可:

  1. 将下载过来的 JDK 增加到 SDK 里面。
  2. 找到当前正在用来运行插件的插件 SDK,将其以依赖的 JDK 修改成刚刚增加的 JDK。

至此问题就已经解决,在这里建议大家如果要开发 Intellij IDEA 的插件的话,还是用 Jetbrains 自家的 JDK 比较好,毕竟 Jetbrains 已经在 OpenJDK 的基础上 Fix 了不少的问题,特别是很多和 Swing 相关的问题,使用他们的 JDK 可以帮助我们少走不少弯路。


PS:如果有人对开发工具、插件感兴趣,欢迎留言联系我,蚂蚁金服需要最优秀的工程师来做研发工具,提升工程师的效率。

问题

今天在排查一个线上的问题,线上的一个应用在初始化一个类的静态字段的时候出现了 NoClassDefFoundError,并且在导致 NoClassDefFoundError 出现的根本原因消失后,后续再次尝试初始化这个类的时候,持续出现了 NoClassDefFoundError

于是怀疑 JVM 是不是对一个类的 NoClassDefFoundError 做了缓存,在第一次加载这个类出现 NoClassDefFoundError 以后,后续再尝试加载就直接抛出 NoClassDefFoundError

实验

为了证实自己的猜想,尝试设计了一个简单的实验,一个涉及三个类

1
2
3
public class Test1 {
    static Test2 test2 = new Test2();
}
1
2
public class Test2 {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
    public static void main(String... args) throws Exception {
        while(true) {
            System.out.println("================================");
            try {
                new Test1(); // 尝试实例化 Test1,触发 NoClassDefFoundError
            } catch (Throwable e) {
                e.printStackTrace();

                try {
                    Test.class.getClassLoader().loadClass("Test2"); // 尝试加载 Test2,用于证实当将 
                                                                    // Test2.class 拷贝到 ClassPath 下的时候,
                                                                    // Test2 就可以加载到了。
                } catch (Throwable ex) {
                    ex.printStackTrace();
                }
            }
            Thread.sleep(3000);
        }
    }
}

上述类的的作用是:Test2 是一个空的类,Test1 里面有一个 Test2 的静态成员。Test 是程序的主入口,在一个无限循环内部,不断地尝试去实例化 Test1,并且在加载 Test1 出现异常的时候,尝试加载一下 Test2。

实验的步骤是:

  1. 编译以上类,运行 javac Test.java
  2. 将生成出的 Test2.class 重命名成 Test2.class.bak
  3. 运行 java Test,这个时候程序去加载 Test1 的时候,就会出现 NoClassDefFoundError,并且在尝试加载 Test2 的时候,会出现 ClassNotFoundException
  4. 将第二步重命名的 Test2.class.bak 该回成 Test2.class,这个时候程序去加载 Test1 的时候,就会出现 NoClassDefFoundError,在加载 Test2 的时候,不会出现 ClassNotFoundException

实验的第二步的目的是为了程序在加载 Test1 的时候因为找不到 Test2 出现 NoClassDefFoundError,第四步是为了和第二步做对照,说明在后续程序可以加载到 Test2 的时候,在实例化 Test1 的时候,依旧出现 NoClassDefFoundError

在我的机器上,按照上面的方式去操作,结果如下:

NoClassDefFoundError

结果正如预期,即使在后面 Test2 在 ClassPath 下的时候,NoClassDefFoundError 依旧出现,所以 JVM 里面肯定有地方对 NoClassDefFoundError 做了缓存。

JVM 里面的实现

带着这个疑问,请教了部门里面的 JVM 专家,这个猜测得到了证实,并且他给出了 JVM 内部具体处理这段逻辑的代码,处理的代码在 JDK 的 instanceKlass.cpp 这个文件里面:

1
2
3
4
5
6
7
8
9
10
11
12
bool instanceKlass::link_class_impl(
     instanceKlassHandle this_oop, bool throw_verifyerror, TRAPS) {
   // check for error state
   if (this_oop->is_in_error_state()) {
     ResourceMark rm(THREAD);
     THROW_MSG_(vmSymbols::java_lang_NoClassDefFoundError(),
                this_oop->external_name(), false);
   }
   // return if already verified
   if (this_oop->is_linked()) {
     return true;
   }

并且在 instanceClass.hpp 这个文件中,定义了类的 _init_state,其中,is_in_error_state 这个方法的定义如下:

1
bool is_in_error_state() const           { return _init_state == initialization_error; }

^M,神奇的字符!相信很多人写 Shell 脚本的时候都被这个字符坑过,我自己也至少被坑过两次。最近周围的好几个小伙伴又被 ^M 坑,花了好几个小时检查脚本的错误,结果发现是 ^M 导致的。所以写了这篇文章讲一下什么是 ^M,当 ^M 出现的时候一般会伴随着什么样的现象,出现了我们可以用什么手段去解决。

^M 是何方神圣

这个得先从 Windows 和 Unix 下的换行符开始说起,在我的 Intellij IDEA 的右下方的状态栏上,有一块是展示当前文件的换行符的:

Windows 和 Unix 下的文件换行符

可以看到在 Windows 下,换行符是 \r\n,在 Unix 下换行符是 \n。如果我们用把一个文件的换行符换成 Windows 的换行符,那么当我们用 cat -v 来看的时候,就可以看到:

cat -v 查看文件是否含有 ^M

实际上 ^M 就是 Windows 下的换行符中的 \r 部分。因为 Unix 下的换行符是 \n,所以当一个用 Windows 下的换行符的文件放在 Unix 下的时候,单行的最后一个字符就变成了 \r\r 在 ASCII 码中是 0xD,而 0xD 在 VIM 和 cat -v 则刚好被显示为 ^M

刚才之所以用 cat -v 而不用普通的 cat 是因为 ^M 是不可见的字符,如果仅仅用 cat,是看不到这个字符的。cat-v 参数的作用就是显示不可打印的字符。

^M 会导致什么样的问题?

我们已经知道了 ^M 实际上就是 \r,而 \r 是回车符(Carriage Return),回车符的作用是将设备的位置重置到当前行的开头

知道了 \r 的作用时候,我们来看一个现象:

1
2
3
4
5
6
7
### 有一个普通文件,存放了一个路径,当前行的最后以 ^M 结尾
$ cat -v Main
/home/admin/khotyn.huangt/test/^M

### Echo 一下,神奇了!
$ echo "`cat Main`/where am i"
/where am i/khotyn.huangt/test/

看到后面那个 echo 命令,它将 Main 文件中的内容提取出来,再在后面加上 /where am i 这个字符串,结果我们看到,/where am i 在打印的结果中跑到最前面去了,这正是 \r 这个字符的作用,因为 cat Main 的执行的结果的最后一个字符是 \r,所以一遇到这个字符,设备指针就直接回到了当前行的开头,所以 \r 后面的 /where am i 就直接显示在了最前面。

所以,当你看到什么奇怪的路径,这个路径中莫名其妙地少了一些字符,出现了一些莫名其妙的字符串的,很可能就是 ^M 导致的。

如何逃离 ^M 的魔掌

当你发现了 ^M 导致的问题的时候,最直截了当的方式就是将 ^M 从文件中去掉。

一、临时解决的几个方法

如果的机器上安装有 dos2unix,那么恭喜你,直接运行

1
dos2unix /path/to/file

就可以将一个文件的换行符从 Windows 的转换成 Unix 的。

但是,如果机器上没有装 dos2unix,而你又没法装上去(在一家公司工作总是会有各种各样的让你感觉很丧气的权限控制),那么你可以用 sed 来替换:

1
sed --in-place='' 's/^M//g' /path/to/file

注意:上面的 ^M 只是显示的效果,输入的时候需要用组合键输入,先 Ctrl + V,然后马上 Ctrl + M 就可以在终端中输入 ^M 了。

当然,用 tr 之类的命令也可以,不过我一般用 sed 的原因是 sed 加上 --in-place 参数可以做到直接替换原文件,而不用产生临时的文件(危险而高效的操作)。

二、预防此问题

不过,前面说的只是当出现问题的时候如何解决,那么如何预防这个问题呢?

第一个方法当然是直接放大招,换个 Mac 啥的,或者把你的机器上的 Windows 格了,装个 Ubuntu 也好啊。真心觉得 Windows 对于程序员来说真的没有啥好处(我好想听说连微软都开源 .Net 了,并且会提供多平台的支持)。

第二个方法嘛,当在 Windows 下使用各种编辑器的时候,尽量将换行符设置成 Unix 的换行符。不要偷懒用 Windows 的换行符,出现了问题就是好几个小时的排查时间。(目前没有发现有什么场景下有必须用到 Windows 下的换行符的,如果有同学知道有这样的场景的话,不吝赐教)。

三、防止被别人坑

虽然我个人觉得不应该用 Windows,不过还是有同学的确是喜欢用,或者因为不可抗拒的因素而暂时在使用,为了防止出现这个问题,可以在版本管理软件上做控制,比如 Git 就可以设置换行符,当你提交文件的时候,可以将你的所有文本的换行符替换成你设定的换行符,详细可以看 https://help.github.com/articles/dealing-with-line-endings/

所有的这些功能都是在 Intellij IDEA 14 中测试的,其他的版本不一定适用

打开类的直接定位到某一行

在 Mac 下,IDEA 默认的打开类的快捷键是 Command+O,不过这个快捷键也有一些技巧。

第一个是可以在打开类的时候直接跳到某一行,比如下面这样:

打开 String 这个类的同时直接跳转到 String 的第 40 行。

到某个类的某个方法

IDEA 的 Open Symbol 功能可以直接定位到某一个类的某一个方法,默认的快捷键是 Option+Command+O,如下:

像 Sublime 那样多行编辑

以前要做多行编辑,总是现在 Sublime 里面先做好,然后再拷贝回到 IDEA 里面,现在知道了 IDEA 本身就自带这个功能,快捷键是 Option+Shift+鼠标,直接来看一个 gif 动画看来这个功能吧:

Smart Code Completion

除了普通的代码补全的功能之外,IDEA 还提供了智能的不全功能,我们看下对比:

下面是基本的补全功能:

这个是智能的补全功能:

可以看到智能补全可以直接推断类型,把不符合类型的提示直接全部过滤,让我们可以更加高效地编写代码。

草稿

工作的时候我们经常会创建一些临时文件,在 IDEA 14 中,加入了一个非常有用的创建草稿的功能,Mac 下的快捷键是 Command+Shift+N,你可以在一个工程里面随意创建任意数量的草稿。

上面的这些是前几天参加 QCon 的时候听 IDEA 的一个 Session 知道的一些技巧。个人认为这个 Session 比很多其他的 Session 都更加有料

「多看阅读」一直是我最喜欢的阅读器,没有之一,在多看阅读上买的书也不少了,绝佳的用户体验,「多看阅读」甩出豆瓣阅读、唐茶等 N 条街。

不过,今天在多看阅读上看书的时候,却弹出了这样的东西:

多看阅读弹窗

多看在读者阅读书籍的中间弹出了这样的东西,并且不止弹出了一次,过个几分钟又弹出一次。我觉得多看这种极度不尊重读者的做法与被小米收购有着莫大的关系,小米一直以来给我的印象就是一个营销的公司,而不是一个认真做产品的公司,多看到了小米手里已经沦落成这份模样了,靠这样的弹窗来增加用户量,我决定在看完目前的这本书以后,就卸载多看阅读,以后看书还是买个 Kindle 省事。

Guice 的初学者在使用 Guice 往一个类中注入一个集合注入的时候,肯定有感觉到非常地不自然(这里的不自然我觉得一定程度上是不符合 Guice 给人的初印象),由于最近在项目中也在使用 Guice,所以在这里对 Guice 的集合注入做一个记录。

一、使用 Guice 的扩展 guice-multibindings

Guice 的文档上关于 Guice 注入的最简单的例子应该就是:

1
bind(Interface.java).to(Implementation.java);

我们希望在使用 Guice 做集合注入的时候肯定也是希望使用类似的 API 做注入,不过可惜的是,Guice 的核心里面并没有提供类似的 API 让我们可以使用来注入集合。

所幸的是,Guice 提供了一个扩展的包 guice-multibindings 使用和 Guice 最原始的 API 类似的方式来做注入。

需要使用这个扩展的包,使用 Maven 的话,可以在项目中加入如下的依赖:

1
2
3
4
5
<dependency>
    <groupId>com.google.inject.extensions</groupId>
    <artifactId>guice-multibindings</artifactId>
    <version>3.0</version>
</dependency>

guice-multibindings 主要使用了两种方式来注入,一种是注入一个 Set:

1
2
Multibinder<CheckHandler> checkAdapter = Multibinder.newSetBinder(binder(), CheckHandler.class);
checkAdapter.addBinding().to(InstalledCheckHandler.class);

首先创建一个 checkAdapter,然后往这个 Multibinder 中,我们可以添加任意多的 CheckHandler 的实现。

另一种方式是注入一个 Map:

1
2
MapBinder<String, CheckHandler> mapBinder = MapBinder.newMapBinder(binder(), String.class, CheckHandler.class);
mapBinder.addBinding("Hello").to(InstalledCheckHandler.class);

Map 的注入方式和 Set 的注入方式非常类似。不过奇怪的一点是,Guice 并没有提供注入 List 的方法,这是值得思考的一点

二、使用 @Provides 来注入

看了第一种方法,我们可以看到,上面的这种方法并不能注入一个 List,不过,我们还是有办法来注入一个 List 的,就是使用一个 @Provides 注解,比如在我们的 Guice Module 的类里面加入一下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Provides
List<BindingAdapter> provideBindingAdapter() {
    List<BindingAdapter> bindingAdapters = new ArrayList<BindingAdapter>();
    List<OsgiServiceHolder<BindingAdapter>> bindingAdapterHolders = OsgiFrameworkUtils
        .getServices(bundleContext, BindingAdapter.class);

    for (OsgiServiceHolder<BindingAdapter> bindingAdapterHolder : bindingAdapterHolders) {
        final BindingAdapter bindingAdapter = bindingAdapterHolder.getService();

        if (bindingAdapter != null) {
            bindingAdapters.add(bindingAdapter);
        }
    }

    return bindingAdapters;

}

这样,我们就可以从其他的地方拿到对应的类的实例,然后放到一个 List 中,通过 Guice 注入给其他的类了。

上面的这两种方式其实各有优劣。一般情况下,我觉得选择第一种就可以了,毕竟,第一种方法的类的实例是由 Guice 来生成的。选择第二种方式的场景我觉得可能有:

  • 类的实例是从其他的地方来的,比如上面的例子中,是从 OSGi 来的。
  • 简单类型的类,比如一个 String 的 List。

最近在写框架的测试代码的时候,有需求要对 Log4j 的输出进行测试(依赖 Log4j 的输出来进行测试,这一点本身可能得深思一下),之前也有对 stdout 和 stderr 进行测试,用了一个叫做 system-rule 的包:

1
2
3
4
5
<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-rules</artifactId>
    <version>1.5.0</version>
</dependency>

利用这个包中类,只需要在测试用例中加上一个 JUnit Rule,就可以获取到 stdout 和 stderr 中的内容,然后对其进行测试。现在我也想对 Log4j 的输出采用类似的方式进行测试,于是扩展了 JUnit 的 Rule,就有了以下这一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
 * 此 Rule 用于对 Log4j 进行测试
 *
 * @author khotyn 8/14/14 9:18 PM
 */
public class Log4jRule extends ExternalResource {
    private String       logName;
    private List<String> loggerMessages = new ArrayList<String>();

    public Log4jRule(String loggerName) {
        this.logName = loggerName;
    }

    public Log4jRule(Class className) {
        this.logName = className.getName();
    }

    public List<String> getLoggerMessages() {
        return loggerMessages;
    }

    public String getLogMessageAsString() {
        String result = "";

        for (String loggerMessage : loggerMessages) {
            result += loggerMessage;
            result += "\n";
        }

        return result;
    }

    @Override
    protected void before() throws Throwable {
        Logger logger = LogManager.getLogger(logName);
        logger.addAppender(new AppenderSkeleton() {
            @Override
            protected void append(LoggingEvent event) {
                loggerMessages.add(event.getMessage().toString());
            }

            @Override
            public void close() {

            }

            @Override
            public boolean requiresLayout() {
                return false;
            }
        });
    }
}

整段代码非常简单,继承了 JUnit 的 ExternalResource 类,然后在 before 方法中,给对应的 Logger 加上了一个 Appender,在 Appender 中,将日志内容收集到一个 List 中,然后拿到这个 List 就可以拿到日志的输出了。

使用的时候非常简单:

1
2
3
4
5
6
7
8
9
10
11
public class SampleTest {
    @Rule
    public Log4jRule log4jRule = new Log4jRule(SampleTest.class);

    @Test
    public void test() {
      	// .........
        Assert.assertTrue(log4jRule.getLogMessageAsString().contains(
            "Hello, world"));
    }
}

通过

1
log4jRule.getLogMessageAsString()

可以拿到一个 String 格式的日志输出,或者通过:

1
log4jRule.getLoggerMessages()

来获得一个日志输出的内容的 List,List 中的没一行就是日志中的一行

最近在开发公司的集成测试框架,有一些关于单元测试的体会,写一个博客记录一下想法。

为什么要写单元测试?

这个问题,已经有无数的关于技术的书、文章去阐述了,不断地强调单元测试的重要性。比如单元测试可以让你在软件开发的早期阶段发现 Bug,而不必到集成测试的时候才发现等等。不过,对于我来说,在切实地戳到我的痛点之前,我一直都没有去重视这些关于单元测试的忠告(虽然在心中记着,但是实际上并不是很在意)。

在写公司的集成测试框架的时候,有那么好几次,在调整了现有的功能,或者修复了某个 Bug 之后,因为懒惰,也因为跑一次完整的单元测试所需要的耗时较长,我侥幸地认为这些修改应该没有问题,直接打包交付。结果是,墨菲定律出现了,果然,没有经过测试的修改引发了新的 Bug,我不得不重新修改代码,然后厚着脸皮让用户重新试一次。

人总是不靠谱的,我们懒惰,我们存在侥幸心理,坏事儿总是在我们最不希望发生的时候发生。写单元测试不能防止我们懒惰,防止我们存在侥幸心理。但是一次成本低廉的单元测试会让我们觉得:“反正运行一次但单元测试不会耗费很多时间,不如跑一次吧”,它在一定程度上降低我们犯错的几率。

单元测试对于重构的意义也非常重大。很多有意思的程序员都有洁癖,会想着去修改某一段「恶心」的代码。我有过这样几次经历,在把一段「恶心」的代码修改地赏心悦目后,最后上线后发现引入了一个 Bug,心里暗骂一句 WTF,然后一脸黑线地把 Bug 修改了,想想如果当时有单元测试,那么会给我多大的勇气,让我可以肆无忌惮地去重构代码,这是多么爽的一件事情。

好的单元测试应该是怎样的?

一个好的单元测试,我觉得最重要的一点就是运行成本得低,也就是说一个单元测试越快越好。运行一次单元测试的成本越低,你才会越愿意去运行单元测试。如果运行一次单元测试得 10 分钟 20 分钟,那么我想很多人的侥幸心理又会出来了。

我是怎样写单元测试的?

我一般是这样写单元测试的,先想清楚模块的边界,有哪几种可能的输入,这些输入对应的可能输出是什么,然后以最快的速度堆积代码把功能先实现出来,接着写单元测试,把测试用例全部跑过。接下来马上着手重构之前写的代码,不断重构,不断地跑单元测试,知道重构后的代码让自己满意为止。

背景

Tomcat 作为 Servlet 规范的实现者,它在应用启动的时候会扫描 Jar 包里面的 .tld 文件,加载里面定义的标签库,但是,我们在开发的时候很多都不是采用 JSP 作为 Web 页面的模板的,很多都是使用 Velocity 之类的模板引擎,自然而然,为了加快应用的启动速度,我们可以把 Tomcat 里面的这个功能给关掉。

方法

看 Tomcat 的配置文档,关于 Context 的设置这一块,看到了 processTlds 这个属性可以设置,看下这个属性的说明:

Whether the context should process TLDs on startup. The default is true. The false setting is intended for special cases that know in advance TLDs are not part of the webapp.

只要在 Context 中把这个属性设置成 false,那么我们就可以关闭 Tomcat 的 TLD 扫描功能了,为了让所有的应用都可以关闭这个功能,我们可以将 Tomcat 目录下的 conf/context.xml 修改成如下这样:

1
2
3
4
<?xml version='1.0' encoding='utf-8'?>
<Context processTlds="false">
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
</Context>

但是,在 Tomcat 6 中测试的时候,发现这个功能没有生效,无奈只能 Debug Tomcat 的源码,发现 StandardContext 的 init 方法下有如下代码:

1
2
3
4
5
6
7
8
if (processTlds) {
    this.addLifecycleListener(new TldConfig());
}

super.init();

// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(INIT_EVENT, null);

这里需要说明的一点是,我们的默认的 context 配置是在 lifecycle.fireLifecycleEvent(INIT_EVENT, null); 这行代码中被处理的,而在这行代码之前,Tomcat 就已经使用了 processTlds,我们的配置完全没有生效。

Workaround

那么,这么解决呢?在 context 中,我们还可以配置一个 JarScanner,这个 JarScanner 会被用来扫描 Jar 包中的 tld 文件,我们可以在默认的 context.xml 中配置一个空的 JarScanner,像下面这样:

1
2
3
4
<?xml version='1.0' encoding='utf-8'?>
<Context processTlds="false">
    <JarScanner className="com.alipay.sofa.runtime.test.patch.tomcat.NullJarScanner"/>
</Context>

NullJarScanner 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.alipay.sofa.runtime.test.patch.tomcat;

import org.apache.tomcat.JarScanner;
import org.apache.tomcat.JarScannerCallback;

import javax.servlet.ServletContext;
import java.util.Set;

/**
 * @author khotyn 14-1-21 下午4:37
 */
public class NullJarScanner implements JarScanner {
    @Override
    public void scan(ServletContext context, ClassLoader classloader, JarScannerCallback callback, Set<String> jarsToSkip) {
        // Do nothing at all.
    }
}

需要注意的是,Tomcat 7 不会出现上述的问题,你只要在配置中把 processTlds 设置成 false 即可。

如果进度正常,新版本的 Java,Java 8 将在三月份发布,Java 开发人员期待已久的 lambda 也将在 Java 8 中得到支持。目前,Java 8 的早期版本已经可以在 Java 的网站上下载到了,Intellij IDEA 也已经在其最新的版本支持了 Java 8。所以,最近花了点时间了解了一下 Java 8 中新增加的一些特性。

由于 lambda 的引入,Java 8 对原来的集合类做了大幅的更新,让集合操作可以支持 lambda 表达式。在看新的的集合类的代码的时候,发现了 java 8 似乎增加了一个新的方法描述符,比如在 java.lang.Iterable 里面就新加入了下面这个方法:

1
2
3
4
5
6
default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

在方法的最前面,是一个 default 描述符。等等,Iterable 不是个接口吗,怎么有具体的实现代码了?

这个 default 就是在 java 8 中新引入的,它可以让你的接口有一个默认的实现,接口的实现类可以不用去实现 default method,比如,下面这段代码,是可以正常编译通过的:

1
2
3
4
5
6
7
8
9
class Impl implements A {

}

interface A {
    default String foo() {
        return "A";
    }
}

引入 default 的带来的一个好处就是在现有的接口上增加方法而不用让其实现修改代码,通过这种机制,Java 8 可以通过平滑的方式在原有的 Java 的 API 上引入 lambda 的支持。

那么,如果一个类实现了两个接口,这两个接口里面有方法签名相同的 default method,那运行的时候到底会选择哪一个?答案是编译不通过,如果出现这种情况,实现类必须实现 default method,以消除歧义,比如下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MultiImpl implements A, B {

    /**
     * 由于 A,B 中都有 String foo() 接口,不知道要调用哪个,所以实现类必须实现一下
     *
     * @return
     */
    @Override
    public String foo() {
        return "C";
    }
}

interface A {
    default String foo() {
        return "A";
    }
}

interface B {
    default String foo() {
        return "B";
    }
}

当然,在的实现类中,也可以直接调用某个接口的 default method:

1
2
3
4
5
6
class MultiImplInvokeSuper implements A, B {
    @Override
    public String foo() {
        return B.super.foo();
    }
}