一、基本介绍
自己平时主要使用java作为开发语言,在开发的过程中经常会使用到Lambda表达式,虽然大部分情况下能用Lambda表达式完成相应的功能,但是对其内部的原理以及涉及到的一些概念一直不是很理解。最近查了一些文档,结合自己的理解做了一些笔记,欢迎大家一起交流学习。
Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本,Oracle 公司于 2014 年 3 月 18 日发布 Java 8。Java8 新增了非常多的特性,包括Lambda表达式、方法引用、默认方法、Stream API、Optional类等等。
Lambda表达式可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。举一个简单的例子,将数组[1,6,10,4]中大于5的数字输出,下面使用传统方式和Lambda表达式进行对比:
// 定义列表
List<Integer> list = Arrays.asList(1, 6, 10, 4);
// 传统方式
for (int num : list) {
if (num > 5) {
System.out.println(num);
}
}
// Lambda表达式
list.stream().filter(e -> e > 5).forEach(System.out::println);
Lambda表达式官方的定义是:parameter -> expression
,对应到语句list.stream().filter(e -> e > 5).forEach(System.out::println);
中的是e -> e > 5
和System.out::println
部分(说明:这里是结合方法引用对Lambda进行了简写),但是我们一般在使用Lambda表达式的时候还会同时用到Stream、方法引用、函数式接口等,所以这里我更倾向于叫语句list.stream().filter(e -> e > 5).forEach(System.out::println);
为函数式编程。在Java8中为了用好函数式编程,个人认为需要理解四部分内容,分别是Lambda表达式、函数式接口、方法引用和Stream。
这里先简单给一下说明:
- Lambda表达式:是函数式接口中的唯一抽象方法的实现
- 函数式接口:只有一个抽象方法的接口,是Lambda表达式的类型
- 方法引用:可直接引用已有Java类或对象的方法或构造器,与Lambda表达式联合使用让代码更简洁
- Stream:结合Lambda表达式以声明性方式处理集合,使代码更加简洁,可读性更强
二、概念理解
1. Lambda表达式
Lambda 表达式由三部分组成:参数、->符号、方法体
(参数列表) -> { 方法体 }
说明:
- Lambda 表达式的参数可以是零个或多个
- 参数的类型可以明确声明,也可以不声明,由 JVM 隐式地推断,例如 (int a) 和 (a) 效果相同
- 当参数只有一个,且类型可推导时,可以省略括号,例如 (a) 与 a 效果相同
- 如果 Lambda 表达式的方法体只有一条语句时,可以省略花括号
- 如果 Lambda 表达式的方法体只有一条语句,且为返回值的时候,可以省略 return
代码示例:
// 不需要参数,返回值为 2
() -> 2
// 接收一个参数(数字类型),返回值为其2倍的值
x -> 2 * x
// 接收两个参数(数字类型),并返回它们的和
(x, y) -> x + y
// 接收两个 int 类型参数,返回它们的乘积(与上一个等效,jvm会进行类型自动推断)
(int x, int y) -> x * y
// 接收一个String对象,并在控制台打印
(String s) -> System.out.println(s)
java是一种面向对象的语言,java中的一切都是对象,即数组、每个类创建的实例也是对象。在java中定义的函数或方法不可能完全独立,也不能将方法函数作为参数或返回值给实例。在java7及以前,我们一直都是通过匿名内部类把方法或函数当做参数传递,如下是一个线程实例:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("test");
}
}).start();
在java8中可以通过Lambda表达式将其简化为:
new Thread(() -> System.out.println("test")).start();
第一种方式我们比较熟悉,使用的是匿名内部类(以Runnable作为接口,创建一个没有实际类名的类,在该类中重写了run方法,最后创建该类的对象)
第二种方式中使用了Lambda表达式作为"类对象",注意虽然第一种和第二种方式中new Thread()
的括号中内容形式完全不一样,但是我们通过Thread类的定义可以看到都是调用了Thread(Runnable target)
作为构造函数,因此我们知道() -> System.out.println("test")
这个Lambda表达式实际上是Runnable接口的实现,Runnable是这个Lambda表达式的函数式接口。
2. 函数式接口
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口(这个好理解,因为Lambda表达式只是一个函数实现,并且只能是这个唯一抽象方法的函数实现,如果有多个抽象方法,那就对应不上了)。与@Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface 。该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法(equal、hashcode、default、static等方法不算),否则将会报错。但是这个注解不是必须的,只要符合函数式接口的定义,那么这个接口就是函数式接口。
JDK 1.8 之前已有的函数式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
JDK 1.8 新增加的函数接口:
- java.util.function
java.util.function包下面包含了很多类,用来支持 Java的 函数式编程,该包中常用的函数式接口有:
函数接口 | 抽象方法 | 功能 | 参数 | 返回类型 |
---|---|---|---|---|
Predicate | test(T t) | 判断真假 | T | boolean |
Consumer | accept(T t) | 消费消息 | T | void |
Function | R apply(T t) | 将T映射为R | T | R |
Supplier | T get() | 生产消息 | None | T |
UnaryOperator | R apply(T t) | 一元操作 | T | R |
BinaryOperator | R apply(T t, U u) | 二元操作 | (T, U) | R |
使用举例:
// Runnable
new Thread(() -> System.out.println("test")).start();
// Callable
Executors.newCachedThreadPool().submit(() -> null);
// Predicate
Predicate<Integer> predicate = x -> x > 10;
System.out.println(predicate.test(20));
// Consumer
Consumer<String> consumer = x -> System.out.println(x);
consumer.accept("test");
// Function
Function<Integer, String> function = x -> x + "abc";
System.out.println(function.apply(10));
// Supplier
Supplier<String> supplier = () -> "11";
System.out.println(supplier.get());
// UnaryOperator
UnaryOperator<Integer> unaryOperator = x -> x + 1;
System.out.println(unaryOperator.apply(10));
// BinaryOperator
BinaryOperator<Integer> binaryOperator = (a, b) -> a * b;
System.out.println(binaryOperator.apply(2, 3));
// CustomFuncitonInterface
@FunctionalInterface
interface Worker {
void work(String name);
}
Worker worker = (name) -> System.out.println(name + " is working");
worker.work("zhangsan");
3. 方法引用
方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。
- 方法引用通过方法的名字来指向一个方法
- 方法引用可以使语言的构造更紧凑简洁,减少冗余代码
- 方法引用使用一对冒号 ::
方法引用与Lambda表达式的关系:
类型 | 方法引用 | Lambda表达式 |
---|---|---|
静态方法引用 | 类名::staticMethod | (args) -> 类名.staticMethod(args) |
实例方法引用 | inst::instMethod | (args) -> inst.instMethod(args) |
对象方法引用 | 类名::instMethod | (inst,args) -> 类名.instMethod(args) |
构建方法引用 | 类名::new | (args) -> new 类名(args) |
举个经常用到的例子:
// 定义数组
List<Integer> list = Arrays.asList(1, 2, 3);
// Lambda表达式方式输出
list.forEach(item -> System.out.println(item));
// 方法引用方式输出
list.forEach(System.out::println);
4. Stream
Stream流是jdk8为了更方便对集合类的迭代而产生的,通过Stream流你可以实现对集合的遍历、分组、过滤、排序、集合类型转化,找到集合(主要是list和set)中的极值等等。
Stream流特点:
- Stream并不是某种数据结构,它更像是数据源的一种迭代器(Iterator),单向、不可重复。
- Stream数据源只能遍历一次。Stream通常调用对应的工具方法创建,如:list.stream()。
- Stream流操作共分为两个大类:惰性求值、及时求值。及时求值有foreach,collect操作。当且仅当存在及时求值(终端操作)时,惰性求职(中间操作)操作才会被执行。
流的使用一般包括三件事:
- 一个数据(如集合)来执行一个查询
- 一个中间操作链,形成一条流的流水线
- 一个终端操作,执行流水线,并能生成结果
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。
举例:
List<String> names = menu.stream() //获取流
.filter(d -> d.getCalories() > 300) //中间操作
.map(Dish::getName) //中间操作
.limit(3) //中间操作
.collect(toList()); //终端操作
其示意图如下所示:
三、常用方法
Lambda表达式最常用的使用场景是在集合中,包括forEach、filter、map、distinct、sorted、groupingBy、reduce、limit、max/min等,下面逐一举例介绍。
//预置条件
class Student {
private String name;
private String sex;
private int age;
public Student(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public String getName() {
return name;
}
public String getSex() {
return sex;
}
public int getAge() {
return age;
}
}
private static List<Student> list = Arrays.asList(
new Student("james", "M", 20),
new Student("lili", "W", 18),
new Student("tom", "M", 16),
new Student("lucy", "W", 20),
new Student("william", "M", 21),
new Student("emma", "W", 18)
);
1. forEach
// 输出每个学生的名字
list.forEach(e -> System.out.println(e.getName()));
2. filter
// 筛选出年龄大于20的学生并输出他们的名字
list.stream().filter(e -> e.getAge() > 20).forEach(e -> System.out.println(e.getName()));
3. map
// 获取所有学生的名字并保存在列表中
List<String> names = list.stream().map(Student::getName).collect(Collectors.toList());
names.forEach(System.out::println);
4. distinct
// 获取所有学生的年龄并去重
List<Integer> ages = list.stream().map(Student::getAge).distinct().collect(Collectors.toList());
ages.forEach(System.out::println);
5. sorted
// 将所有学生按年龄正序排序并输出名字
list.stream().sorted((s1, s2) -> s1.getAge() - s2.getAge()).forEach(e -> System.out.println(e.getName()));
//这个与上面的等价
list.stream().sorted(Comparator.comparingInt(Student::getAge)).forEach(e -> System.out.println(e.getName()));
// 将所有学生按年龄倒序排序并输出名字
list.stream().sorted((s1, s2) -> s2.getAge() - s1.getAge()).forEach(e -> System.out.println(e.getName()));
6. groupingBy
// 将所有学生按性别进行分组并输出性别以及对应的学生姓名信息
Map<String, List<Student>> map = list.stream().collect(Collectors.groupingBy(Student::getSex));
map.forEach((key, value) -> System.out.println(key + ": " + value.stream().map(Student::getName).collect(Collectors.joining(","))));
7. reduce
// 计算所有学生的年龄总和
int sum = list.stream().map(Student::getAge).reduce(0, Integer::sum);
System.out.println(sum);
8. limit
// 输出年龄最小的3位学生的姓名
list.stream().sorted(Comparator.comparingInt(Student::getAge)).limit(3).map(Student::getName).forEach(System.out::println);
9. max/min
// 输出年龄最小的学生信息
list.stream().min(Comparator.comparingInt(Student::getAge)).ifPresent(student -> System.out.println(student.getName() + ": " + student.getAge()));
// 输出年龄最大的学生信息
list.stream().max(Comparator.comparingInt(Student::getAge)).ifPresent(student -> System.out.println(student.getName() + ": " + student.getAge()));