使用 Lambda 表达式的正确姿势,写得太好了叭

简介: Lambda 表达式非常方便,在项目中一般在 stream 编程中用得比较多。List<Student> studentList = gen();Map<String, Student> map = studentList .stream().collect(Collectors.toMap(Student::getId, a -> a, (a, b) -> a));理解一个 Lambda 表达式就三步:1. 确认 Lambda 表达式的类型2. 找到要实现的方法3. 实现这个方法就这三步,没其他的了。而每一步,都非常非常简单,以至于我分别展开讲一下,你就懂了。

网络异常,图片无法展示
|

Lambda 表达式非常方便,在项目中一般在 stream 编程中用得比较多。

List<Student> studentList = gen();
Map<String, Student> map = studentList .stream()
.collect(Collectors.toMap(Student::getId, a -> a, (a, b) -> a));

理解一个 Lambda 表达式就三步:

1. 确认 Lambda 表达式的类型

2. 找到要实现的方法

3. 实现这个方法

就这三步,没其他的了。而每一步,都非常非常简单,以至于我分别展开讲一下,你就懂了。

确认 Lambda 表达式的类型

能用 Lambda 表达式来表示的类型,必须是一个函数式接口,而函数式接口,就是只有一个抽象方法的接口。

我们看下非常熟悉的 Runnable 接口在 JDK 中的样子就明白了。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
这就是一个标准的函数式接口。
因为只有一个抽象方法。而且这个接口上有个注解

@FunctionalInterface

这个仅仅是在编译期帮你检查你这个接口是否符合函数式接口的条件,比如你没有任何抽象方法,或者有多个抽象方法,编译是无法通过的。

// 没有实现任何抽象方法的接口
@FunctionalInterface
public interface MyRunnable {}
// 编译后控制台显示如下信息
Error:(3, 1) java:
意外的
@FunctionalInterface 注释
MyRunnable 不是函数接口
在 接口 MyRunnable 中找不到抽象方法

再稍稍复杂一点,Java 8 之后接口中是允许使用默认方法静态方法的,而这些都不算抽象方法,所以也可以加在函数式接口里。

看看你可能不太熟悉又有点似曾相识的一个接口。

@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {...}
}

看,只有一个抽象方法,还有一个默认方法(方法体的代码省略了),这个也不影响它是个函数式接口。再看一个更复杂的,多了静态方法,这同样也是个函数式接口,因为它仍然只有一个抽象方法。自行体会吧。

@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {...}
default Predicate<T> negate() {...}
default Predicate<T> or(Predicate<? super T> other) {...}
static <T> Predicate<T> isEqual(Object targetRef) {...}
static <T> Predicate<T> not(Predicate<? super T> target) {...}
}

先不用管这些方法都是干嘛的,这些类在 Stream 设计的方法中比比皆是,我们就先记住这么一句话,Lambda 表达式需要的类型为函数式接口,函数式接口里只有一个抽象方法,就够了,以上三个例子都属于函数式接口。

恭喜你,已经学会了 Lambda 表达式最难的部分,就是认识函数式接口。

找到要实现的方法

Lambda 表达式就是实现一个方法,什么方法呢?就是刚刚那些函数式接口中的抽象方法

那就太简单了,因为函数式接口有且只有一个抽象方法,找到它就行了。我们尝试把刚刚那几个函数式接口的抽象方法找到。

@FunctionalInterface
public interface Runnable {
public abstract void run();

}

@FunctionalInterface

publicinterfaceConsumer<T> {

void accept(T t);

default Consumer<T> andThen(Consumer<? super T> after) {...}

}

@FunctionalInterface

publicinterfacePredicate<T> {

boolean test(T t);

default Predicate<T> and(Predicate<? super T> other) {...}

default Predicate<T> negate() {...}

default Predicate<T> or(Predicate<? super T> other) {...}

static <T> Predicate<T> isEqual(Object targetRef) {...}

static <T> Predicate<T> not(Predicate<? super T> target) {...}

}

好了,这就找到了,简单吧!

实现这个方法

