Java 8 Lambda表达式与双冒号语法

3,506 阅读11分钟

前言

最近在学习一些关于响应式编程方面的内容,而在响应式编程中,响应式流(Reactive Stream,Java 9+)发挥了重要作用,在操作的过程中,我发现其使用了大量的 Lambda 表达式及双冒号语法,这两个特性是 Java 8 出现并应用的,以前我也有过了解与使用,因此在这里对这两块内容进行梳理巩固并加以总结。


正文

匿名类

想要真正去理解 Lambda 表达式,我认为应该先从匿名类开始说起。

遥记那年冬天的第一场雪,比以往来得更晚一些……一帮初出茅庐、仍未褪去稚气的热血少年,正襟危坐的围坐在舒适温暖的教室里,用一双双渴望知识的眼神,注视着老师在黑板上郑重写下的四个大字——Java。不错,我们的 Java 学习之旅就此拉开了帷幕……

咳,扯远了、扯远了,闲话少说,圆规正转!!

在刚学习 Java 课程时,老师给的期末课程设计题目就是设计一款 Java GUI 应用,传送门:blog.csdn.net/weixin_4365…

现在想想仍是头大,不过还好在那时头比较铁,硬着头皮给磨了出来,虽然挺垃圾,但那也是我第一次敲代码搞到凌晨三点钟,一下子就把优秀程序猿必备的特质给抓的死死的,祸兮福所倚、福兮祸所伏,果不其然,第二天我就多掉了两根头发,使本就不富裕的脑袋瓜子又雪上加霜……

好像又扯远了。。。

在 Java GUI 应用中,存在多种监听器,如:ActionListener、KeyListener、MouseListener 等,它们都是以接口的形式存在,实现它们的类就是一个发挥具体作用的监听器。

以 ActionListener 接口为例,它只存在一个需要重写的方法:

public interface ActionListener extends EventListener {
    public void actionPerformed(ActionEvent e);
}

设想这样一个场景,我们设计了一个登录的 JFrame 窗体(就是 B/S 中常说的登录表单),当我们输入账号密码后,点击登录按钮就会把身份信息提交给系统,由系统判断身份的合法性,从而提示登录成功或失败。

与 B/S 系统中提交登录表单思想一样,我们需要在点击登录按钮时,触发一个事件,这个事件的处理逻辑应为:获取用户输入的账号密码,提交给系统进行身份验证。

此时我们就要用 Java GUI 中的监听器来完成,以实现 ActionListener 的监听器为例,我们只需按上述处理逻辑重写其接口方法,最后将这个实现的监听器绑定到登录按钮上就可以了。

Talk is 糊里糊涂,Show you code:

btn_login.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        String username = txt_tel.getText(); // 获取账号
        String password = txt_pwd.getText(); // 获取密码
        if (validateUser(username, password)) {
            // …… 如果用户身份正确,登录成功
        } else {
            // …… 如果用户身份错误,登录失败
        }
    }
});

其中 btn_login.addActionListener(...) 就是为登录按钮绑定一个以匿名类方式实现的 ActionListener 监听器,并编写了登录时的处理逻辑。

好了,我们来分析一下这个匿名类,它并不像我们平常所看到的使用 class 等关键词定义的类,而是直接 new 出来的,其实如果我们换一种编写方式,就会好理解很多。首先实现一个监听器,实现登录处理逻辑:

class LoginListener implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        // …… 与上面一致的登录逻辑
    }
}

之后我们把这个监听器对象绑定到登录按钮:

btn_login.addActionListener(new LoginListener())

这种方式与上面匿名类所实现的功能完全一致。

经过这两种实现方式的对比,我们可以发现,匿名类其实就是某类的实现子类去掉其声明头,只保留方法体。有些拗口,大家自己捋捋~

可能在刚接触匿名类的时候会有些许的不适应,但到后面用熟练了,那可是真香,为啥呢?

很明显的一个优点是使用匿名类就不必再去使用 classimplements 等关键词去定义新的类(而且也不用绞尽脑汁、脚趾抠地,去想新类的名字了),而是直接使用 new + 父类类名 的方式实现相应的处理逻辑,简单快捷,岂不美哉!!!(说是这么说,如果真碰到有复杂处理逻辑的匿名类,还是得去单独创建一个子类,使结构更加清晰)。

就这样,我算是入了匿名类的坑,以至于到后面遇到了更多的可以使用匿名类的一些场景,比如自定义一个排序比较器:

