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

前言

最近在学习一些关于响应式编程方面的内容,而在响应式编程中,响应式流(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,第一时间获取最新分享~

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享