Lambda 表达式就是要实现这个抽象方法,如果不用 Lambda 表达式,你一定知道用匿名类如何去实现吧?比如我们实现刚刚 Predicate 接口的匿名类。

Predicate<String> predicate = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() != 0;
}
};

那如果换成 Lambda 表达式呢?就像这样。

Predicate<String> predicate = 
    (String s) -> {
        return s.length() != 0;
    };
 看出来了么?这个 Lambda 语法由三部分组成:

参数块:就是前面的 (String s),就是简单地把要实现的抽象方法的参数原封不动写在这。

小箭头:就是 -> 这个符号。

代码块:就是要实现的方法原封不动写在这。

不过这样的写法你一定不熟悉,连 idea 都不会帮我们简化成这个奇奇怪怪的样子,别急,我要变形了!其实是对其进行格式上的简化。

首先看参数快部分,(String s) 里面的类型信息是多余的,因为完全可以由编译器推导,去掉它。

Predicate<String> predicate =
(s) -> {
return s.length() != 0;
};

当只有一个参数时,括号也可以去掉。

Predicate<String> predicate = 
    s -> {
        return s.length() != 0;
    };

再看代码块部分,方法体中只有一行代码,可以把花括号和 return 关键字都去掉。

Predicate<String> p = s -> s.length() != 0;

这样看是不是就熟悉点了?

来,再让我们实现一个 Runnable 接口。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
Runnable r = () -> System.out.println("I am running");

你看,这个方法没有入参,所以前面括号里的参数就没有了,这种情况下括号就不能省略。

通常我们快速新建一个线程并启动时,是不是像如下的写法,熟悉吧?

new Thread(() -> System.out.println("I am running")).start();

多个入参

之前我们只尝试了一个入参,接下来我们看看多个入参的。

@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
// default methods removed
}

然后看看一个用法,是不是一目了然。

BiConsumer<Random, Integer> randomNumberPrinter =
(random, number) -> {
for (int i = 0; i < number; i++) {
System.out.println(
"next random = " + random.nextInt());
}
};
randomNumberPrinter.accept(
new Random(314L), 5));

刚刚只是多个入参,那我们再加个返回值。

@FunctionalInterface
public interface BiFunction<T, U, R> {
R
apply(T t, U u);
// default methods removed
}
// 看个例子
BiFunction<String, String, Integer> findWordInSentence =
(word, sentence) -> sentence.indexOf(word);

发现规律了没

OK,看了这么多例子,不知道你发现规律了没?

其实函数式接口里那个抽象方法,无非就是入参的个数,以及返回值的类型。入参的个数可以是一个或者两个,返回值可以是 void,或者 boolean,或者一个类型。那这些种情况的排列组合,就是 JDK 给我们提供的java.util.function包下的类。

BiConsumer

BiFunction

BinaryOperator

BiPredicate

BooleanSupplier

Consumer

DoubleBinaryOperator

DoubleConsumer

DoubleFunction

DoublePredicate

DoubleSupplier

DoubleToIntFunction

DoubleToLongFunction

DoubleUnaryOperator

Function

IntBinaryOperator

IntConsumer

IntFunction

IntPredicate

IntSupplier

IntToDoubleFunction

IntToLongFunction

IntUnaryOperator

LongBinaryOperator

LongConsumer

LongFunction

LongPredicate

LongSupplier

LongToDoubleFunction

LongToIntFunction

LongUnaryOperator

ObjDoubleConsumer

ObjIntConsumer

ObjLongConsumer

Predicate

Supplier

ToDoubleBiFunction

ToDoubleFunction

ToIntBiFunction

ToIntFunction

ToLongBiFunction

ToLongFunction

UnaryOperator

别看晕了,我们分分类就好了。可以注意到很多类前缀是 Int,Long,Double 之类的,这其实是指定了入参的特定类型,而不再是一个可以由用户自定义的泛型,比如说 DoubleFunction。

@FunctionalInterface
public interface DoubleFunction<R> {
    R apply(double value);
}

这完全可以由更自由的函数式接口 Function 来实现。

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

那我们不妨先把这些特定类型的函数式接口去掉(我还偷偷去掉了 XXXOperator 的几个类,因为它们都是继承了别的函数式接口),然后再排排序,看看还剩点啥。

