文章目录
饿了困了,来个语法糖——Lambda
0. 前言
Lambda 是JDK 8新特性, 是我们常用且常见的语法糖之一,极大地简化了我们的代码。
还记得初学Lambda的时候,尖尖的箭头像极了C的指针,实际上概念完全不同。对于初学者而言,Lambda可读性差,但是用习惯了之后,基本上都感到:真香!
本质
Lambda 的本质 依然是匿名方法,因此使用 Lambda 是对语法的简化,即语法糖,并无侵入性。同时,Lambda 的思想是将方法作为参数进行传递,增加程序灵活性。
常见用处
- 简化匿名内部类,如 FileFilter
- 函数式编程,如 Mybatis-Plus的 LambdaQueryWrapper
- 并行处理,如Stream(管道)聚合操作
学习目标
初学:掌握Lambda表达式用法
回顾:牢记Lambda的本质,进一步熟悉用法
Stream:学习并掌握常见Stream聚合操作,结合Lambda
其他场景:如 熟练使用 LambdaQueryWrapper 等,请读者参考Mybatis-Plus相关文档与实际项目进行演练
参考资料
优快云—Lambda入门—参考博文:https://blog.youkuaiyun.com/weixin_43779187/article/details/126490173
知乎—Lambda到底怎么“玩”—优秀参考文章:https://zhuanlan.zhihu.com/p/340832516
知乎—四万字Lambda表达式完整教程—优秀参考文章:https://zhuanlan.zhihu.com/p/501465110
1. Lambda 入门
在前言中我们了解了Lambda是对匿名内部类的简化,本质是匿名方法,可以理解为把方法作为参数传递。那么我们可以通过常见的FileFilter入门:
FileFilter源码
public interface FileFilter {
/**
* Tests whether or not the specified abstract pathname should be
* included in a pathname list.
* ......其他注释
*/
boolean accept(File pathname);
}
由源码得知,FileFilter 是一个接口,里面只有一个抽象方法
匿名内部类创建FileFilter子类
FileFilter filter = new FileFilter() {
@Override
public boolean accept(File file) {
return false;
}
};
使用Lambda
新版 IDEA 结合一些插件,是有Lambda提示的,我们点击提示,可得到:
FileFilter filter = file -> false;
这个表达式其实是与上面的匿名内部类是一致的。接下来,我们详细了解Lambda表达式的基本用法。
1.1 Lambda 匿名方法
使用条件
并不是所有的抽象方法都能用 Lambda 简化。
所实现的接口只能有一个抽象方法,例如FilieFilter就只有以个accept方法。
如果某接口中有多个抽象方法,那么Lambda将不适用。
基本语法
(参数列表)->{
方法体
}
举例
例如 上面 提到的 FileFilter 匿名内部类可以写成:
FileFilter filter1 = (File pathname) -> {
return false;
};
省略参数类型
参数列表中的参数类型也可以省略:
//参数类型可以不写
FileFilter filter2 = (pathname) ->{
return false;
};
省略括号
当参数只有一个时,括号也可以省略:
//当参数只有一个时
FileFilter filter3 = pathname -> {
return false;
};
省略方法体
当方法体只有一句话时,可以省略方法体,箭头直接指向返回值,也就是我们最初举的例子
FileFilter filter4 = pathname -> false;
综合举例
例如,我们使用文件过滤器,实现对包含某个名字的文件进行过滤时,可以写成如下:
File [] fs = new File("./src/file").
listFiles(pathname -> pathname.getName().contains("M")
);
//遍历一下结果
for (File subs : fs){
System.out.println(subs);
}
1.2 Lambda 方法引用
方法引用通过方法的名字来指向一个方法,使语言的构造更加紧凑,减少冗余代码。
Lambda 引用方法 可以采用双冒号 ::
的形式。
引用静态方法
举个例子:
public class TestLambdaStatic {
public static void main(String[] args) {
// random string list
int size = 10;
List<String> stringList = new ArrayList<>();
for (int i = 0; i < size; i++) {
stringList.add(generateRandomString());
}
System.out.println("stringList = " + stringList);
String c = "a";
// using StringChecker to check string
StringChecker checker = (s, cs) -> s.contains(cs);
// 常规调用
checkStringList(stringList,c,checker);
// 使用 :: 调用静态方法
List<String> resultList = checkStringList(stringList, c, TestLambdaStatic::check);
System.out.println(resultList);
}
public static String generateRandomString() {
// Generate a random length less than 10
int length = new Random().nextInt(10);
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int randomIndex = new Random().nextInt(characters.length());
char randomChar = characters.charAt(randomIndex);
sb.append(randomChar);
}
return sb.toString();
}
public static List<String> checkStringList(List<String> stringList,String cs ,StringChecker c) {
List<String> resultList = new ArrayList<>();
for (String s : stringList) {
if (c.check(s, cs)) {
resultList.add(s);
}
}
return resultList;
}
public static boolean check(String s , String cs) {
return s.contains(cs);
}
}
通过上述列举的例子我们得知,Lambda 将方法作为参数传递的思想,我们可以理解成,双冒号::
调用静态方法实际上是通过这种Lambda获得一个参数作为方法的参数进行调用。
引用对象方法
引用对象方法,需要先new 对象,这是符合静态方法的特性的,我们沿用上面列举过的引用静态方法例子,做个修改:
public static void main(String[] args) {
// random string list
int size = 10;
List<String> stringList = new ArrayList<>();
for (int i = 0; i < size; i++) {
stringList.add(generateRandomString());
}
System.out.println("stringList = " + stringList);
String c = "a";
// using StringChecker to check string
StringChecker checker = (s, cs) -> s.contains(cs);
// 常规调用
checkStringList(stringList,c,checker);
// 使用 :: 类对象方法
List<String> resultList = checkStringList(stringList, c, new TestLambdaStatic() :: check);
System.out.println(resultList);
}
public static String generateRandomString() {
// Generate a random length less than 10
int length = new Random().nextInt(10);
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int randomIndex = new Random().nextInt(characters.length());
char randomChar = characters.charAt(randomIndex);
sb.append(randomChar);
}
return sb.toString();
}
public static List<String> checkStringList(List<String> stringList,String cs ,StringChecker c) {
List<String> resultList = new ArrayList<>();
for (String s : stringList) {
if (c.check(s, cs)) {
resultList.add(s);
}
}
return resultList;
}
public boolean check(String s, String cs) {
return s.contains(cs);
}
引用构造方法
有的接口中的方法会返回一个对象,例如java.util.function.Supplier
Supplier的源码:
public interface Supplier<T> {
T get();
}
我们拿这个 Supplier 引用 ArrayList 的构造方法:
public static void main(String[] args) {
List<String> stringList = getStringList(ArrayList::new);
stringList.add("Hello");
System.out.println(stringList);
}
/**
* 静态方法 获取一个List
*
* @param supplier 供给型接口
* @return List of String
*/
public static List<String> getStringList(Supplier<List<String>> supplier) {
return supplier.get();
}
2. Stream
注意,本小节将会出现大量的Lambda,可参照上文对照理解各Lambda的意思,加以练习。
Java 中的 聚合操作 是我们非常常用的集合操作之一。结合Lambda,我们可以通过聚合操作,以少量的代码,高效实现集合的筛选、过滤、合并等常见业务处理。聚合操作就是我们常见的Stream。
值得注意的是,此处的Stream,翻译过来是流,但是这个流和我们的IO流没有多少练习,此“流”非彼“流”。在一些教程中,为了区分,将这个Stream表示的一系列元素,结合管道,称之为聚合操作。
通过学习聚合操作,我们能进一步练习Lambda表达式,能更深刻体会Lambda方法作为参数的思想,能掌握更多数据处理方式。
2.1 管道的概念
管道指的是Stream一系列聚合操作,管道分为:
- 管道源:指的是要操作的数据,通常为我们要操作的集合。
- 中间操作: 每个中间操作,又会返回一个Stream,比如.filter()又返回一个Stream, 中间操作是“懒”操作,并不会真正进行遍历。
- 结束操作:当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。 结束操作不会返回Stream,但是会返回int、float、String、 Collection或者像forEach,什么都不返回, 结束操作才进行真正的遍历行为,在遍历的时候,才会去进行中间操作的相关判断。
通过对上述管道概念的介绍,显然,我们需要重点关注的是中间操作与结束操作,接下来,我们详细介绍这两个操作的常见操作方式。
2.2 常见中间操作
为了方便演示,我们先建一个类:
public class Animal {
private int age;
private String name;
public void voice(){
System.out.println("Animal voice");
}
public Animal() {
}
public Animal(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Animal{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2.2.1 filter 过滤器
使用聚合操作时,我们在集合后.stream()
之后,链式调用各种操作方法即可:
public static void main(String[] args) {
List<Animal> animalList = new ArrayList<>();
animalList.add(new Animal(1, "cat"));
animalList.add(new Animal(2, "dog"));
animalList.add(new Animal(3, "pig"));
animalList.add(new Animal(4, "bird"));
animalList.add(new Animal(5, "fish"));
// test filter
Stream<Animal> animalStream = animalList.stream().filter(animal -> animal.getAge() > 3);
animalStream.forEach(System.out::println);
}
2.2.2 distinct 去重
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(1);
numbers.add(1);
numbers.add(2);
numbers.stream().distinct().forEach(System.out::println);
}
注意,distinct() 默认按照 hashcode 去重,当我们要自定义根据某个属性的值进行去重时,可以自己封装一个去重方法作为filter
的参数传入:
/**
* 去重Predicate,filter参数
* @param keyExtractor 去重依据
* @param <T> 泛型类型
*/
public <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Map<Object,Boolean> seen = new ConcurrentHashMap<>();
return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}
2.2.3 sorted 排序
很好理解,我们直接举例子:
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(7);
numbers.add(1);
numbers.add(2);
numbers.add(9);
numbers.add(3);
numbers.add(2);
numbers.stream().distinct().sorted().forEach(System.out::println);
}
sorted 有一个 带参 重载方法,带一个Comparator 条件:
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(7);
numbers.add(1);
numbers.add(2);
numbers.add(9);
numbers.add(3);
numbers.add(2);
// 从大到小排序
numbers.stream().sorted((n1,n2) -> n1 > n2 ? -1 : 1).forEach(System.out::println);
2.2.4 limit 保留
使用 limit 保留前几个,类似于咱们 SQL 的 limit 分页:
List<Animal> animals = new ArrayList<>();
animals.add(new Animal(1, "cat"));
animals.add(new Animal(2, "dog"));
animals.add(new Animal(3, "pig"));
animals.stream().limit(2).forEach(System.out::println);
2.2.5 skip 忽略
与 limit 保留参数所值得的位数相对的,skip 忽略参数指定前几位,保留后几位:
List<Animal> animals = new ArrayList<>();
animals.add(new Animal(1, "cat"));
animals.add(new Animal(2, "dog"));
animals.add(new Animal(3, "pig"));
animals.stream().skip(1).forEach(System.out::println);
2.2.6 map 转换成其他类型的Stream
例如,将这个 animals 的 name 单独拿出来
List<Animal> animals = new ArrayList<>();
animals.add(new Animal(1, "cat"));
animals.add(new Animal(2, "dog"));
animals.add(new Animal(3, "pig"));
animals.stream().map(Animal::getName).forEach(System.out::println);
也可以单独写个匿名方法,获取特定的对象,匿名方法的返回值,即为转换后流的类型
List<Animal> animals = new ArrayList<>();
animals.add(new Animal(1, "cat"));
animals.add(new Animal(2, "dog"));
animals.add(new Animal(3, "pig"));
animals.stream().map(a -> "cat".equals(a.getName())).forEach(System.out::println);
2.2.7 其他操作
其他聚合操作,例如 mapToDouble 、peek 等等,用法都差不多,请读者自行查阅。
2.3 常见结束操作
类似于IO流需要关闭,聚合操作的流也有一系列的结束操作,下面介绍一些常见的结束操作。
2.3.1 foreach 遍历
管道结束操作,我们常用foreach遍历,沿用上文提到的例子:
List<Animal> animals = new ArrayList<>();
animals.add(new Animal(1, "cat"));
animals.add(new Animal(2, "dog"));
animals.add(new Animal(3, "pig"));
animals.stream().map(Animal::getName).forEach(System.out::println);
2.3.2 返回常用类型
有了以上的基础,接下来的操作都是直接结合 Lambda 表达式调用方法,方便且易于理解,因此接下来的操作类型大多都直接举例:
toArray() 返回数组
List<Animal> animals = new ArrayList<>();
animals.add(new Animal(1, "cat"));
animals.add(new Animal(2, "dog"));
animals.add(new Animal(3, "pig"));
Object[] objects = animals.stream().toArray();
for (Object object : objects) {
System.out.println(object);
}
collect()方法返回集合
List<Animal> animals = new ArrayList<>();
animals.add(new Animal(1, "cat"));
animals.add(new Animal(2, "dog"));
animals.add(new Animal(3, "pig"));
List<Animal> collect = animals.stream().skip(1).collect(Collectors.toList());
for (Animal animal : collect) {
System.out.println(animal);
}
2.3.3 校验与统计
校验
List<Animal> animals = new ArrayList<>();
animals.add(new Animal(1, "cat"));
animals.add(new Animal(2, "dog"));
animals.add(new Animal(3, "pig"));
// 是否任意匹配条件
boolean b = animals.stream().anyMatch(animal -> animal.getAge() == 1);
System.out.println(b);
统计相关
List<Animal> animals = new ArrayList<>();
animals.add(new Animal(1, "cat"));
animals.add(new Animal(2, "dog"));
animals.add(new Animal(3, "pig"));
animals.add(new Animal(4, "bird"));
IntSummaryStatistics intSummaryStatistics = animals.stream().mapToInt(Animal::getAge).summaryStatistics();
System.out.println(intSummaryStatistics.getMin());
System.out.println(intSummaryStatistics.getMax());
System.out.println(intSummaryStatistics.getAverage());
System.out.println(intSummaryStatistics.getCount());
System.out.println(intSummaryStatistics.getSum());
2.3.4 其他操作
其他聚合操作,例如 get(),reduce() 等等,用法都差不多,请读者自行查阅。
2.4 并行聚合操作
依赖于多线程,结合Lambda简化且方便的特性,提供并行的方式以应对一些特殊的使用场景。
并行的聚合操作仅仅是将 stream()换成 parallelStream(),之后的绝大部分方法都是一样的。
但是由于多线程本身的开销,并行的聚合操作效率不一定比原本的stream要高,我们稍作测试:
// prepare random Integer list
int size = 10000000;
List<Integer> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add((int) (Math.random() * size));
}
long start = System.currentTimeMillis();
// using stream filter to find all even number > half of size
List<Integer> result = list.stream()
.filter(i -> i % 2 == 0)
.filter(i -> i > size / 2)
.collect(Collectors.toList());
System.out.println("stream filter cost: " + (System.currentTimeMillis() - start));
// using parallel stream filter to find all even number > half of size
start = System.currentTimeMillis();
List<Integer> result2 = list.parallelStream()
.filter(i -> i % 2 == 0)
.filter(i -> i > size / 2)
.collect(Collectors.toList());
System.out.println("parallel stream filter cost: " + (System.currentTimeMillis() - start));
执行结果:
stream filter cost: 142
parallel stream filter cost: 586
3. 总结
本文首先介绍了Lambda的基本概念,并通过一些例子介绍了Lambda常用的两种形式:箭头简化匿名内部类,双冒号引用静态方法、对象方法、构造方法;
然后我们通过最常见的Stream聚合操作,练习了Lambda的常见使用场景,同时了解了一些常见聚合操作的方法,提供了更多的手段处理实际工作中的数据。
当然,还有很多API接口提供了 Lambda 表达式使用的空间,如 多线程 Runnable、Callable,LambdaQureyWrapper 等等。 总之,Lambda 简化匿名内部类的语法,方法作为参数传递的思想,大大提高了开发效率,非常的香。