Java 函数式编程笔记(草稿)
Java
2019-12-21
609
0
流(Streams)
流介绍(Introducing streams)
无状态和有状态(stateless vs. stateful)
诸如map
或filter
等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态(stateless)
的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。
但诸如reduce、sum、max
等操作需要内部状态
来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int
或double
。不管流中有多少元素要处理,内部状态都是有界(bounded size)
的。
相反,诸如sort
或distinct
等操作一开始都和filter
和map
差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作(stateful operations)。
中间操作和终端操作
可以连接起来的流操作称为中间操作(intermedicate operations),如 filter、map、limit,关闭流的操作称为终端操作。关闭流的操作称为终端操作(terminal operations),如 collect。
诸如filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
操作 | 类型 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 |
---|---|---|---|---|
filter | 中间 | Stream<T> | Predicate<T> | T -> boolean |
distinct | 中间(有状态-无界) | Stream<T> | ||
skip | 中间(有状态-有界) | Stream<T> | long | |
limit | 中间(有状态-有界) | Stream<T> | long | |
map | 中间 | Stream<R> | Function<T, R> | T -> R |
flatMap | 中间 | Stream<R> | Function<T, Stream<R>> | T -> Stream<R> |
sorted | 中间(有状态-无界) | Stream<T> | Comparator<T> | (T, T) -> int |
anyMatch | 终端 | boolean | Predicate<T> | T -> boolean |
noneMatch | 终端 | boolean | Predicate<T> | T -> boolean |
allMatch | 终端 | boolean | Predicate<T> | T -> boolean |
findAny | 终端 | Optional<T> | ||
findFirst | 终端 | Optional<T> | ||
forEach | 终端 | void | Consumer<T> | T -> void |
collect | 终端 | R | Collector<T, A, R> | |
reduce | 终端(有状态-有界) | Optional<T> | BinaryOperator<T> | (T, T) -> T |
count | 终端 | long |
List<String> names =
menu.stream()
.filter(d -> {
System.out.println("filtering" + d.getName());
return d.getCalories() > 300;
}) ←─打印当前筛选的菜肴
.map(d -> {
System.out.println("mapping" + d.getName());
return d.getName();
}) ←─提取菜名时打印出来
.limit(3)
.collect(toList());
System.out.println(names);
/**
输出
filtering pork
mapping pork
filtering beef
mapping beef
filtering chicken
mapping chicken
[pork, beef, chicken]
**/
上述代码有好几种优化利用了流的延迟性质。第一,尽管很多菜的热量都高于300卡路里,但只选出了前三个!这是因为limit
操作和一种称为短路(short-circuiting)的技巧。第二,尽管filter
和map
是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环合并(loop fusion))。
使用流(Working with streams)
筛选和切片(Filtering and slicing)
filter 筛选
接受Lambda,从流中排除某些元素。
利用Stream和Lambda表达式顺序或并行地从一个列表里筛选比较重的苹果
// 顺序处理
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples =
inventory.stream().filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
// 并行处理
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples =
inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
返回低热量的菜肴名称的,并按照卡路里排序
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName =
menu.stream()
.filter(d -> d.getCalories() < 400) ←─选出400卡路里以下的菜肴
.sorted(comparing(Dish::getCalories)) ←─按照卡路里排序
.map(Dish::getName) ←─提取菜肴的名称
.collect(toList()); ←─将所有名称保存在List中
// 多核架构并行执行这段代码,你只需要把stream()换成parallelStream():
List<String> lowCaloricDishesName =
menu.parallelStream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dishes::getCalories))
.map(Dish::getName)
.collect(toList());
返回唯一元素(distinct)
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);
限制返回的元素数量(limit)
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(toList());
扔掉前面n 个元素(skip)
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());
映射(Mapping)
接受一个Lambda,将元素转换成其他形式或提取信息。
找出每道菜的名称有多长(map)
List<Integer> dishNameLengths = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(toList());
给定[1, 2, 3, 4, 5],应该返回[1, 4, 9, 16, 25]。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares =
numbers.stream()
.map(n -> n * n)
.collect(toList());
给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。
List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs =
numbers1.stream()
.flatMap(i -> numbers2.stream()
.map(j -> new int[]{i, j})
)
.collect(toList());
总和能被3整除的数对呢?例如(2, 4)和(3, 3)是可以的。
List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs =
numbers1.stream()
.flatMap(i ->
numbers2.stream()
.filter(j -> (i + j) % 3 == 0)
.map(j -> new int[]{i, j})
)
.collect(toList());
流的扁平化(Flattening streams)
给定单词列表["Hello","World"]
,你想要返回列表["H","e","l", "o","W","r","d"]
。
你可能会认为这很容易,你可以把每个单词映射成一张字符表,然后调用distinct来过滤重复的字符。这个方法的问题在于,传递给map方法的Lambda为每个单词返回了一个String[]
(String
列表)。因此,map返回的流实际上是Stream<String[]>
类型的。你真正想要的是用Stream<String>
来表示一个字符流。
words.stream()
.map(word -> word.split(""))
.distinct()
.collect(toList());
使用flatMap
flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
List<String> uniqueCharacters =
words.stream()
.map(w -> w.split("")) ←─将每个单词转换为由其字母构成的数组
.flatMap(Arrays::stream) ←─将各个生成流扁平化为单个流
.distinct()
.collect(Collectors.toList());
给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]
List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs =
numbers1.stream()
.flatMap(i -> numbers2.stream()
.map(j -> new int[]{i, j})
)
.collect(toList());
扩展前一个例子,只返回总和能被3整除的数对。例如(2, 4)和(3, 3)是可以的。
List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs =
numbers1.stream()
.flatMap(i ->
numbers2.stream()
.filter(j -> (i + j) % 3 == 0)
.map(j -> new int[]{i, j})
)
.collect(toList());
查找和匹配(Finding and matching)
anyMatch
anyMatch
方法返回一个boolean
,因此是一个终端操作。
// 检查谓词是否至少匹配一个元素
if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
allMatch
allMatch
方法的工作原理和anyMatch
类似,但它会看看流中的元素是否都能匹配给定的谓词。比如,你可以用它来看看菜品是否有利健康(即所有菜的热量都低于1000卡路里):
boolean isHealthy = menu.stream()
.allMatch(d -> d.getCalories() < 1000);
noneMatch
和allMatch
相对的是noneMatch
。它可以确保流中没有任何元素与给定的谓词匹配。比如,你可以用noneMatch
重写前面的例子:
boolean isHealthy = menu.stream()
.noneMatch(d -> d.getCalories() >= 1000);
findAny
findAny
方法将返回当前流中的任意元素。它可以与其他流操作结合使用。比如,你可能想找到一道素食菜肴。你可以结合使用filter
和findAny
方法来实现这个查询:
Optional<Dish> dish =
menu.stream()
.filter(Dish::isVegetarian)
.findAny();
上文中Optional<T>
类(java.util.Optional
)是一个容器类,代表一个值存在或不存在。在上面的代码中,findAny
可能什么元素都没找到。Java 8的库设计人员引入了Optional<T>
,这样就不用返回众所周知容易出问题的null了。Optional
里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形:
isPresent()
将在Optional
包含值的时候返回true
, 否则返回false
。ifPresent(Consumer<T> block)
会在值存在的时候执行给定的代码块。T get()
会在值存在时返回值,否则抛出一个NoSuchElement
异常。T orElse(T other)
会在值存在时返回值,否则返回一个默认值。
在前面的代码中你需要显式地检查Optional对象中是否存在一道菜可以访问其名称:
menu.stream()
.filter(Dish::isVegetarian)
.findAny() ←─返回一个Optional<Dish>
.ifPresent(d -> System.out.println(d.getName()); ←─如果包含一个值就打印它,否则什么都不做
findFirst
有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由List
或排序好的数据列生成的流)。为此有一个findFirst
方法,它的工作方式类似于findany
。例如,给定一个数字列表,下面的代码能找出第一个平方能被3整除的数:
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree =
someNumbers.stream()
.map(x -> x * x)
.filter(x -> x % 3 == 0)
.findFirst(); // 9
归约(Reducing)
reduce
reduce
接受两个参数:
- 一个初始值,这里是0;
- 一个
BinaryOperator<T>
来将两个元素结合起来产生一个新值,这里我们用的是lambda (a, b) -> a + b
。
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int product = numbers.stream().reduce(1, (a, b) -> a * b);
下图展示了reduce操作是如何作用于一个流的:Lambda反复结合每个元素,直到流被归约成一个值。
你可以使用方法引用让这段代码更简洁。在Java 8中,Integer
类现在有了一个静态的sum
方法来对两个数求和,这恰好是我们想要的,用不着反复用Lambda写同一段代码了:
int sum = numbers.stream().reduce(0, Integer::sum);
无初始值
reduce
还有一个重载的变体,它不接受初始值,但是会返回一个Optional
对象:
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
为什么它返回一个Optional<Integer>
呢?考虑流中没有任何元素的情况。reduce
操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional
对象里,以表明和可能不存在。
计算最大值和最小值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
付诸实践
找出2011年的所有交易并按交易额排序(从低到高)
List<Transaction> tr2011 =
transactions.stream()
.filter(transaction -> transaction.getYear() == 2011) ←─给filter传递一个谓词来选择2011年的交易
.sorted(comparing(Transaction::getValue)) ←─按照交易额进行排序
.collect(toList()); ←─将生成的Stream中的所有元素收集到一个List中
交易员都在哪些不同的城市工作过
List<String> cities =
transactions.stream()
.map(transaction -> transaction.getTrader().getCity()) ←─提取与交易相关的每位交易员的所在城市
.distinct() ←─只选择互不相同的城市
.collect(toList());
// 也可以去掉 distinct()该用 toSet()
Set<String> cities =
transactions.stream()
.map(transaction -> transaction.getTrader().getCity())
.collect(toSet());
查找所有来自于剑桥的交易员,并按姓名排序
List<Trader> traders =
transactions.stream()
.map(Transaction::getTrader) ←─从交易中提取所有交易员
.filter(trader -> trader.getCity().equals("Cambridge")) ←─仅选择位于剑桥的交易员
.distinct() ←─确保没有任何重复
.sorted(comparing(Trader::getName)) ←─对生成的交易员流按照姓名进行排序
.collect(toList());
返回所有交易员的姓名字符串,按字母顺序排序
String traderStr =
transactions.stream()
.map(transaction -> transaction.getTrader().getName()) ←─提取所有交易员姓名,生成一个Strings构成的Stream
.distinct() ←─只选择不相同的姓名
.sorted() ←─对姓名按字母顺序排序
.reduce("", (n1, n2) -> n1 + n2); ←─逐个拼接每个名字,得到一个将所有名字连接起来的String
// 上面的解决方案效率不高(所有字符串都被反复连接,每次迭代的时候都要建立一个新的String对象)下面是一个效率更高的方案使用joining(其内部会用到StringBuilder
String traderStr =
transactions.stream()
.map(transaction -> transaction.getTrader().getName())
.distinct()
.sorted()
.collect(joining());
有没有交易员是在米兰工作的
boolean milanBased =
transactions.stream()
.anyMatch(transaction -> transaction.getTrader()
.getCity()
.equals("Milan")); ←─把一个谓词传递给anyMatch,检查是否有交易员在米兰工作
打印生活在剑桥的交易员的所有交易额
transactions.stream()
.filter(t -> "Cambridge".equals(t.getTrader().getCity())) ←─选择住在剑桥的交易员所进行的交易
.map(Transaction::getValue) ←─提取这些交易的交易额
.forEach(System.out::println); ←─打印每个值
所有交易中,最高的交易额是多少
Optional<Integer> highestValue =
transactions.stream()
.map(Transaction::getValue) ←─提取每项交易的交易额
.reduce(Integer::max); ←─计算生成的流中的最大值
找到交易额最小的交易
Optional<Transaction> smallestTransaction =
transactions.stream()
.reduce((t1, t2) ->
t1.getValue() < t2.getValue() ? t1 : t2); ←─通过反复比较每个交易的交易额,找出最小的交易
// 流支持min和max方法,它们可以接受一个Comparator作为参数,指定计算最小或最大值时要比较哪个键值:
Optional<Transaction> smallestTransaction =
transactions.stream()
.min(comparing(Transaction::getValue));
数字流(Numeric streams)
原始类型流转化
我们在前面看到了可以使用reduce
方法计算流中元素的总和。例如,你可以像下面这样计算菜单的热量:
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
这段代码的问题是,它有一个暗含的装箱成本。每个Integer
都必须拆箱成一个原始类型,再进行求和。要是可以直接像下面这样调用sum
方法,岂不是更好?但这是不可能的。问题在于map方法会生成一个Stream<T>
。虽然流中的元素是Integer类型,但Streams接口没有定义sum方法。
int calories = menu.stream()
.map(Dish::getCalories)
.sum();
Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream
、DoubleStream
和LongStream
,分别将流中的元素特化为int
、long
和double
,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max
。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int
和Integer
之间的效率差异。
1.映射到数值流(Mapping to a numeric stream)
将流转换为特殊版本的常用方法是mapToInt
、mapToDouble
和mapToLong
。这些方法和前面说的map
方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream<T>
。例如,你可以像下面这样用mapToInt
对menu
中的卡路里求和:
int calories = menu.stream() ←─返回一个Stream<Dish>
.mapToInt(Dish::getCalories) ←─返回一个IntStream
.sum();
这里mapToInt
会从每道菜中提取热量(用一个Integer
表示),并返回一个IntStream
(而不是一个Stream<Integer>
)。然后你就可以调用IntStream
接口中定义的sum
方法,对卡路里求和了!请注意,如果流是空的,sum
默认返回0
。IntStream
还支持其他的方便方法,如max
、min
、average
等。
2.转回刘对象(Converting back to a stream of objects)
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); ←─将Stream 转换为数值流
Stream<Integer> stream = intStream.boxed(); ←─将数值流转换为Stream
3.默认值OptionalInt
求和的那个例子很容易,因为它有一个默认值:0
。但是,如果你要计算IntStream
中的最大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢?前面我们介绍了Optional
类,这是一个可以表示值存在或不存在的容器。Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt
、OptionalDouble
和OptionalLong。 `` 例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个
OptionalInt`:
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
现在,如果没有最大值的话,你就可以显式处理OptionalInt
去定义一个默认值了:
int max = maxCalories.orElse(1); ←─如果没有最大值的话,显式提供一个默认最大值
数值范围
Java 8引入了两个可以用于IntStream
和LongStream
的静态方法,帮助生成这种范围:range
和rangeClosed
。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range
是不包含结束值的,而rangeClosed
则包含结束值。让我们来看一个例子:
IntStream evenNumbers = IntStream.rangeClosed(1, 100) ←─表示范围[1, 100]
.filter(n -> n % 2 == 0); ←─一个从1到100的偶数流
System.out.println(evenNumbers.count()); ←─从1 到100 有50个偶数
实践:勾股数(Pythagorean triples)
通过以上的学习我们来生成1~100之间复合勾股定理组数
Stream<int[]> pythagoreanTriples =
IntStream.rangeClosed(1, 100).boxed()
.flatMap(a ->
IntStream.rangeClosed(a, 100)
.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.mapToObj(b ->
new int[]{a, b, (int)Math.sqrt(a * a + b * b)})
);
上面的解决办法并不是最优的,因为你要求两次平方根。让代码更为紧凑的一种可能的方法是,先生成所有的三元数(aa, bb, aa+bb),然后再筛选符合条件的:
Stream<double[]> pythagoreanTriples2 =
IntStream.rangeClosed(1, 100).boxed()
.flatMap(a ->
IntStream.rangeClosed(a, 100)
.mapToObj(
b -> new double[]{a, b, Math.sqrt(a*a + b*b)}) ←─产生三元数
.filter(t -> t[2] % 1 == 0)); ←─元组中的第三个元素必须是整数
构建流(Building streams)
由值创建流(Streams from values)
静态方法Stream.of
,例如,以下代码直接使用Stream.of
创建了一个字符串流。
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
你可以使用empty
得到一个空流,如下所示:
Stream<String> emptyStream = Stream.empty();
由数组创建流
静态方法Arrays.stream
例如,你可以将一个原始类型int
的数组转换成一个IntStream
。
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum(); ←─总和是41
由文件生成流
Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流。使用你迄今所学的内容,你可以用这个方法看看一个文件中有多少各不相同的词:
long uniqueWords = 0;
try(Stream<String> lines =
Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){ ←─流会自动关闭
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) ←─生成单词流
.distinct() ←─删除重复项
.count(); ←─数一数有多少各不相同的单词
}
catch(IOException e){
←─如果打开文件时出现异常则加以处理
}
创建无限流(creating infinite streams)
Stream API提供了两个静态方法来从函数生成流:Stream.iterate
和Stream.generate
。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate
和generate
产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
1.迭代(iterate)
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
iterate
方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator<t>
类型)。
斐波纳契元组序列
Stream.iterate(new int[]{0, 1},
t -> new int[]{t[1],t[0] + t[1]})
.limit(10)
.map(t -> t[0])
.forEach(System.out::println);
2.生成(generate)
与iterate
方法类似,generate
方法也可让你按需生成一个无限流。但generate
不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>
类型的Lambda提供新的值。我们先来看一个简单的用法:
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
这段代码将生成一个流,其中有五个0到1之间的随机双精度数。
你可能想知道,generate
方法还有什么用途。我们使用的供应源(指向Math.random
的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。你可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。举个例子,我们将展示如何利用generate
斐波纳契数列,这样你就可以和用iterate
方法的办法比较一下。但很重要的一点是,在并行代码中使用有状态的供应源是不安全的。因此下面的代码仅仅是为了内容完整,应尽量避免使用!
我们在这个例子中会使用IntStream说明避免装箱操作的代码。IntStream
的generate
方法会接受一个IntSupplier
,而不是Supplier<t>
。例如,可以这样来生成一个全是1的无限流:
IntStream ones = IntStream.generate(() -> 1);
Lambda允许你创建函数式接口的实例,只要直接内联提供方法的实现就可以。你也可以像下面这样,通过实现IntSupplier
接口中定义的getAsInt
方法显式传递一个对象(虽然这看起来是无缘无故地绕圈子,也请你耐心看):
IntStream twos = IntStream.generate(new IntSupplier(){
public int getAsInt(){
return 2;
}
});
generate
方法将使用给定的供应源,并反复调用getAsInt
方法,而这个方法总是返回2
。但这里使用的匿名类和Lambda的区别在于,匿名类可以通过字段定义状态,而状态又可以用getAsInt
方法来修改。这是一个副作用的例子。你迄今见过的所有Lambda都是没有副作用的;它们没有改变任何状态。
回到斐波纳契数列的任务上,你现在需要做的是建立一个IntSupplier
,它要把前一项的值保存在状态中,以便getAsInt
用它来计算下一项。此外,在下一次调用它的时候,还要更新IntSupplier
的状态。下面的代码就是如何创建一个在调用时返回下一个斐波纳契项的IntSupplier
:
IntSupplier fib = new IntSupplier(){
private int previous = 0;
private int current = 1;
public int getAsInt(){
int oldPrevious = this.previous;
int nextValue = this.previous + this.current;
this.previous = this.current;
this.current = nextValue;
return oldPrevious;
}
};
IntStream.generate(fib).limit(10).forEach(System.out::println);
聚合数据(Collecting data)
collect
对交易按照货币分组
// Java 7
Map<Currency, List<Transaction>> transactionsByCurrencies =
new HashMap<>(); ←─建立累积交易分组的Map
for (Transaction transaction : transactions) { ←─迭代Transaction的List
Currency currency = transaction.getCurrency(); ←─提取Transaction的货币
List<Transaction> transactionsForCurrency =
transactionsByCurrencies.get(currency);
if (transactionsForCurrency == null) { ←─如果分组Map中没有这种货币的条目,就创建一个
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies
.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction); ←─将当前遍历的Transaction加入同一货币的Transaction的List
}
// Java 8
import static java.util.stream.Collectors.toList;
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream()
.filter((Transaction t) -> t.getPrice() > 1000) ←─筛选金额较高的交易
.collect(groupingBy(Transaction::getCurrency)); ←─按货币分组
数一数菜单里有多少种菜
long howManyDishes = menu.stream().collect(Collectors.counting());
// 这还可以写得更为直接:
long howManyDishes = menu.stream().count();
如果你已导入了Collectors
类的所有静态工厂方法,那你就可以写counting()
而用不着写Collectors.counting()
之类的了。
import static java.util.stream.Collectors.*;
查找流中的最大值和最小值
假设你想要找出菜单中热量最高的菜。你可以使用两个收集器,Collectors.maxBy
和Collectors.minBy
,来计算流中的最大或最小值。这两个收集器接收一个Comparator
参数来比较流中的元素。你可以创建一个Comparator
来根据所含热量对菜肴进行比较,并把它传递给Collectors.maxBy
:
Comparator<Dish> dishCaloriesComparator =
Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish =
menu.stream()
.collect(maxBy(dishCaloriesComparator));
汇总、平均
Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。举个例子来说,你可以这样求出菜单列表的总热量:
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
但汇总不仅仅是求和;还有Collectors.averagingInt,连同对应的averagingLong和averagingDouble可以计算数值的平均数:
double avgCalories =
menu.stream().collect(averagingInt(Dish::getCalories));
使用summarizingInt
工厂方法返回的收集器
通过一次summarizing操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值:
IntSummaryStatistics menuStatistics =
menu.stream().collect(summarizingInt(Dish::getCalories));
// 结果
IntSummaryStatistics{count=9, sum=4300, min=120,
average=477.777778, max=800}
连接字符串
joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。这意味着你把菜单中所有菜肴的名称连接起来,如下所示:
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
请注意,joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。此外还要注意,如果Dish类有一个toString方法来返回菜肴的名称,那你无需用提取每一道菜名称的函数来对原流做映射就能够得到相同的结果:
String shortMenu = menu.stream().collect(joining());
但该字符串的可读性并不好。幸好,joining工厂方法有一个重载版本可以接受元素之间的分界符,这样你就可以得到一个逗号分隔的菜肴名称列表:
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
广义的归约汇总(Generalized summarization with reduction)
我们已经讨论的所有收集器,都是一个可以用reducing工厂方法定义的归约过程的特殊情况而已。Collectors.reducing工厂方法是所有这些特殊情况的一般化。可以说,先前讨论的案例仅仅是为了方便程序员而已。(但是,请记得方便程序员和可读性是头等大事!)例如,可以用reducing方法创建的收集器来计算你菜单的总热量,如下所示:
int totalCalories = menu.stream().collect(reducing(
0, Dish::getCalories, (i, j) -> i + j));
// 简化版本
int totalCalories = menu.stream().collect(reducing(0, ←─初始值
Dish::getCalories, ←─转换函数
Integer::sum)); ←─累积函数
- 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
- 第二个参数将菜肴转换成一个表示其所含热量的int。
- 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和。
同样,你可以使用下面这样单参数形式
的reducing来找到热量最高的菜,如下所示:
Optional<Dish> mostCalorieDish =
menu.stream().collect(reducing(
(d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
还有另一种方法不使用收集器也能执行相同操作——将菜肴流映射为每一道菜的热量,然后用前一个版本中使用的方法引用来归约得到的流:
int totalCalories =
menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
请注意,就像流的任何单参数reduce操作一样,reduce(Integer::sum)返回的不是int而是Optional<Integer>
,以便在空流的情况下安全地执行归约操作。然后你只需用Optional对象中的get方法来提取里面的值就行了。请注意,在这种情况下使用get方法是安全的,只是因为你已经确定菜肴流不为空。一般来说,使用允许提供默认值的方法,如orElse或orElseGet来解开Optional中包含的值更为安全。最后,更简洁的方法是把流映射到一个IntStream,然后调用sum方法,你也可以得到相同的结果:
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
分组(grouping)
假设你要把菜单中的菜按照类型进行分类,有肉的放一组,有鱼的放一组,其他的都放另一组。用Collectors.groupingBy
工厂方法返回的收集器就可以轻松地完成这项任务,
Map<Dish.Type, List<Dish>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));
// 其结果是下面的Map:
{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza],
MEAT=[pork, beef, chicken]}
在分组过程中对流中的项目进行分类
你可能想把热量不到400卡路里的菜划分为“低热量”(diet),热量400到700卡路里的菜划为“普通”(normal),高于700卡路里的划为“高热量”(fat)。由于Dish类的作者没有把这个操作写成一个方法,你无法使用方法引用,但你可以把这个逻辑写成Lambda表达式:
public enum CaloricLevel { DIET, NORMAL, FAT }
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return
CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
} ));
多级分组
要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。那么要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(
groupingBy(Dish::getType, ←─一级分类函数
groupingBy(dish -> { ←─二级分类函数
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
} )
)
);
// 这个二级分组的结果就是像下面这样的两级Map:
{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]},
FISH={DIET=[prawns], NORMAL=[salmon]},
OTHER={DIET=[rice, seasonal fruit], NORMAL=[french fries, pizza]}}
这里的外层Map的键就是第一级分类函数生成的值:“fish, meat, other”,而这个Map的值又是一个Map,键是二级分类函数生成的值:“normal, diet, fat”。最后,第二级map的值是流中元素构成的List,是分别应用第一级和第二级分类函数所得到的对应第一级和第二级键的值:“salmon、pizza…” 这种多级分组操作可以扩展至任意层级,n 级分组就会得到一个代表 n 级树形结构的 n 级Map。
按子组收集数据
在上一节中,我们看到可以把第二个groupingBy收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy。例如,要数一数菜单中每类菜有多少个,可以传递counting收集器作为groupingBy收集器的第二个参数:
Map<Dish.Type, Long> typesCount = menu.stream().collect(
groupingBy(Dish::getType, counting()));
// 结果
{MEAT=3, FISH=2, OTHER=4}
再举一个例子,你可以把前面用于查找菜单中热量最高的菜肴的收集器改一改,按照菜的类型分类:
Map<Dish.Type, Optional<Dish>> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,
maxBy(comparingInt(Dish::getCalories))));
// 结果
{FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}
注意:这个Map中的值是Optional,因为这是maxBy工厂方法生成的收集器的类型,但实际上,如果菜单中没有某一类型的Dish,这个类型就不会对应一个Optional. empty()值,而且根本不会出现在Map的键中。groupingBy收集器只有在应用分组条件后,第一次在流中找到某个键对应的元素时才会把键加入分组Map中。这意味着Optional包装器在这里不是很有用,因为它不会仅仅因为它是归约收集器的返回类型而表达一个最终可能不存在却意外存在的值。
因为分组操作的Map结果中的每个值上包装的Optional没什么用,所以你可能想要把它们去掉。要做到这一点,或者更一般地来说,把收集器返回的结果转换为另一种类型,你可以使用Collectors.collectingAndThen工厂方法返回的收集器,如下所示。
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType, ←─分类函数
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)), ←─包装后的收集器
Optional::get))); ←─转换函数
// 结果
{FISH=salmon, OTHER=pizza, MEAT=pork}
求出所有菜肴热量总和的收集器,对每一组Dish求和
Map<Dish.Type, Integer> totalCaloriesByType =
menu.stream().collect(groupingBy(Dish::getType,
summingInt(Dish::getCalories)));
分区(Partitioning)
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。例如,如果你是素食者或是请了一位素食的朋友来共进晚餐,可能会想要把菜单按照素食和非素食分开:
Map<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(partitioningBy(Dish::isVegetarian)); ←─分区函数
// 返回
{false=[pork, beef, chicken, prawns, salmon],
true=[french fries, rice, season fruit, pizza]}
那么通过Map中键为true的值,就可以找出所有的素食菜肴了:
List<Dish> vegetarianDishes = partitionedMenu.get(true);
请注意,用同样的分区谓词,对菜单List创建的流作筛选,然后把结果收集到另外一个List中也可以获得相同的结果:
List<Dish> vegetarianDishes =
menu.stream().filter(Dish::isVegetarian).collect(toList());
分区的优势
分区的好处在于保留了分区函数返回true或false的两套流元素列表。在上一个例子中,要得到非素食Dish的List,你可以使用两个筛选操作来访问partitionedMenu这个Map中false键的值:一个利用谓词,一个利用该谓词的非。而且就像你在分组中看到的,partitioningBy工厂方法有一个重载版本,可以像下面这样传递第二个收集器
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
menu.stream().collect(
partitioningBy(Dish::isVegetarian, ←─分区函数
groupingBy(Dish::getType))); ←─第二个收集器
// 这将产生一个二级Map:
{false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]},
true={OTHER=[french fries, rice, season fruit, pizza]}}
这里,对于分区产生的素食和非素食子流,分别按类型对菜肴分组,得到了一个二级Map,你可以重用前面的代码来找到素食和非素食中热量最高的菜:
Map<Boolean, Dish> mostCaloricPartitionedByVegetarian =
menu.stream().collect(
partitioningBy(Dish::isVegetarian,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
// 结果
{false=pork, true=pizza}
我们已经看到,和groupingBy收集器类似,partitioningBy收集器也可以结合其他收集器使用。尤其是它可以与第二个partitioningBy收集器一起使用来实现多级分区。再看两个例子
menu.stream().collect(partitioningBy(Dish::isVegetarian,
partitioningBy (d -> d.getCalories() > 500)));
// 以下二级Map:
{ false={false=[chicken, prawns, salmon], true=[pork, beef]},
true={false=[rice, season fruit], true=[french fries, pizza]}}
menu.stream().collect(partitioningBy(Dish::isVegetarian,
counting()));
// 它会计算每个分区中项目的数目,得到以下Map:
{false=5, true=4}
将数字按质数和非质数分区
假设你要写一个方法,它接受参数int n,并将前 n 个自然数分为质数和非质数。但首先,找出能够测试某一个待测数字是否是质数的谓词会很有帮助:
public boolean isPrime(int candidate) {
return IntStream.range(2, candidate) ←─产生一个自然数范围,从2开始,直至但不包括待测数
.noneMatch(i -> candidate % i == 0); ←─如果待测数字不能被流中任何数字整除则返回true
}
一个简单的优化是仅测试小于等于待测数平方根的因子:
public boolean isPrime(int candidate) {
int candidateRoot = (int) Math.sqrt((double) candidate);
return IntStream.rangeClosed(2, candidateRoot)
.noneMatch(i -> candidate % i == 0);
}
现在最主要的一部分工作已经做好了。为了把前n个数字分为质数和非质数,只要创建一个包含这n个数的流,用刚刚写的isPrime方法作为谓词,再给partitioningBy收集器归约就好了:
public Map<Boolean, List<Integer>> partitionPrimes(int n) {
return IntStream.rangeClosed(2, n).boxed()
.collect(
partitioningBy(candidate -> isPrime(candidate)));
}
Collectors类的静态工厂方法能够创建的所有收集器的表清单
工厂方法 | 返回类型 | 用于 |
---|---|---|
toList | List<T> | 把流中所有项目收集到一个List |
使用示例:List<Dish> dishes = menuStream.collect(toList()); | ||
toSet | Set<T> | 把流中所有项目收集到一个Set ,删除重复项 |
使用示例:Set<Dish> dishes = menuStream.collect(toSet()); | ||
toCollection | Collection<T> | 把流中所有项目收集到给定的供应源创建的集合 |
使用示例:Collection<Dish> dishes = menuStream.collect(toCollection(), | ||
counting | Long | 计算流中元素的个数 |
使用示例:long howManyDishes = menuStream.collect(counting()); | ||
summingInt | Integer | 对流中项目的一个整数属性求和 |
使用示例:int totalCalories = | ||
averagingInt | Double | 计算流中项目Integer 属性的平均值 |
使用示例:double avgCalories = | ||
summarizingInt | IntSummaryStatistics | 收集关于流中项目Integer 属性的统计值,例如最大、最小、总和与平均值 |
使用示例:IntSummaryStatistics menuStatistics = | ||
joining\` | String | 连接对流中每个项目调用toString 方法所生成的字符串 |
使用示例:String shortMenu = | ||
maxBy | Optional<T> | 一个包裹了流中按照给定比较器选出的最大元素的Optional ,或如果流为空则为Optional.empty() |
使用示例:Optional<Dish> fattest = | ||
minBy | Optional<T> | 一个包裹了流中按照给定比较器选出的最小元素的Optional ,或如果流为空则为Optional.empty() |
使用示例:Optional<Dish> lightest = | ||
reducing | 归约操作产生的类型 | 从一个作为累加器的初始值开始,利用BinaryOperator 与流中的元素逐个结合,从而将流归约为单个值 |
使用示例:int totalCalories = | ||
collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果应用转换函数 |
使用示例:int howManyDishes = | ||
groupingBy | Map<K, List<T>> | 根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果Map 的键 |
使用示例:Map<Dish.Type,List<Dish>> dishesByType = | ||
partitioningBy | Map<Boolean,List<T>> | 根据对流中每个项目应用谓词的结果来对项目进行分区 |
使用示例:Map<Boolean,List<Dish>> vegetarianDishes = |
收集器接口(The Collector interface)
Collector接口包含了一系列方法,为实现具体的归约操作(reduction operations,即 colectors收集器)提供了范本。我们将展示如何实现Collector接口来创建一个收集器,来比先前更高效地将数值流划分为质数和非质数。
首先让我们在下面的列表中看看Collector接口的定义,它列出了接口的签名以及声明的五个方法。
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
- T是流中要收集的项目的泛型。
- A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
- R是收集操作得到的对象(通常但并不一定是集合)的类型。
例如,你可以实现一个ToListCollector
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
理解Collector接口声明的方法
现在我们可以一个个来分析Collector接口声明的五个方法了。通过分析,你会注意到,前四个方法都会返回一个会被collect方法调用的函数,而第五个方法characteristics则提供了一系列特征,也就是一个提示列表,告诉collect方法在执行归约操作的时候可以应用哪些优化(比如并行化)。
- 建立新的结果容器:supplier方法
supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。很明显,对于将累加器本身作为结果返回的收集器,比如我们的ToListCollector,在对空流执行操作的时候,这个空的累加器也代表了收集过程的结果。在我们的ToListCollector中,supplier返回一个空的List,如下所示:
public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>();
}
// 也可以只传递一个构造函数引用
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
- 将元素添加到结果容器:accumulator方法
accumulator方法会返回执行归约操作的函数。当遍历到流中第 n 个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n-1 个项目),还有第 n 个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。对于ToListCollector,这个函数仅仅会把当前项目添加至已经遍历过的项目的列表:
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item);
}
// 你也可以使用方法引用,这会更为简洁:
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
- 对结果容器应用最终转换:finisher方法
在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。通常,就像ToListCollector的情况一样,累加器对象恰好符合预期的最终结果,因此无需进行转换。所以finisher方法只需返回identity函数:
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
- 合并两个结果容器:combiner方法
四个方法中的最后一个——combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。对于toList而言,这个方法的实现非常简单,只要把从流的第二个部分收集到的项目列表加到遍历第一部分时得到的列表后面就行了:
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1; }
}
- characteristics方法
最后一个方法——characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics是一个包含三个项目的枚举。
- UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。
- CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。
- IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。
我们迄今开发的ToListCollector是IDENTITY_FINISH的,因为用来累积流中元素的List已经是我们要的最终结果,用不着进一步转换了,但它并不是UNORDERED,因为用在有序流上的时候,我们还是希望顺序能够保留在得到的List中。最后,它是CONCURRENT的,但我们刚才说过了,仅仅在背后的数据源无序时才会并行处理。
全部融合到一起
前面谈到的五个方法足够我们开发自己的ToListCollector了。你可以把它们都融合起来,如下面的代码清单所示。
import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import static java.util.stream.Collector.Characteristics.*;
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new; ←─创建集合操
作的起始点
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add; ←─累积遍历过的项目,原位修改累加器
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.indentity(); ←─恒等函数
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2); ←─修改第一个累加器,将其与第二个累加器的内容合并
return list1; ←─返回修改后的第一个累加器
};
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(
IDENTITY_FINISH, CONCURRENT)); ←─为收集器添加IDENTITY_FINISH和CONCURRENT标志
}
}
请注意,这个实现与Collectors.toList方法并不完全相同,但区别仅仅是一些小的优化。这些优化的一个主要方面是Java API所提供的收集器在需要返回空列表时使用了Collections.emptyList()这个单例(singleton)。这意味着它可安全地替代原生Java,来收集菜单流中的所有Dish的列表:
List<Dish> dishes = menuStream.collect(new ToListCollector<Dish>());
这个实现和标准的List<Dish> dishes = menuStream.collect(toList())
构造之间的其他差异在于toList是一个工厂,而ToListCollector必须用new来实例化。
开发自己的收集器以获得更好的性能
之前我们用Collectors类提供的一个方便的工厂方法创建了一个收集器,它将前 n 个自然数划分为质数和非质数,如下所示。
public Map<Boolean, List<Integer>> partitionPrimes(int n) {
return IntStream.rangeClosed(2, n).boxed()
.collect(partitioningBy(candidate -> isPrime(candidate));
}
// 当时,通过限制除数不超过被测试数的平方根,我们对最初的isPrime方法做了一些改进:
public boolean isPrime(int candidate) {
int candidateRoot = (int) Math.sqrt((double) candidate);
return IntStream.rangeClosed(2, candidateRoot)
.noneMatch(i -> candidate % i == 0);
}
还有没有办法来获得更好的性能呢?答案是“有”,但为此你必须开发一个自定义收集器。
Lambda 表达式
谓词(Predicate)
先看一个方法引用的例子:
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p){
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
// [Apple{color='green', weight=80}, Apple{color='green', weight=155}]
List<Apple> greenApples = filterApples(inventory, FilteringApples::isGreenApple);
System.out.println(greenApples);
// [Apple{color='green', weight=155}]
List<Apple> heavyApples = filterApples(inventory, FilteringApples::isHeavyApple);
System.out.println(heavyApples);
前面的代码传递了方法Apple::isGreenApple
(它接受参数Apple并返回一个boolean)给filterApples
,后者则希望接受一个Predicate<Apple>
参数。谓词(predicate)在数学上常常用来代表一个类似函数的东西,它接受一个参数值,并返回true
或false
。
从传递方法到Lambda
把方法作为值来传递显然很有用,但要是为类似于上文的isHeavyApple
和isGreenApple
这种可能只用一两次的短方法写一堆定义有点烦人。Java 8为了解决了这个问题,引入了新记法(匿名函数或Lambda),可以写成如下格式:
filterApples(inventory, (Apple a) -> "green".equals(a.getColor()) );
filterApples(inventory, (Apple a) -> a.getWeight() > 150 );
// 或
filterApples(inventory, (Apple a) -> a.getWeight() < 80 || "brown".equals(a.getColor()) );
Lambdas及函数式接口的例子
使用案例 | Lambda的例子 | 对应的函数式接口 |
---|---|---|
布尔表达式 | (List<String> list) -> list.isEmpty() | Predicate<List<String>> |
创建对象 | () -> new Apple(10) | Supplier<Apple> |
消费一个对象 | (Apple a) -> System.out.println(a.getWeight()) | Consumer<Apple> |
从一个对象中选择/提取 | (String s) -> s.length() | Function<String, Integer> 或ToIntFunction<String> |
合并两个值 | (int a, int b) -> a * b | IntBinaryOperator |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) | Comparator<Apple> 或BiFunction<Apple, Apple, Integer> 或ToIntBiFunction<Apple, Apple> |
函数式接口
List<Apple> greenApples =
filter(inventory, (Apple a) -> "green".equals(a.getColor()));
那到底在哪里可以使用Lambda呢?你可以在函数式接口上使用Lambda表达式。在上面的代码中,你可以把Lambda表达式作为第二个参数传给filter方法,因为它这里需要Predicate<T>
,而这是一个函数式接口。如果这听起来太抽象,不要担心,现在我们就来详细解释这是什么意思,以及函数式接口是什么。
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){ ←─引入类型参数T
List<T> result = new ArrayList<>();
for(T e: list){
if(p.test(e)){
result.add(e);
}
}
return result;
}
以上代码为了参数化filter
方法的行为而创建的Predicate<T>
接口。它就是一个函数式接口!为什么呢?因为Predicate
仅仅定义了一个抽象方法:
public interface Predicate<T>{
boolean test(T t);
}
一言以蔽之,函数式接口就是只定义一个抽象方法的接口。例如:
public interface Comparator<T> { ←─java.util.Comparator
int compare(T o1, T o2);
}
public interface Runnable{ ←─java.lang.Runnable
void run();
}
public interface ActionListener extends EventListener{ ←─java.awt.event.ActionListener
void actionPerformed(ActionEvent e);
}
public interface Callable<V>{ ←─java.util.concurrent.Callable
V call();
}
public interface PrivilegedAction<V>{ ←─java.security.PrivilegedAction
V run();
}
用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。下面的代码是有效的,因为Runnable是一个只定义了一个抽象方法run的函数式接口:
Runnable r1 = () -> System.out.println("Hello World 1"); ←─使用Lambda
Runnable r2 = new Runnable(){ ←─使用匿名类
public void run(){
System.out.println("Hello World 2");
}
};
public static void process(Runnable r){
r.run();
}
process(r1); ←─打印“Hello World 1”
process(r2); ←─打印“Hello World 2”
process(() -> System.out.println("Hello World 3")); ←─利用直接传递的Lambda打印“Hello World 3”
使用函数式接口
函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描述符。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。Java 8的库设计师帮你在java.util.function包中引入了几个新的函数式接口。列表如下:
函数式接口 | 函数描述符 | 原始数据类型接口 |
---|---|---|
Predicate<T> | T->boolean | IntPredicate,LongPredicate,DoublePredicate |
Consumer<T> | T->void | IntConsumer,LongConsumer, DoubleConsumer |
Function<T,R> | T->R | IntFunction<R>,IntToDoubleFunction,IntToLongFunction,LongFunction<R>,LongToDoubleFunction,LongToIntFunction,DoubleFunction<R>,ToIntFunction<T>,ToDoubleFunction<T>,ToLongFunction<T> |
Supplier<T> | ()->T | BooleanSupplier,IntSupplier,LongSupplier, DoubleSupplier |
UnaryOperator<T> | T->T | IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator |
BinaryOperator<T> | (T,T)->T | IntBinaryOperator,LongBinaryOperator,DoubleBinaryOperator |
BiPredicate<L,R> | (L,R)->boolean | |
BiConsumer<T,U> | (T,U)->void | ObjIntConsumer<T>,ObjLongConsumer<T>,ObjDoubleConsumer<T> |
BiFunction<T,U,R> | (T,U)->R | ToIntBiFunction<T,U>,ToLongBiFunction<T,U>,ToDoubleBiFunction<T,U> |
上表总结了Java API中提供的最常用的函数式接口及其函数描述符。请记得这只是一个起点。如果有需要,你可以自己设计一个。请记住,(T,U) -> R
的表达方式展示了应当如何思考一个函数描述符。表的左侧代表了参数类型。这里它代表一个函数,具有两个参数,分别为泛型T
和U
,返回类型为R
。
我们接下来会介绍Predicate
、Consumer
和Function
Predicate
java.util.function.Predicate<T>
接口定义了一个名叫test
的抽象方法,它接受泛型T对象,并返回一个boolean
。这恰恰和你先前创建的一样,现在就可以直接使用了。在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。比如,你可以定义一个接受String对象的Lambda表达式,如下所示。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer<T>
定义了一个名叫accept
的抽象方法,它接受泛型T
的对象,没有返回(void
)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach
方法,接受一个Integers
的列表,并对其中每个元素执行操作。在下面的代码中,你就可以使用这个forEach
方法,并配合Lambda来打印列表中的所有元素。
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
public static <T> void forEach(List<T> list, Consumer<T> c){
for(T i: list){
c.accept(i);
}
}
forEach(
Arrays.asList(1,2,3,4,5),
(Integer i) -> System.out.println(i) ←─Lambda是Consumer中accept方法的实现
);
Function
java.util.function.Function<T, R>
接口定义了一个叫作apply
的方法,它接受一个泛型T
的对象,并返回一个泛型R
的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。在下面的代码中,我们向你展示如何利用它来创建一个map
方法,以将一个String
列表映射到包含每个String
长度的Integer
列表。
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
public static <T, R> List<R> map(List<T> list,
Function<T, R> f) {
List<R> result = new ArrayList<>();
for(T s: list){
result.add(f.apply(s));
}
return result;
}
// [7, 2, 6]
List<Integer> l = map(
Arrays.asList("lambdas","in","action"),
(String s) -> s.length() ←─Lambda是Function接口的apply方法的实现
);
函数式接口异常处理
请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch
块中。
比如上文中函数式接口BufferedReaderProcessor,它显式声明了一个IOException:
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
但是你可能是在使用一个接受函数式接口的API,比如Function<T, R>
,没有办法自己创建一个。这种情况下,你可以显式捕捉受检异常:
Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
}
catch(IOException e) {
throw new RuntimeException(e);
}
};
类型检查、类型推断以及限制
你还可以进一步简化你的代码。Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。换句话说,Java编译器会像下面这样推断Lambda的参数类型:
// 请注意,当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略。
List<Apple> greenApples =
filter(inventory, a -> "green".equals(a.getColor())); ←─参数a没有显式类型
// Lambda表达式有多个参数,代码可读性的好处就更为明显。你可以这样来创建一个Comparator对象:
Comparator<Apple> c =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); ←─没有类型推断
Comparator<Apple> c =
(a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); ←─有类型推断
方法引用(method references)
方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。
// 先前
inventory.sort((Apple a1, Apple a2)
-> a1.getWeight().compareTo(a2.getWeight()));
// 之后(使用方法引用和java.util.Comparator.comparing):
inventory.sort(comparing(Apple::getWeight)); ←─你的第一个方法引用
方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符::
前,方法的名称放在后面。例如,Apple::getWeight
就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple a) -> a.getWeight()
的快捷写法。下面给出了Java 8中方法引用的其他一些例子。
Lambda | 等效的方法引用 |
---|---|
(Apple a) -> a.getWeight() | Apple::getWeight |
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
实际例子:
// 注意compareToIgnoreCase是String类中预先定义的
List<String> str = Arrays.asList("a","b","A","B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
// Lambda表达式的签名与Comparator的函数描述符兼容。利用前面所述的方法,这个例子可以用方法引用改写成下面的样子:
List<String> str = Arrays.asList("a","b","A","B");
str.sort(String::compareToIgnoreCase);
请注意,编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配。
构造函数引用(constructor references)
// 假设有一个构造函数没有参数。它适合Supplier的签名() -> Apple 。
Supplier<Apple> c1 = Apple::new; ←─构造函数引用指向默认的Apple()构造函数
Apple a1 = c1.get(); ←─调用Supplier的get方法将产生一个新的Apple
// 等价于
Supplier<Apple> c1 = () -> new Apple(); ←─利用默认构造函数创建Apple的Lambda表达式
Apple a1 = c1.get(); ←─调用Supplier的get方法将产生一个新的Apple
// 如果你的构造函数的签名是Apple(Integer weight),那么它就适合Function接口的签名
Function<Integer, Apple> c2 = Apple::new; ←─指向Apple(Integer weight)的构造函数引用
Apple a2 = c2.apply(110); ←─调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple
// 等价于
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);用要求的重量创建一个Apple的Lambda表达式
Apple a2 = c2.apply(110);调用该Function函数的apply方法,并给出要求的重量,将产生一个新的Apple对象
//如果你有一个具有两个参数的构造函数Apple(String color, Integer weight),那么它就适合BiFunction接口的签名
BiFunction<String, Integer, Apple> c3 = Apple::new; ←─指向Apple(Stringcolor,Integer weight)的构造函数引用
Apple c3 = c3.apply("green", 110); ←─调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象
// 等价于
BiFunction<String, Integer, Apple> c3 =
(color, weight) -> new Apple(color, weight); ←─用要求的颜色和重量创建一个Apple的Lambda表达式
Apple c3 = c3.apply("green", 110); ←─调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象
复合Lambda表达式的用法
Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator
、Function
和Predicate
都提供了允许你进行复合的方法。这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or
操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。
比较器复合
1.逆序(reversed order)
如果你想要对苹果按重量递减排序怎么办?用不着去建立另一个Comparator的实例。接口有一个默认方法reversed可以使给定的比较器逆序。因此仍然用开始的那个比较器,只要修改一下前一个例子就可以对苹果按重量递减排序:
inventory.sort(comparing(Apple::getWeight).reversed()); ←─按重量递减排序
2.比较器链(chaining comparators)
上面说得都很好,但如果发现有两个苹果一样重怎么办?哪个苹果应该排在前面呢?你可能需要再提供一个Comparator
来进一步定义这个比较。比如,在按重量比较两个苹果之后,你可能想要按原产国排序。thenComparing
方法就是做这个用的。它接受一个函数作为参数(就像comparing
方法一样),如果两个对象用第一个Comparator
比较之后是一样的,就提供第二个Comparator
。你又可以优雅地解决这个问题了:
inventory.sort(comparing(Apple::getWeight)
.reversed() ←─按重量递减排序
.thenComparing(Apple::getCountry)); ←─两个苹果一样重时,进一步按国家排序
谓词复合
谓词接口包括三个方法:negate
、and
和or
,让你可以重用已有的Predicate
来创建更复杂的谓词。比如,你可以使用negate
方法来返回一个Predicate
的非,比如苹果不是红的:
Predicate<Apple> notRedApple = redApple.negate(); ←─产生现有Predicate对象redApple的非
你可能想要把两个Lambda用and
方法组合起来,比如一个苹果既是红色又比较重:
Predicate<Apple> redAndHeavyApple =
redApple.and(a -> a.getWeight() > 150); ←─链接两个谓词来生成另一个Predicate对象
你可以进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果:
Predicate<Apple> redAndHeavyAppleOrGreen =
redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor())); ←─链接Predicate的方法来构造更复杂Predicate对象
请注意,and
和or
方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)
可以看作(a || b) && c
。
函数复合
你还可以把Function
接口所代表的Lambda表达式复合起来。Function
接口为此配了andThen
和compose
两个默认方法,它们都会返回Function
的一个实例。 那么在实际中这有什么用呢?比方说你有一系列工具方法,对用String表示的一封信做文本转换:
public class Letter{
public static String addHeader(String text){
return "From Raoul, Mario and Alan: " + text;
}
public static String addFooter(String text){
return text + " Kind regards";
}
public static String checkSpelling(String text){
return text.replaceAll("labda", "lambda");
}
}
// 现在你可以通过复合这些工具方法来创建各种转型流水线了,比如创建一个流水线:先加上抬头,然后进行拼写检查,最后加上一个落款,
Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline
= addHeader.andThen(Letter::checkSpelling)
.andThen(Letter::addFooter);