流是什么
流是java8 API的新成员,它允许你以声明性的方式处理数据集合。
集合处理方法比较
使用以下代码返回低热量菜肴的名称,按照卡路里排序,在不使用流的情况下,一般是用以下代码进行处理。
List<Dish> lowCaloricDishes = new ArrayList<>();
for (Dish d : menu) {
if (d.getCalories() < 400) {
lowCaloricDishes.add(d);
}
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2) {
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
List<String> lowCaloricDishesName = new ArrayList<>();
for (Dish d : lowCaloricDishes) {
lowCaloricDishesName.add(d.getName());
}
这段代码中使用了一个"垃圾变量"lowCaricDishes。它唯一作用就是作为一次性的中间容器,可以看到针对这个简单的需求,代码实现起来比较冗余,这也是java之前被饱受诟病的地方😅。
而在java8中,实现的细节被放在它本该归属的库中。
List<String> lowCaloricDishName =
menu.stream()
.filter(d -> d.getCalories() < 400)
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
对比以上代码,可以看到Java8使用stream对集合进行处理的方法简洁、明了。并且还能够充分利用多核架构提升性能——只需要将stream()换成parallelStream()。
总结下,Java8中的Stream API可以写出如下代码:
声明性——更简洁,更易读
可复合——更灵活
可并行——性能更好
流简介
流简短的定义是从支持数据处理的源生成的元素序列
元素序列——集合讲的是数据,流讲的是计算。
流水线——很多流本身会返回一个流,这样操作就可以连接起来。
内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
代码示例
package lambdasinaction.chap4;
import java.util.*;
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return name;
}
public boolean isVegetarian() {
return vegetarian;
}
public int getCalories() {
return calories;
}
public Type getType() {
return type;
}
public enum Type { MEAT, FISH, OTHER }
@Override
public String toString() {
return name;
}
public static final List<Dish> menu =
Arrays.asList(
new Dish("pork", false, 800, Dish.Type.MEAT),
new Dish("beef", false, 700, Dish.Type.MEAT),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("prawns", false, 400, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH)
);
}
能够体现上述概念的代码:
List<String> threeHighCaloricDishNames =
Dish.menu.stream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.limit(3)
.collect(Collectors.toList());
System.out.println(threeHighCaloricDishNames);

流与集合
粗略的说流与集合的差异在于什么时候进行计算,集合是一个内存中的数据结构,它包含数据结构中目前的所有值——集合中的每个元素必须得先计算出来才能添加到集合中。流则是概念上固定的数据结构(你不能添加删除元素),其元素都是按需计算的。

只能遍历一次
和迭代器类似,流只能遍历一次。遍历完之后,我们说这个流已经被消费,例如以下代码会报异常:
List<String> title = Arrays.asList("Java8", "in", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println); //这里流的操作已经被关闭

集合与流的另一个关键区别在于他们遍历数据的方式。
外部迭代与内部迭代
使用Collection接口需要用户去做迭代(比如使用for-each),这个称为外部迭代。相反Stream库使用内部迭代——帮你把迭代做了。你只需要给出一个函数说干什么。下面的代码显式这种区别:
- 集合:用for-each循环外部迭代
List<String> names = new ArrayList<>();
for (Dish d : menu) {
names.add(d.getName());
}
- 集合:用背后的迭代器做外部迭代
List<String> names = new ArrayList<>();
Iterator<Dish> iterator = menu.iterator();
while (iterator.hasNext()) {
Dish d = iterator.next();
names.add(d.getName());
}
- 流:内部迭代
List<String> names = menu.stream().map(Dish::getName).collect(Collectors.toList());
流操作
java.util.stream.Stream中的Stream接口定义了许多操作。他们可以分为两大类。
List<String> names = menu.stream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.limit(3)
.collect(Collectors.toList());
从中可以看到两类操作:
filter、map和limit可以连成一条流水线。collect触发流水线执行并关闭。
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。
中间操作
中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。除非触发一个终端操作,否则中间操作不会执行任何处理,这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理掉。
为了搞清楚中间操作到底发生了什么,需要将代码改一改:
List<String> names = Dish.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(Collectors.toList());
运行结果:

