Java8 Lambda 表达式初探

一直对lambda表达式感觉到很缥缈:光秃秃的一个 lambda 表达式突兀地出现在代码,不知道它的入参、返回参数等等是什么。这让我感觉非常困惑,看不懂其中原理。在网上搜了一些文章,也没看明白,似是而非。这两天通过看《Java核心技术卷1》(第10版)终于搞懂了一些皮毛。现记录在此,欢迎各位前辈同仁指导交流。

什么是 lambda 表达式

lambda表达式 久负盛名,尤其是在Python大行其道的时代,名声更是如雷贯耳。但不要被它的名头吓住。简单地讲,lambda表达式就是一个代码块,它可以在定义之后被执行一次或多次,目的是用来将一段代码传递到某个对象中。lambda 表达式最初来源于Alonzo Church用lambda表示参数。后来 lambda 表达式就代指“带参数变量的表达式”。
在Java中,lambda表达式的格式是:

(Type1 param1, Type2 param2 ...) -> { statement1; statement2; ... }

小括号中的部分是lambda表达式的参数,大括号里的内容是 lambda 表达式的具体方法体(也就是要传递的代码块)。lambda表达式的返回值类型由上下文自动推断得出,无需指定。实际上,也不能指定lambda表达式的返回类型。因为 lambda 表达式是作为一个参数(即整个表达式作为一个整体,是一个参数)出现在某个方法的入参处。
在Java中,“->”符号是 lambda 表达式的标志,凡是在代码中看到“->”,那么这就是一个 lambda 表达式。lambda 表达式
lambda表达式也称为闭包。闭包是指若干代码语句和自由变量值组成的代码块。在Java中,lambda表达式就是闭包。

为什么使用 lambda 表达式

在Java8引入 lambda 表达式之前,要实现这样的操作,必须要构造一个包含这段代码的方法,并通过一个具体的对象来调用这个方法完成代码传递。这样的操作以往通常是借助接口来实现。在实现时,需要创建一个接口的匿名类。比如常见的事件监听处理就是这样的例子。比如在Java Swing中的每隔1秒打印一下当前日期的Timer,需要一个event的ActionListener。在Java8之前的做法是:

        Timer timer = new Timer(1000, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println(new Date());
            }
        });

有了 lambda 表达式以后,就可以直接通过lambda表达式来简洁地实现代码传递。上面这段事件监听的代码如果采用 lambda 表达式,则可以写成如下形式:

        Timer timer3 = new Timer(1000, x->System.out.println(new Date()));

代码一下子简洁了很多。可能看惯了接口、面向对象的代码,刚开始看这个lambda表达式会觉得反倒不清晰了。慢慢往下看,你也许就会体会到lambda表达式确实使代码更精炼了。
  这里“x->System.out.println(new Date())”是一个整体,作为参数传递给Timer的构造器的。其中的x是参数,你可以将x换成任意你想取的字符(只要满足Java变量命名规范)。它只是一个符号,并不重要。“System.out.println(new Date())”其实是lambda表达式对应的接口的方法的一个实现。因为这里只有一句,所以省略了大括号。这个“System.out.println(new Date())”就是实现方法的方法体中的具体内容。
  那这里是实现的哪个接口呢?你怎么知道它是实现的这个接口?
  答案是:Java8的编译器会根据程序上下文自己推导出 lambda表达式要实现哪个接口。以这里的句子为例:这里是Timer的构造器,而Timer的构造器中,只有一个含ActionListener接口入参的构造器。因此这里要求 lambda 表达式提供一个ActionListener类型的参数。而ActionListener接口只有一个 actionPerformed(ActionEvent e)方法。表达式中的x作为 actionPerformed()方法的实参,System.out.println(new Date())作为该方法的方法体。
  之所以编译器能推断出这些信息,是因为Java8对lambda表达式的编译指定了如上述的限制。
  所以 lambda 表达式不需要明确写出是实现哪个接口。这就好比程序员和Java之间达成了一个默契:只需要程序员自己清楚该使用哪个接口,Java就能保证按照这个接口进行编译。
  这里引出另外一个概念——函数式接口。编译器要正确进行上面的推断,是基于以下假设的:
  接口只有一个抽象方法;如果有多个抽象方法,显然编译器就不知道该选哪一个了。
  所以,对于只有一个抽象方法的接口就被称为函数式接口
  在Java中,对 lambda 表达式所能做的也只是能转换为函数式接口。“想要用 lambda 表达式做某些处理吗,还是要谨记表达式的用途,为它建立一个特定的函数式接口”。
  与lambda表达式的还有方法引用、构造器引用的概念。方法引用和构造器引用的标志是“::”(两个冒号)。方法引用和构造器引用本质上都是lambda表达式,是lambda表达式的进一步简化。这两部具体讲起来有点绕,书中讲得清楚些。主要的知识点如下。
  方法引用分为三种:
1. object::instanceMethod
2. Class::staticMethod
3. Class::instanceMethod
构造器引用的格式是“Class::new”,其中的“new”是固定的。
方法引用和构造器引用都需要转换为函数式接口的实例。

怎样使用 lambda 表达式

想在程序中传递一段代码,想简化监听器等只有一个抽象方法的接口的匿名实现,就可以首先使用 lambda 表达式。 lambda表达式的书写格式见本文前面部分。需要注意的如下。

  1. 即使 lambda 表达式没有参数,"()"也必须要写出来。
  2. 如果lambda表达式的方法体只有一条语句,大括号可以省略。
  3. 如果表达式参数的类型可以由上下文推导得出,则类型也可以省略。

注意事项

  1. 在lambda表达式中,只能引用值不会改变的变量。这包括两层含义:一是如果lambda表达式的方法体中引用了接口的参数,则方法体中不能对该变量进行修改;二是如果lambda表达式的方法体中引用了上下文中的变量,则该变量的值不能进行更改。即要求lambda表达式捕获的变量必须是实际上的最终变量(即effective final)。
  2. 注意下面这个方法中的变量
    tempTest.repeat(10, i-> System.out.println(i));
    其中repeat方法如下:
    public void repeat(int n, IntConsumer action){
    for (int i=0; i<n; i++){
    action.accept(i);
    }
    }
    这里的i是传递给接口的参数,是可以变的。跟上一节讲的lambda表达式中的变量须是effective final不是一回事。那里的变量是位于表达式中的,需要参与到表达式的运算,所以需要是effective final。

高级操作

可以把比较器与 thenComparing 方法串起来。例如,
Arrays.sort(people, Comparator.comparing(Person::getlastName)
.thenConipari ng(Pe rson::getFi rstName)) ;
如果两个人的姓相同, 就会使用第二个比较器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值