暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

走进 Java 8(上): Lambda 表达式

我与风来 2021-06-28
903

一份耕耘,一份收获


Java 8 自发布以来,已过了好几个年头,渐渐地,它已成为主流这个版本是一个拥有丰富特性的主要版本,如果还不熟悉它的使用姿势,赶紧学习并使用起来吧!

通过阅读本文,你可以收获以下:

  • 熟悉 Lambda 表达式;

  • 理解为什么 Lambda 表达式的局部变量需要是 final 或 等效 final

  • 灵活使用方法引用;

  • 了解接口的默认方法以及静态方法;

Lambda 表达式

Lambda 表达式是一个新的语言特性,它可以让我们将功能视为方法参数,或者将代码视为数据。使用 Lambda 表达式,我们可以更简洁地表示单方法接口(称为功能接口)的实例。

这个特性带来的帮助非常大。在以前,经常羡慕 JS能够拥有如下的写法:

js.lambda

尽管这种写法饱受诟病,但如果在 Java 中可以选择的话,我们还是应该拥抱它。

Java 文档中关于 Lambda 表达式的介绍是紧挨着匿名类的。所以,不难想象它们之间的关联。

匿名类可以实现一个基类而不给它名字,虽然这比命名类更加简洁,但对于只有一个方法的类,即使是匿名类也显得有点麻烦。我们使用匿名类和 Lambda 表达式的来对集合进行遍历,代码如下:

匿名类.lambda

语法

Lambda 表达式语法如下:

  • 用逗号分隔的形式参数列表,用括号括起来;

    Note: 数据类型可以省略,只有一个参数还可以省略括号,例如,上图的 element

  • 标识符: ->

  • 主体:放在 {} 内。

    Note:如果主体仅仅是一个表达式或者或一个语句块,{} 和 ; 可以省略。只存在一条返回语句的话,也可以省略 return 关键字 。

我们对集合做一个遍历,展示一个完整的演进过程:

Lambda 简化过程

适用场景

不过,Lambda 表达式只能用于功能接口(函数式接口),也就是说只能用在一个接口除了含有单个抽象方法和 Object
中的 public
访问修饰符定义的方法外,不能再含有其它抽象方法。Java 中常见的 Comparator
便是一个很好的例子。

@FunctionalInterface
interface Comparator<T{
    boolean equals(Object obj);
    int compare(T o1, T o2);
}

尽管,Comparator
拥有两个抽象方法,但 equals
方法属于 Object
的公共方法,所以,Copmarator
仍可被视作函数式接口,可以用 Lambda 表达式来表达。

那么,如何理解 Lambda 表达式和函数式接口的关系呢?

我们可以这样理解:对lambda表达式的计算将生成函数式接口的实例。不过请放心,这并不会导致表达式主体的执行,相反,表达式主体只会在调用函数式接口的方法时执行。

使用变量的局限性

还需要注意的是在 Lambda 表达式中,使用外部的局部变量需要变量是 final
或者 等效 final
的。等效 final
作为一种语法糖,代表编译器可以识别这样的情况:虽然 final
关键字不存在,但引用根本没有改变,这意味着它实际上是 final

那么,为什么要求是 final
或者等效 final
的呢?这其实是与变量的性质以及 Java 如何在内存中存储它们有关。

public Supplier<Integer> hello(Integer value){
    return () -> value++;
}

这段代码编译并不会通过,Lambda 表达式会捕获 value ,这意味着需要复制它,强制变量是 final
能够避免这样的错觉:在 Lambda 表达式内部增加 value 的值会修改到方法参数 value 的值。

Lambda 表达式在内部方法被调用之前,并不会被执行。但在这里将作为方法的返回,而 value 又是存储在栈上,随着调用方法的结束,它不得不被回收。所以,为了使这个 Lambda 表达式在方法之外存在,Java 不得不复制 value。

那么,Lambda 表达式使用存储在堆中的成员变量,就可以不用声明为 final
了嘛?答案是是的。

还有个猜测就是并发问题,局部变量用在 Lambda 表达式中,实现其可见性非常复杂,而成员变量则可以利用 volatile
或者同步,实现其可见性。

关于shadow

通过代码可以做个对比便能明白:

    public Student(String name){
        this.name = name;
        Function<String, String> function = name -> name;
    }

function
的 name 编译时会提醒 variable name is already defined in the scope
。但形式参数却能定义成为和成员变量 name 相同的名字,这便是 shadow