有好几种优化利用了流的延迟性质。只选出了前三个,这是因为limit操作和一种称之为短路的技巧。尽管filter和map是两个独立的操作,但是它们合并到了同一次遍历之中了(称之为循环合并)。
终端操作
终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Integer甚至是void,例如:
menu.stream().forEach(System.out::println);
其他的终端操作:
long count = Dish.menu.stream()
.filter(d -> d.getCalories() > 300)
.distinct()
.limit(3)
.count();
使用流
总而言之,流的操作一般包括三件事:
- 一个数据源(集合)来指向一个查询。
- 一个中间操作链,形成一条流的流水线。
- 一个终端操作,执行流水线,并能生成结果。
一些常用的操作:
中间操作
| 操作 | 类型 | 返回类型 | 操作函数 | 函数描述符 |
|---|---|---|---|---|
| filter | 中间 | Stream | Predicate | T -> boolean |
| map | 中间 | Stream | Function<T, R> | T -> R |
| limit | 中间 | Stream | ||
| sorted | 中间 | Stream | Comparator | (T, T) -> int |
| distinct | 中间 | Stream |
终端操作
| 操作 | 类型 | 目的 |
|---|---|---|
| foreach | 终端 | 消费流中的每个元素并对其应用Lambda |
| count | 终端 | 返回流中元素的个数。这一操作返回long |
| collect | 终端 | 把流归约成一个集合,比如List、Map甚至是Integer |
筛选和切片
用谓词筛选
Stream接口支持filter方法。该方法接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。例如:
List<Dish> vegetarianMenu =
menu.stream().filter(Dish::isVegetarian)
.collect(Collectors.toList());

筛选各异的元素
流还支持一个叫做distinct的方法,它会返回一个元素各异(根据流所生成元素hashCode和equals方法实现)的流。例如:
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(n)方法,该方法会返回一个不超过给定长度的流。
List<Dish> vegetarianMenu = menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(Collectors.toList());

跳过元素
流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中的元素不足n个,则返回一个空流。limit(n)和skip(n)是互补的。
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(Collectors.toList());

映射
一个非常常见的数据处理套路就是从对象中选择信息。StreamAPI也通过map和flatMap方法提供了类似的工具。
对流中每一个元素应用函数
使用映射一词是它和转换类似,但是差别在于它是创建一个新版本而不是去修改。例如:
List<String> dishNames = menu.stream()
.map(Dish::getName)
.collect(Collectors.toList());
提取菜名长度:
List<Integer> dishNamesLengths = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(Collectors.toList());
流的扁平化
对于一张单词表["hello", "world"],想要返回列表["h", "e", "l", "o", "w", "r", "d"]。
第一个版本可能是这样的:
words.stream()
.map(word -> word.split(""))
.distinct()
.collect(Collectors.toList());
这个方法的问题在于,传递给map方法的Lambda为每个单词返回了一个String[]。然而真正想要的是Stream来表示一个字符流。
尝试使用map和Arrays.stream()
首先,需要的是一个字符流而不是数组流。有一个叫做Arrays.stream()的方法可以接受一个数组并产生一个流,例如:
String[] arrayOfWords = {"goodbye", "world"};
Stream<String> streamOfWords = Arrays.stream(arrayOfWords);
将它用在前面的流水线中:
words.stream()
.map(word -> word.split(""))
.map(Arrays::stream)
.distinct()
.collect(Collectors.toList());
当前的方法仍然搞不定,返回的是一个流的列表(Stream)。
使用flatMap
words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
使用flatMap的方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容,所以使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。