List<String> itemList = new ArrayList<>();
itemList.add("Eric"); itemList.add("CoderGeshu");
itemList.sort(new Comparator<String>() { // 进行排序
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

上述代码表示将 itemList 中的元素按照字符串长度进行排序。

这样看来,使用匿名类的方式是不是已经很简便了?

但是呢,有些人就总是想更懒一点,能少写几行代码就少写几行代码,能少用几个字母就少用几个字母(我可是勤快滴很),因此,直至 Java 8,Java Lambda 表达式诞生了……


Lambda 表达式

在 Java 8 中,出现了这样的一个注解:@FunctionalInterface,翻译成中文为函数式接口,在此接口的官方介绍中,提到:

Conceptually, a functional interface has exactly one abstract method.
……
Note that instances of functional interfaces can be created with lambda expressions, method references, or constructor references.

一个函数式接口只有一个抽象方法,并说明了其实例可以用 Lambda 表达式、方法引用或构造函数引用来创建。

所以自 Java 8 开始,只要带有 @FunctionalInterface 注解的接口,就是仅有单个抽象方法的函数式接口,可以使用 Lambda 表达式来创建相应实例。(提一嘴:仅有单个抽象方法的接口并不等同于接口中只包含一个方法哦~)

那么 Lambda 表达式到底是个什么东东?它又是怎样的用法儿?

我们先来看两个例子,首先还是上面的登录场景,这次我们使用 Lambda 表达式来进行改写:

btn_login.addActionListener((ActionEvent e) -> {
    String username = txt_tel.getText();
    String password = new String(txt_password.getPassword());
    if (validateUser(username, password)) {
        // …… 如果用户身份正确,登录成功
    } else {
        // …… 如果用户身份错误,登录失败
    }
});

这次更加简洁,直接把 new 关键字、类名、方法名等都去掉了,只保留了 (参数) -> { 方法体 } 这样的代码段。

那我们再来改写一下上面的排序器看看:

List<String> itemList = new ArrayList<>();
itemList.add("Eric"); itemList.add("CoderGeshu");
itemList.sort((s1, s2) -> s1.length() - s2.length()); // 排序

我直接 WTF ??这是个啥东西??这样就能把上面那种排序功能给实现了??

使用这种方式运行了一遍,还真他娘的管用,终究是我道行太浅了。

既然不懂,那就学呗,经过一阵叮咚哐啷地捣鼓,总算是掌握了关于 Java Lambda 表达式的使用方法,并且最终我果然喜新厌旧,抛弃了伴我良久的匿名类。。。

所以,就让我来简单介绍一下 Java Lambda 表达式的几种用法吧。

首先要说的是 Lambda 表达式的标准形式:(参数类型 参数名称) ‐> { 方法体 }

只有参数列表和方法体,中间再用 -> 指向连接,不过这里有几点说明:

  1. 如果小括号里没有参数就留空 (),如果存在多个参数就用逗号分隔;
  2. -> 是 Java 8 Lambda 的语法格式;
  3. 大括号内是编写方法体的地方,与传统方法体基本一致。

大家可以看到在上面的例子中,我写的形式与标准形式多多少少还是存在些不同,这是因为在 Lambda 标准形式的基础上,我们还可以省略部分内容,这些内容可以由 Lambda 表达式从上下文自行推断:

  1. 参数:小括号内的参数类型可以省略,如果小括号中只有一个参数,那么连小括号也可以省略;
  2. 方法体:如果大括号内的方法体只有一条语句,无论是否有返回值,都可以省略大括号、return 关键字及语句分号。

好了,了解了这些使用方法,我们就带着它们应用在具体例子上来看一看。

情况一:接口不含参数、无返回值

假如我们需要有一个显示器,来显示相应的信息。首先我们创建一个接口:

@FunctionalInterface
public interface Displayer {
    void display();
}

当前接口只有一个无参无返回值的抽象方法,所以当我们使用 @FunctionalInterface 注解进行标记时 IDE 不会报错,如果接口里存在的抽象方法不唯一,就会编译报错。

当我们想要使用此接口显示信息时,按照传统的方法,首先想到的就是先创建一个具体实现类,然后调用实现类中的 display() 方法:

class DisplayImpl implements Displayer {
    @Override
    public void display() {
        System.out.println("I'm CoderGeshu");
    }
}

public class Test {
    public static void main(String[] args) {
        Displayer displayer = new DisplayImpl();
        displayer.display();
    }
}

输出结果:

I'm CoderGeshu

但是如果 DisplayImpl 类只是为了实现 Display 接口而存在,并且只被使用了一次,那么就应该使用匿名内部类来简化这一操作:

public class Test {
    public static void main(String[] args) {
        Displayer displayer = new Displayer() {
            @Override
            public void display() {
                System.out.println("I'm CoderGeshu");
            }
        };
        displayer.display();
    }
}

上面都是在 Java 支持 Lambda 表达式前的做法,如果使用的是 Java 8+,我们就可以使用 Lambda 表达式进一步简化,并且根据上面 Lambda 的标准形式以及省略表达(无参无返回值),我们可以这样编写:

public class Test {
    public static void main(String[] args) {
        // 不含参数,所以使用(),方法体中只有一条语句,所以省略大括号
        Displayer displayer = () -> System.out.println("I'm CoderGeshu");
        displayer.display();
    }
}

一行代码就搞定了上面几行代码所完成的功能。

当然这也是最简洁最理想的情况,是应用 Lambda 的意义所在,就如是选择实现类还是选择匿名类一样,如果你在 Lambda 表达式里处理很复杂的逻辑操作,那倒还不如老老实实的创建一个具体类,否则就会导致程序的可读性大大降低。

情况二:接口中含有参数

向接口中增加 info 参数:

@FunctionalInterface
public interface Displayer {
    void display(String info); 
}

标准形式的 Lambda 表达式为:

Displayer displayer = (String e) -> { System.out.println(e); };
displayer.display("I'm CoderGeshu");

但是因为只有一个参数,所以可省略小括号,由于方法体中只有一条语句,又可以省略大括号,所以简化后的表达式:

Displayer displayer = e -> System.out.println(e);
displayer.display("I'm CoderGeshu");

情况三:接口方法含有返回值

@FunctionalInterface
public interface Displayer {
    int infoLength(String info); // 增加返回值
}

其 Lambda 表达式:

Displayer displayer = (e) -> { return e.length(); };
int infoLength = displayer.infoLength("I'm CoderGeshu");
System.out.println(infoLength);

由于方法体中只有一条 return 语句,则可以省略大括号与 return 关键字

Displayer displayer = (e) -> e.length();
int infoLength = displayer.infoLength("I'm CoderGeshu");
System.out.println(infoLength);

输出结果:

14


Lambda 总结

经过上面几个示例的介绍,相信大家对 Lambda 的使用有了更近一步的掌握,为了加深印象,这里再对贴一下总结。

使用 Lambda 的前提

  • 使用 Lambda 表达式必须具有接口,无论这个接口是 JDK 内置的接口还是自定义接口,都要求接口中有且仅有一个抽象方法(函数式接口)。
  • 使用 Lambda 必须具有上下文推断。也就是方法的参数或局部变量类型必须为 Lambda 对应的接口类型,才能使用 Lambda 作为该接口的实例。

标准形式:(参数类型 参数名称) ‐> { 方法体 }

  • 参数:
    • 如果小括号里没有参数就使用空 (),不可省略;
    • 小括号内的参数类型可以省略;
    • 如果只存在一个参数,可以省略小括号;
    • 如果存在多个参数,则参数名称之间使用逗号分隔,小括号不可省略;
  • 方法体:
    • 如果大括号内的方法体只有一条语句,无论是否有返回值,都可以省略大括号、return 关键字及语句分号。
    • 如果方法体处理逻辑过于臃肿复杂,建议使用具体子类改写,保证可读性。

双冒号语法

其实对于上述的 Lambda 表达式,还可以进一步做代码简化,这就用到了 Java 8 中的另一个新特性:双冒号语法。

抽取其中的一个例子,就拿集合排序来说吧,用 Lambda 表达式是这样写的:

List<String> itemList = new ArrayList<>();
itemList.add("Eric"); itemList.add("CoderGeshu");
itemList.sort(new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

如果使用双冒号语法进行简化,则可以改为:

List<String> itemList = new ArrayList<>();
itemList.add("Eric"); itemList.add("CoderGeshu");
itemList.sort(Comparator.comparingInt(String::length));

是不是更加简洁了??

双冒号语法非常简单,其使用格式为: 类名::方法名

没有其他任何复杂的处理,注意双冒号前使用的是类名,双冒号后使用方法名,并且不带 ()

比如:

user -> user.getName(); // user是实列对象
// 可以改写成:
User::getName;  // User是类名

() -> new HashMap<>();
// 可以替换成
HashMap::new;

Java 8 引入双冒号操作,在一些场景中非常有用,特别是在 Stream 的操作中,通过理解函数式接口可以更好地理解其原理,关于 Stream 流操作,这里就不再详述了,大家有兴趣可以去看下这篇文章:blog.csdn.net/mu_wind/art…


作者信息

大家好,我是 CoderGeshu,一位爱养爬的程序猿,如果这篇文章对您有所帮助,别忘了点赞收藏哦

点击关注👉 CoderGeshu,第一时间获取最新分享~