Consumer

Function

Predicate

BiConsumer

BiFunction

BiPredicate

Supplier

哇塞,几乎全没了,接下来就重点看看这些。这里我就只把类和对应的抽象方法列举出来

Consumer void accept(T t)

Function R apply(T t)

Predicate boolean test(T t)

BiConsumer void accept(T t, U u)

BiFunction R apply(T t, U u)

BiPredicate boolean test(T t, U u)

Supplier T get()

看出规律了没?上面那几个简单分类就是:

supplier:没有入参,有返回值。

consumer:有入参,无返回值。

predicate:有入参,返回 boolean 值

function:有入参,有返回值

然后带 Bi 前缀的,就是有两个入参,不带的就只有一个如参。OK,这些已经被我们分的一清二楚了,其实就是给我们提供了一个函数的模板,区别仅仅是入参返参个数的排列组合。

用我们常见的 Stream 编程熟悉一下

下面这段代码如果你项目中有用 stream 编程那肯定很熟悉,有一个 Student 的 list,你想把它转换成一个 map,key 是 student 对象的 id,value 就是 student 对象本身。

List<Student> studentList = gen();
Map<String, Student> map = studentList .stream()
.collect(Collectors.toMap(Student::getId, a -> a, (a, b) -> a));

把 Lambda 表达式的部分提取出来。

Collectors.toMap(Student::getId, a -> a, (a, b) -> a)

由于我们还没见过 :: 这种形式,先打回原样,这里只是让你预热一下。

Collectors.toMap(a -> a.getId(), a -> a, (a, b) -> a)

为什么它被写成这个样子呢?我们看下 Collectors.toMap 这个方法的定义就明白了。

public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends U> valueMapper,
        BinaryOperator<U> mergeFunction) 
{
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}

看,入参有三个,分别是Function,Function,BinaryOperator,其中 BinaryOperator 只是继承了 BiFunction 并扩展了几个方法,我们没有用到,所以不妨就把它当做BiFunction

还记得 Function 和 BiFunction 吧?

Function R apply(T t)

BiFunction R apply(T t, U u)

那就很容易理解了。

第一个参数a -> a.getId()就是 R apply(T t) 的实现,入参是 Student 类型的对象 a,返回 a.getId()

第二个参数a -> a也是 R apply(T t) 的实现,入参是 Student 类型的 a,返回 a 本身

第三个参数(a, b) -> a是 R apply(T t, U u) 的实现,入参是Student 类型的 a 和 b,返回是第一个入参 a,Stream 里把它用作当两个对象 a 和 b 的 key 相同时,value 就取第一个元素 a

其中第二个参数 a -> a 在 Stream 里表示从 list 转为 map 时的 value 值,就用原来的对象自己,你肯定还见过这样的写法。

Collectors.toMap(a -> a.getId(), Function.identity(), (a, b) -> a)

为什么可以这样写,给你看 Function 类的全貌你就明白了。

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t); 
    ...
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

看到了吧,identity 这个方法,就是帮我们把表达式给实现了,就不用我们自己写了,其实就是包了个方法。这回知道一个函数式接口,为什么有好多还要包含一堆默认方法和静态方法了吧?就是干这个事用的。

我们再来试一个,Predicate 里面有这样一个默认方法。

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
}

它能干嘛用呢?我来告诉你,如果没有这个方法,有一段代码你可能会这样写。

Predicate<String> p = 
    s -> (s != null) && 
    !s.isEmpty() && 
    s.length() < 5;

如果利用上这个方法,就可以变成如下这种优雅形式。

Predicate<String> nonNull = s -> s != null;
Predicate<String> nonEmpty = s -> s.isEmpty();
Predicate<String> shorterThan5 = s -> s.length() <
5;
Predicate<String> p = nonNull.and(nonEmpty).and(shorterThan5);

自行体会吧。

方法引用

那我们回过头再看刚刚的 Student::getId 这种写法。当方法体中只有一个方法调用时,就可以作这样的简化。