测试
- 给定一个数字列表如
[1, 2, 3, 4, 5],返回[1, 4, 9, 16, 25]。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream().map(n -> n * n).collect(Collectors.toList());
- 给定两个数字列表返回所有的数对。
List<Integer> number1 = Arrays.asList(1, 2, 3);
List<Integer> number2 = Arrays.asList(3, 4);
List<int[]> pairs = number1.stream()
.flatMap(i -> number2.stream()
.map(j -> new int[]{i, j}))
.collect(Collectors.toList());
扩展,返回能被三整除的整数对:
List<int[]> pairs = number1.stream()
.flatMap(i -> number2.stream()
.filter(j -> (i + j) % 3 == 0)
.map(j -> new int[]{i, j}))
.collect(Collectors.toList());
查找和匹配
检查谓词是否至少匹配一个元素
if (menu.stream().anyMatch(Dish::isVegetarian)) {
System.out.println("The menu is vegetarian friendly");
}
检查谓词是否匹配所有元素
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
noneMatch
与allMatch相对的是noneMatch。
boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
anyMatch、allMatch和noneMatch这三个操作都用到了所谓的短路,类似Java中&&和||运算符短路在流中的版本。对于某些操作不需要处理整个流就能得到结果。例如:只需要找到一个元素就可以有结果。同样的limit也是一个短路操作。
查找元素
findAny将返回当前流中的任意元素。
Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();
流水线将在后台优化使其只走一遍,并利用短路找到结果时立即结束。
上面一段代码用到了Optional类,该类是util包下的一个容器类,代表一个值存在或不存在。该类有以下几个方法:
isPresent():将在Optional包含值的时候返回true,否则返回false。ifPresent(Consumer<T> block):会在值存在的时候执行给定的代码块。T get():会在值存在时返回值,否则抛出一个NoSuchElement异常。T orElse(T other):会在值存在时返回值,否则返回一个默认值。
例子:
menu.stream()
.filter(Dish::isVegetarian)
.findAny()
.ifPresent(d -> System.out.println(d.getName()));
查找第一个元素
有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序。
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();
findFirst和findAny都是查找一个元素,区别在于找到第一个元素在并行上限制更多。如果不关心返回的元素是哪个,使用findAny,因为它在使用并行流时限制较少。
归纳
元素求和
使用for-each循环对数字列表中的元素求和:
int sum = 0;
for (int x : numbers) {
sum += x;
}
numbers中的每个元素都用加法运算符反复迭代来得到结果,可以像下面这样对流中所有元素求和:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce接受两个参数:
- 一个是初始值,这里是
0。 - 一个
BinaryOperator来将两个元素结合起来产生一个新值。
可以使用方法引用使这段代码更简洁:
int sum = numbers.stream().reduce(0, Integer::sum);
无初始值
reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
因为没有初始值所以返回的是Optional对象。
最大值和最小值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

数值流
在使用reduce计算流中元素的综合时候例如:
int calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);
这段代码有一个隐含装箱成本,这时,Stream提供了原始类型流特化,专门支持处理数值流的方法。
原始类型流特化
Java8 引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long、double。从而避免装箱成本。
映射到数值流
int calories = menu.stream().mapToInt(Dish::getCalories).sum();
这里的mapToInt会返回一个IntStream而非Stream<Integer>,然后可以调用该接口的sum方法进行求和。如果流是空的将返回0。IntStream还支持其他方法,如max、min、average等。
转换回对象流
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
默认值OptionalInt
前面有Optional来区分值存不存在。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt、OptionalDouble、OptionalLong。
例如查找IntStream中最大元素,可以调用max方法。
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();
数值范围
生成数值范围,Java8引入了IntStream和LongStream的静态方法,range和rangeClosed,这两个方法都是第一个接受参数起始值,第二个接受结束值。但是range不包含结束值,rangeClosed包含。
IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
数值流应用
生成100以内的勾股数:
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)}));
IntStream中的map只能为流中的每个元素返回另一个int,所以需要使用mapToObj。
上述方法需要求两次平方根,更好的做法:
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));
构建流
由值创建流
Stream<String> stream = Stream.of("java8", "Lambda", "In", "Action");
由数组创建流
int[] numbers = {1, 2, 3, 4, 5, 67};
int sum = Arrays.stream(numbers).sum(); //返回的是IntStream
由文件生成流
查看文件中有多少各不相同的单词:
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) {
e.printStackTrace();
}
由函数生成流
Stream API 提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流。一般来说应该使用limit(n)来对这种流加以限制。
迭代
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
斐波那契元组序列:
Stream.iterate(new int[]{0, 1}, n -> new int[]{n[1], n[0] + n[1]})
.limit(20)
.forEach(t -> System.out.println("(" + t[0] + ", " + t[1] + ")"));
生成
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
我们使用的供应源是无状态的,它不会在任何地方记录任何值。但供应源不一定是无状态的,你可以存储状态的供应源,它可以修改状态,并为流生成下一个值的时候使用。但是在并行分代码中使用有状态的供应源是不安全的。