如果特定作用域(如内部类或方法定义)中的类型声明(如成员变量或参数名)与封闭作用域中的另一个声明具有相同的名称,则该声明将隐藏封闭作用域的声明。不能仅通过名称引用带阴影的声明(可以使用 this
等)。

Lambda表达式是词汇范围的。这意味着它们不会从父类型继承任何名称,也不会引入新级别的作用域。

方法引用

方法引用可看作对 Lambda 表达式在特定场景下的一种简写。这里的特定场景指的便是将已具有名称的方法用作 Lambda 表达式。

方法引用分为四种:

  • 引用一个静态方法:ContainingClass::staticMethodName

  • 引用一个特定对象的实例方法:containingObject::instanceMethodName

  • 引用特定类型的任意对象的实例方法:ContainingType::methodName

  • 引用一个构造函数:ClassName::new

我们通过代码来分析该如何使用:

Student.java

实体类 Student.java

方法引用

map(Student::getName)
是引用特定类型的任意对象的实例方法。map
接受的函数式接口是 Function
,定义如下:

@FunctionalInterface
public interface Function<TR{
    apply(T t);
}

也就是说需要一个形参,并产生返回值。在这里,形参的类型是 Student
,那么如上写法,最终产生的影响是:

将流中的单个 Student 对象作为参数传入 apply 方法,并调用该 student 对象的 getName
方法作为返回值。

map(Student::new)
则是引用一个构造函数,在这里,形参的类型是 String
,那么这种写法产生的影响是:传入一个 string,并调用 Student 的有参构造函数。这里也可写为 map(Student::newInstance)
,通过静态方法的形式来产生一个对象。

其实分析以上不难发现,方法引用需要已有的方法 “符合” 函数式接口定义的抽象方法。这个 “符合” 指的是:

  • 如果函数式接口定义的抽象方法,有返回值,那么方法引用所指向的方法也必须有返回值。

  • 如果函数式接口定义的抽象方法,有形式参数,那么可分为以下两种:

  • 静态方法,特定对象的实例方法,构造函数 :这三种方法引用所指向的方法都必须有对应的形式参数;

  • 引用特定类型的任意对象的实例方法:这种方法引用所指向的方法可以没有形式参数,但在函数式接口的内部方法执行时,会使用传入的参数对象来调用所指向的实例方法,所以它们的类型必须匹配。

    Note:针对特定类型的任意对象的实例方法,我们可以想象为针对传入这一函数式接口方法的入参都执行该实例方法的调用,所以也就不再需要指定具体的对象实例了,例如上面的 map(Student::getName)

依赖于编程工具的智能,我们能够很容易地发现方法引用的问题所在,并很快地解决它!不过学习并知悉语言特性,而不单纯地依赖编程工具,仍然是作为一名合格程序员所必备的技能。

默认方法

默认方法指的是在接口中可以实现方法,所以在常见的面试问题中, 接口和抽象类有什么不同? 便不要再回答抽象类可以有方法的实现,而接口不可以有了。

在 Java 版本的迭代中,一切都在发生着变化。唯一不变的可能便是兼容性这一保证了。后续的版本需兼容前面的版本,被添加有默认方法的接口仍然在二进制上保持着兼容。

Comparator.java

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

默认方法其实是为了帮助我们使用上 Lambda,它可以将接受 Lambda 表达式作为参数的方法添加到现有的接口中。其中,最主要的可能便是集合类接口了,为了实现 Stream 操作,它添加了大量的默认方法,支持接收 Lambda 表达式作为参数。

Stream 作为一大特性,也通过结合 Lambda 表达式来使流操作更加的简单明了。我们不再关心怎么做,而只关心做什么,这可能是流式操作结合Lambda表达式能带给我们最直观的感受了!

接口中的静态方法

除了默认方法外,还可以在接口中定义静态方法。静态方法是与定义它的类而不是与任何对象关联的方法。类的每个实例都共享它的静态方法。

Comparator.java

    public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
        return Collections.reverseOrder();
    }

在接口中的所有方法,包括静态方法,都是隐式声明 public
的,所以可以省略 public

写在最后

关于 Lambda 表达式,默认方法以及接口中的静态方法介绍便是这些了。相信在不断地编程中,我们会越来越熟悉它们的使用,就像传统的语法使用一样。所以,现在就开始吧!

这是 Java 8 新特性系列 的第一篇文章,如果你觉得我的文章还不错,并对后续文章感兴趣的话,都可以通过点击文章顶部蓝色字体 我与风来 关注我的公众号。赞!

文章转载自我与风来,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论