比如这个 a -> a.getId() 就只是对 Student 对象上 getId() 这个方法的调用,那么就可以写成 Student::getId 这种形式。

再看几个例子

Function<String, Integer> toLength = s -> s.length();
Function<String, Integer> toLength = String::length;
Function<User, String> getName = user -> user.getName();
Function<String, Integer> toLength = User::getName;
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer = System.out::println;

如果是构造方法的话,也可以简化。

Supplier<List<String>> newListOfStrings = () -> new ArrayList<>();
Supplier<List<String>> newListOfStrings = ArrayList::new;

总结

学会理解和写 Lambda 表达式,别忘了最开始的三步。

1. 确认 Lambda 表达式的类型

2. 找到要实现的方法

3. 实现这个方法

Lambda 表达式的类型就是函数式接口,要实现的方法就是函数式接口里那个唯一的抽象方法,实现这个方法的方式就是参数块 + 小箭头 + 方法体,其中参数块和方法体都可以一定程度上简化它的写法。

是不是很简单了!

以上代码例子,都来源于官方的教程,英语好的同学可以看看,是最科学的 Lambda 表达式教程了。

https://dev.java/learn/tutorial/getting-to-know-the-language/lambda-expressions/lambdas.html

今天的文章主要就是讲怎么写出 Lambda 表达式,至于原理,之后再说。这里提个引子,你觉得 Lambda 表达式是匿名类的简化么?按照官方的说法,Lambda 表达式在某些情况下就是匿名类的一种更简单的写法,但是从字节码层面看,完全不同,这又是怎么回事呢?

相关文章
|
9月前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
578 70
|
11月前
|
算法 测试技术 Swift
Kimi开源Moonlight-16B-A3B:基于Muon优化器的高效大模型,性能与训练效率双突破!
Kimi开源Moonlight-16B-A3B:基于Muon优化器的高效大模型,性能与训练效率双突破!
457 5
|
中间件 Go API
使用Go语言构建高性能RESTful API
在现代软件开发中,RESTful API因其简洁和高效而成为构建网络服务的首选。Go语言以其并发处理能力和高性能著称,是开发RESTful API的理想选择。本文将介绍如何使用Go语言构建RESTful API,包括基础的路由设置、中间件的使用、数据验证、错误处理以及性能优化。通过实际代码示例,我们将展示Go语言在API开发中的强大功能和灵活性。
|
人工智能 网络协议 安全
探索未来网络:从IPv4到IPv6的演变
在数字化时代,互联网的发展速度令人瞩目。然而,随着物联网、大数据和人工智能等新兴技术的崛起,传统的IPv4协议已难以满足日益增长的地址需求。本文将探讨IPv6如何成为下一代互联网的关键,它不仅解决了地址枯竭的问题,还带来了安全性提升、效率改进等诸多优势。通过分析IPv6的技术创新和应用前景,我们可以预见一个更加智能、互联的未来世界。
|
安全 算法 网络协议
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
在数字时代,网络安全和信息安全已经成为了我们生活中不可或缺的一部分。本文将介绍网络安全漏洞、加密技术和安全意识等方面的内容,帮助读者更好地了解网络安全的重要性和应对措施。通过阅读本文,您将了解到网络安全的基本概念、常见的网络安全漏洞、加密技术的原理和应用以及如何提高个人和组织的网络安全意识。
|
数据安全/隐私保护 Windows
windows系统bat批处理 笔记本开wifi 笔记本查看wifi密码
windows系统bat批处理 笔记本开wifi 笔记本查看wifi密码
521 0
|
数据可视化 数据挖掘
Signac|成年小鼠大脑 单细胞ATAC分析(1)
Signac|成年小鼠大脑 单细胞ATAC分析(1)
Java 实现 Elasticsearch 查询全部数据
【7月更文挑战第7天】Java 实现 Elasticsearch 查询全部数据
|
Linux C++ iOS开发
VLC源码解析:视频播放速度控制背后的技术
VLC源码解析:视频播放速度控制背后的技术
1313 0
|
自然语言处理 搜索推荐 Python
使用python读取word统计词频并生成词云
使用python读取word统计词频并生成词云
409 0