本文内容部分参考自B站UP主三更草堂(侵权必删)
前言:为什么要学函数式编程?大家如果有进入公司实习或者看GitHub上著名项目后
会发现它们都有一个特点:很多地方使用了函数式编程,因此代码会很整洁,可读性也很高。这里我们举个例子:
这里我们想“查询未成年作家的评分在70以上的书籍,同时由于作家和书籍可能出现重复,所以需要进行去重”。因此我们没学函数式编程前会这样写代码:
List bookList = new ArrayList<>();
Set uniqueBookValues = new HashSet<>();
Set uniqueAuthorValues = new HashSet<>();
for (Author author : authors) {
if (uniqueAuthorValues.add(author)) {
if (author.getAge() < 18) {
List books = author.getBooks();
for (Book book : books) {
if (book.getScore() > 70) {
if (uniqueBookValues.add(book)) {
bookList.add(book);
}
}
}
}
}
}
System.out.println(bookList);
可以看到,这里由于我们的需求,导致出现了“嵌套地狱”,因此阅读起来十分麻烦!但是,如果我们使用了函数式编程后,代码会变成怎么样呢?如下:
List collect = authors.stream()
.distinct()
.filter(author -> author.getAge() < 18)
.map(author -> author.getBooks())
.flatMap(Collection::stream)
.filter(book -> book.getScore() > 70)
.distinct()
.collect(Collectors.toList());
System.out.println(collect);
可见,代码量肉眼可见的减少了,也变得美观,可读性也大大提升,因此这就是我们要学习函数式编程的原因!
函数式编程思想
1.1 什么是函数式编程?它的概念是什么?
拿我们之前学过的面向对象编程思想来说:
面向对象编程是“关注我们把哪些东西封装到一个类里面,然后用这个对象做了什么事情”。而函数式编程是“只关注我们对数据做了什么事情,进行了哪些操作”。(这里如果不太理解的话,接下来的Lambda表达式就是很好的例子,大家耐心往下看)
1.2 函数式编程的优点
这里我们上文也说了,其优点有:
代码简洁,开发快速
接近自然语言,易于理解
易于"并发编程"
Lambda表达式
2.1 Lambda表达式的描述
Lambda表达式是JDK8中的一个语法格式,它能够对以匿名类为参数的方法进行语法简化,是函数式编程思想的一个重要体现,也是我们接下来理解函数式编程,简化代码的一个重要知识。它让我们不用关注是什么对象。而是更关注我们对数据进行了什么操作。
2.2 基本格式
语法:(重写方法的参数列表)-> { 代码 }
注意!Lambda只能在对同时满足以下条件的代码进行简化:
①是某个接口的匿名内部类
②只有一个重载方法
看到上述语法,大家可能不太理解,确实有点抽象,因此我们这里举几个例子:
例一:
不使用Lambda表达式创建线程时的写法:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("CUG能不能别做早操了,球球了");
}
}).start();
使用Lambda表达式的写法:
new Thread(()->{ System.out.println("CUG能不能别做早操了,球球了");
}
).start();
例二:
不使用Lambda表达式时,我们在某个集合的sort方法里面实现Comparator接口的匿名内部类如下:
List authors = getAuthors();
authors.sort(new Comparator() {
@Override
public int compare(Author o1, Author o2) {
return 0;
}
});
使用Lambda表达式对上述代码进行优化后:
List authors = getAuthors();
authors.sort(((Author o1, Author o2) -> {
return o1.getAge()-o2.getAge();
}));
可见,这里就和我们上文说的:只关注实现的方法做了什么,而不去关注我们创建的类是什么、方法名叫什么。并且代码量也肉有可见的减少了,看来也更加美观,如果学会Lambda后,阅读起来也更加方便,可读性更高!
但是,大家会发现我们上文举例子中的Lambda表达式,比如例二,Idea会提示我们还可以优化成下面这样:
List authors = getAuthors();
authors.sort((o1, o2) -> o1.getAge()-o2.getAge());
看到这,大家就纳闷了,tnnd,怎么这么奇怪,怎么一下少了这么多东西,不对劲!不对劲起来了!这里我们就不得不提一下Lambda表达式的省略规则了:
参数类型可以省略
方法体只有一句代码时大括号return和唯一一句代码的分号可以省略
方法只有一个参数时小括号可以省略
大家看完省略规则,再去对比例二和我们Idea帮忙改的代码后,就会理解了!
Stream流
3.1 概述
Stream是Java8的一个新特性,使用的是函数式编程模式,如同它的名字一样,它可以被用来对集合或数组进行链状流式的操作。可以更方便的让我们对集合或数组操作。
为什么要用Stream流,大家可以回到文章顶部对比一下那两段代码:一个是我们平时正常写代码,不仅量大,不美观,还出现了“嵌套地狱”,阅读起来十分难受。另一个则使用了Stream流来处理我们需要的数据。
那么,Stream流怎么得到呢?它的操作有哪些?大家别急,我们接下来慢慢说。
3.2 常用操作
一般我们对流操作的三要素:①创建流 -> ②中间操作 -> ③终结操作
可以这样理解:要操作流,那肯定要创建流,然后对流进行一系列的中间操作(比如去重、排序),最后对流进行终结操作(比如输出、查找)。
3.2.1 创建Stream流
我们用到Stream流处理数据时,一般会对如下三种数据类型进行操作:
①单列集合
语法:
集合对象.stream()
比如:
List authors = getAuthors();
//通过集合对象.stream()获取集合对象的stream流
Stream stream = authors.stream();
②数组
语法:
Arrays.stream(数组名) `或者使用` Stream.of(数组名)`来创建
比如:
Integer[] arr = {1,2,3,4,5};
//通过Arrays.stream(数组名)获取数组的stream流
Stream stream = Arrays.stream(arr);
//通过Stream.of(数组名)获取数组的stream流
Stream stream2 = Stream.of(arr);
③双列集合(比如Map集合):
目前来说还没有提供双列集合直接转成对应Stream流的方法,因此我们将其转换成单列集合后再创建Stream流。
比如这里我们通过将Map集合的entry转换成Set集合,再来获取流:
Map map = new HashMap<>();
map.put("蜡笔小新",19);
map.put("黑子",17);
map.put("日向翔阳",16);
Stream> stream = map.entrySet().stream();
OK,现在我们学会如何获取Stream流后,应该来学学如何对Stream流进行操作了,也就是中间操作。(接下来操作都会使用作者举的例子:authors.stream())
3.2.2 中间操作
①filter:可以对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中。
比如:我们想要获取并打印所有成年(年龄不小于18)作家的名字,代码如下:
List authors = getAuthors();
authors.stream()
//通过filter过滤掉不满足条件的数据(保留满足条件的)
.filter(new Predicate() {
@Override
public boolean test(Author author) {
return author.getAge()>=18;
}
})
.forEach(new Consumer() {
@Override
public void accept(Author author) {
System.out.println(author.getName());
}
});
这里我们复习一下Lambda表达式,对上述代码简化后如下:
List authors = getAuthors();
authors.stream()
//通过filter过滤掉不满足条件的数据(保留满足条件的)
.filter(author -> author.getAge() >= 18)
.forEach(author -> System.out.println(author.getName()));
(接下来我们的所有案例都直接用Lambda表达式优化后的代码)
②map:用于对流中的元素进行计算或转换。
对于转换这个功能来说,比如:我们想将authors.stream()流里面的数据转成装着age数据的流,我们可以这样:
List authors = getAuthors();
authors.stream()
.map(author -> author.getAge())
.forEach(age -> System.out.println(age));
这里我们通过跟踪代码来看一下:
从Idea的提示来看,在经过Map操作后,Stream流中的数据类型从Author变为了Integer。可见,Map是可以将Stream流中的数据进行类型转换的,转换的类型取决于你返回的数据类型(比如我们上图返回的是age,因此是Integer类型)。
对于转换这个功能来说,比如我们想在上述代码的基础上,给每个作家的年龄再加上10,我们可以这样:
List authors = getAuthors();
authors.stream()
.map(author -> author.getAge())
.map(age->age+10)
.forEach(age -> System.out.println(age));
③distinct:用来去除流中重复的数据。
例如我们想去除作家流中所有重复的数据,并且打印去重后所有作家的名字,可以这样:
List authors = getAuthors();
authors.stream()
.distinct()
.forEach(author -> System.out.println(author.getName()));
④sorted:对流中所有元素进行排序
在使用sorted()之前,我们需要回顾一下:之前我们使用Collections工具类中的sort函数时,除了Java指定的几个基本类外(比如Integer、Double、Float),我们对其他类排序的时候都需要让它们继承Comparable接口并重写方法或者在sort里面new一个Compartor的匿名内部类。这里很好理解为什么,因为正常的一些整型数据,计算机肯定知道如何为它们排序,但是如果是我们自定义的类型(比如Author类),那计算机怎么知道它们如何排序呢?所以这里sorted方法有两种用法:
a.调用空参的sorted()方法,需要流中的元素是实现了Comparable。
List authors = getAuthors();
// 对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。
authors.stream()
.distinct()
.sorted()
.forEach(author -> System.out.println(author.getAge()));
b.调用sorted()方法,并且在()内部new 一个比较器(Compartor)的内部类。
List authors = getAuthors();
// 对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。
authors.stream()
.distinct()
.sorted(new Comparator() {
@Override
public int compare(Author o1, Author o2) {
return o1.getAge()-o2.getAge();
}
})
.forEach(author -> System.out.println(author.getAge()));
用Lambda表达式简化上式后变为:
List authors = getAuthors();
// 对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。
authors.stream()
.distinct()
.sorted((o1, o2) -> o2.getAge()-o1.getAge())
.forEach(author -> System.out.println(author.getAge()));
⑤limit:可以设置流的最大长度,超出的部分将被抛弃。
比如我们的作家流,我们只想获取前两个数据,那么可以这样:
List authors = getAuthors();
authors.stream()
.limit(2)
.forEach(author -> System.out.println(author.getAge()));
注意:limit只会去掉超出长度后的数据!比如流有十条数据,我们设置limit(5),便会去除后5条。但是如果我们设置limit(11),就不会去除数据。
⑥skip:跳过流中的前n个元素,返回剩下的元素。
比如我们要跳过作家流的前两个数据,可以这样:
List authors = getAuthors();
authors.stream()
.skip(2)
.forEach(author -> System.out.println(author.getAge()));
⑦flatMap:可以把一个对象转换成多个对象作为流中的元素。
map只能把一个对象转换成另一个对象来作为流中的元素。而flatMap可以把一个对象转换成多个对象作为流中的元素。
上述这句话怎么理解呢,我们先来看一下作家类的结构:
我们想获取所有作家的所有作品的流,那么我们肯定会想到用之前的Map,将作家流转成作品流,如下:
List authors = getAuthors();
authors.stream()
.map(author -> author.getBooks())
.forEach(books -> System.out.println(books));
但是可以发现,在Idea给我们的提示中,这里map转换后,流中的数据类型并不是我们想要的Book,而是List
遇到这种情况,那就无法继续用map来转换了,因此我们要用新的中间操作“flatMap”
List authors = getAuthors();
authors.stream()
.flatMap(new Function>() {
@Override
public Stream> apply(Author author) {
return author.getBooks().stream();
}
})
.forEach(books -> System.out.println(books));
使用Lambda表达式简化后:
List authors = getAuthors();
authors.stream()
.flatMap((Function>) author -> author.getBooks().stream())
.forEach(books -> System.out.println(books));
可以看到,flatMap的返回值是Stream流,从Idea的提示也可以看出,此时流中的数据类型变为了Stream
虽然显示的是Stream
好了,以上是我们全部会使用的中间操作,中间操作是用来根据需求处理流中的数据的,那么处理完后,我们要使用流中的数据,也就是我们接下来要说的:“终结操作”。
3.2.2 终结操作
①forEach:对流中的元素进行遍历操作,我们通过传入的参数去指定对遍历到的元素进行什么具体操作。
这个操作大家应该不陌生,因为我们上面每个例子最后都是用forEach来使用我们对流处理后的数据的。因此我们这里不做过多介绍,直接来举个例子:我们要输出作家流中所有人的名字可以这样:
List authors = getAuthors();
authors.stream()
.forEach(author -> System.out.println(author.getName()));
②count:可以用来获取当前流中元素的个数。
作用就是我们获取流,并且对流中的数据进行处理后(中间操作),获取剩下流中元素的个数。
比如我们想获取不重复作家的个数,可以这样:
List authors = getAuthors();
//count是不重复作家的个数
long count = authors.stream()
.distinct()
.count();
③max、min:用来获取流中的最大、最小值。
比如我们想获取作家中所有书籍评分的最大和最小值,并且打印出来:
// 分别获取这些作家的所出书籍的最高分和最低分并打印。
//Stream -> Stream ->Stream ->求值
List authors = getAuthors();
Optional max = authors.stream()
.flatMap(author -> author.getBooks().stream())
.map(book -> book.getScore())
.max((score1, score2) -> score1 - score2);
Optional min = authors.stream()
.flatMap(author -> author.getBooks().stream())
.map(book -> book.getScore())
.min((score1, score2) -> score1 - score2);
System.out.println(max.get());
System.out.println(min.get());
这里可以看到,我们是分别获取了两个流来求最值的,因为这里有个注意事项:一个流只能有一个终结操作,终结操作后就不能继续操作流了。
④collect:把当前流转换成一个集合。
比如我们要获取一个存放所有作者名字的List集合:
// 获取一个存放所有作者名字的List集合。
List authors = getAuthors();
List nameList = authors.stream()
.map(author -> author.getName())
.collect(Collectors.toList());
System.out.println(nameList);
比如我们要获取一个所有书名的Set集合:
// 获取一个所有书名的Set集合。
List authors = getAuthors();
Set books = authors.stream()
.flatMap(author -> author.getBooks().stream())
.collect(Collectors.toSet());
System.out.println(books);
比如我们要获取一个Map集合,map的key为作者名,value为List
// 获取一个Map集合,map的key为作者名,value为List
List authors = getAuthors();
Map> map = authors.stream()
.distinct()
.collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks()));
System.out.println(map);
⑥anyMatch:可以用来判断是否有任意符合匹配条件的元素,结果为boolean类型。
比如判断流中是否有年龄在29以上的作家:
// 判断是否有年龄在29以上的作家
List authors = getAuthors();
boolean flag = authors.stream()
.anyMatch(author -> author.getAge() > 29);
System.out.println(flag);
⑦allMatch:可以用来判断是否都符合匹配条件,结果为boolean类型。如果都符合结果为true,否则结果为false。
比如判断是否作家都是成年人:
List authors = getAuthors();
boolean b = authors.stream()
.allMatch(author -> author.getAge()>=18);
System.out.println(b);
⑧noneMatch:可以判断流中的元素是否都不符合匹配条件。如果都不符合结果为true,否则结果为false。
比如判断作家中是不是所有人年龄都没超过100岁:
// 判断作家是否都没有超过100岁的。
List authors = getAuthors();
boolean b = authors.stream()
.noneMatch(author -> author.getAge() > 100);
System.out.println(b);
⑨findAny:获取流中的任意一个元素。因为是随机获取!所以该方法没有办法保证获取的一定是流中的第一个元素。
比如我们随机获取一个年龄大于18的作家,如果存在就输出他的名字:
// 获取任意一个年龄大于18的作家,如果存在就输出他的名字
List authors = getAuthors();
Optional optionalAuthor = authors.stream()
.filter(author -> author.getAge()>18)
.findAny();
optionalAuthor.ifPresent(author -> System.out.println(author.getName()));
这里我们用到了Optional这个类,不懂的同学可以去学一下。大家看他的方法ifPresent也应该能猜出来内部帮我们判断了一下:如果我们获取的数据存在则输出,不存在则不输出。
⑩findFirst:获取流中的第一个元素。
比如我们获取一个年龄最小的作家,并输出他的姓名:
// 获取一个年龄最小的作家,并输出他的姓名。
List authors = getAuthors();
Optional first = authors.stream()
.sorted((o1, o2) -> o1.getAge() - o2.getAge())
.findFirst();
first.ifPresent(author -> System.out.println(author.getName()));
终结操作之疑难点——reduce:对流中的数据按照你指定的计算方式计算出一个结果。
reduce的作用是把stream中的元素给组合起来,我们可以传入一个初始值,它会按照我们的计算方式依次拿流中的元素和初始化值进行计算,计算结果再和后面的元素计算。
①reduce两个参数的重载形式内部的计算方式如下:
T result = identity;
for (T element : this stream)
result = accumulator.apply(result, element)
return result;
其中identity就是我们可以通过方法参数传入的初始值,accumulator的apply具体进行什么计算也是我们通过方法参数来确定的。
比如我们使用reduce求所有作家年龄的和:
// 使用reduce求所有作者年龄的和
List authors = getAuthors();
Integer sum = authors.stream()
.distinct()
.map(author -> author.getAge())
.reduce(0, (result, element) -> result + element);
System.out.println(sum);
②reduce一个参数的重载形式内部的计算方式如下:
boolean foundAny = false;
T result = null;
for (T element : this stream) {
if (!foundAny) {
foundAny = true;
result = element;
}
else
result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();
如果用一个参数的重载方法去求所有作家的年龄之和代码如下:
List authors = getAuthors();
Optional reduce = authors.stream()
.map(author -> author.getAge())
.reduce((result, age) -> result + age);
reduce.ifPresent(System.out::println);
可见,这二者的区别就是:两个参数的我们可以赋一个初始值;而一个参数的初始值则没有,程序会将流中第一个元素的值赋给它,然后再与流之后的元素进行计算。
好了,以上是我们流操作所有的方法,这里有一些注意事项大家需要知道一下:
惰性求值(如果没有终结操作,没有中间操作是不会得到执行的)
流是一次性的(一旦一个流对象经过一个终结操作后。这个流就不能再被使用)
不会影响原数据(我们在流中可以多数据做很多处理。但是正常情况下是不会影响原来集合中的元素的。这往往也是我们期望的)