Stream
允许你以声明性方式处理数据集合,流还可以透明地并行处理,你就无需写任何多线程代码了。和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。
例如,以下代码会抛出一个异常,说流已经被消费掉了:
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),这称为外部迭代。 相反,Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。
内部迭代:内部迭代时,项目可以透明地并行处理,或者用更优化的顺序进行处理,如果使用外部迭代,这些优化都是很困难的,通过写for-each而选择了外部迭代,那你基本上就要自己管理所有的并行问题了。
外部迭代:外部迭代一个集合,显式地取出每个项目再加以处理,Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。
流操作
import static java.util.stream.Collectors.toList;
List<String> threeHighCaloricDishNames =
menu.stream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.limit(3)
.collect(toList());
- filter:接受Lambda,从流中排除某些元素。
- map:—接受一个Lambda,将元素转换成其他形式或提取信息。
- limit:截断流,使其元素不超过给定数量
-
collect:将流转换为其他形式。
除了collect之外,所有这些操作都会返回另一个流,这样它们就可以接成一条流水线,于是就可以看作对源的一个查询。最后collect操作开始处理流水线,并返回结果(它和别的操作不一样,因为它返回的不是流,在这里是一个List),在调用collect之前,没有任
何结果产生,实际上根本就没有从menu里选择元素。
中间操作
诸如filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理,是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。-
终端操作
终端操作会从流的流水线生成结果,其结果是任何不是流的值。在下面的流水线中,forEach是一个返回void的终端操作,它会对源中的每道菜应用一个Lambda。把System.out.println传递给forEach:menu.stream().forEach(System.out::println);
使用流:
- 一个数据源(如集合)来执行一个查询;
- 一个中间操作链,形成一条流的流水线;
- 一个终端操作,执行流水线,并能生成结果。
方法名 | 作用 |
---|---|
filter |
该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流 |
distinct |
它会返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流,可确保流中没有重复元素 |
limit(n) |
方法会返回一个不超过给定长度的流 |
count |
返回流中元素的个数 |
sorted |
对流进行排序 |
skip(n) |
返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流,与limit()互补 |
map |
它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素 |
flatMap |
让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流 |
-
sorted
list.stream().sorted(Comparator.comparing(Student::getAge)) list.stream().sorted(Comparator.comparing(Student::getAge).reversed())//倒序
-
flatMap
例如:给定单词列表["Hello","World"],想要返回列表["H","e","l", "o","W","r","d"]。你可以把每个单词映射成一张字符表,然后调用distinct来过滤
重复的字符。words.stream() .map(word -> word.split("")) .distinct() .collect(toList());
这个方法的问题在于,传递给map方法的Lambda为每个单词返回了一个String[](String列表)。因此,map返回的流实际上是Stream<String[]>类型的。你真正想要的是用Stream<String>来表示一个字符流。
这时候,可能想到有一个叫作Arrays.stream()
的方法可以接受
一个数组并产生一个流,例如:
String[] arrayOfWords = {"Goodbye", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
words.stream()
.map(word -> word.split(""))
.map(Arrays::stream)
.distinct()
.collect(toList());
但是这种方案依然不行,因为,你现在得到的是一个流的列表(更准确地说是Stream<String>)你先是把每个单词转换成一个字母数组,然后把每个数组变成了一个独立的流,但是这样每个数组变成一个单独的流。
使用flatMap方法:
List<String> uniqueCharacters =
words.stream()
.map(w -> w.split("")) //将每个单词转换为由其字母构成的数组
.flatMap(Arrays::stream)//将各个生成流扁平化为单个流
.distinct()
.collect(Collectors.toList());
使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。
flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
例如: 给定两个数字列表,如何返回所有的数对呢?例如,给定列表[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)是可以的(filter可以配合谓词使用来筛选流中的元素。因为在flatMap操作后,你有了一个代表数对的int[]流,所以你只需要一个谓词来检查总和是否能被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());
- 检查与匹配
方法 | 作用 |
---|---|
anyMatch |
流中是否有一个元素能匹配给定的谓词,返回一个boolean,因此是一个终端操作 |
allMatch |
流中的元素是否都能匹配给定的谓词 |
noneMatch |
确保流中没有任何元素与给定的谓词匹配 |
anyMatch、allMatch和noneMatch这三个操作都用到了的短路,对于流而言,某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。同样,limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。
-
查找元素
findAny:
将返回当前流中的任意元素。它可以与其他流操作结合使用。Optional<Dish> dish = menu.stream() .filter(Dish::isVegetarian) .findAny();
Optional
Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在。在上面的代码中,findAny可能什么元素都没找到。这样就不用返回容易出问题的null了。
- Optional里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法:
- isPresent()将在Optional包含值的时候返回true, 否则返回false。
- ifPresent()会在值存在的时候执行给定的代码块。
- T get()会在值存在时返回值,否则抛出一个NoSuchElement异常。
- T orElse(T other)会在值存在时返回值,否则返回一个默认值。
查找第一个元素:
findFirst:
有些流有一个出现顺序来指定流中项目出现的逻辑顺序(比如由List或排序好的数据列生成的流)。对于这种流,可能想要找到第一个元素,它的工作方式类似于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();
何时使用findFirst和findAny
为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。
归约:将流中的元素反复结合起来,得到一个值的操作。
-
reduce
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce接收两个参数:
- 一个初始值,这里是0
- 一个BinaryOperator<T>来将两个元素结合起来产生一个新值,这里用的是lambda (a, b) -> a + b
Integer类现在有了一个静态的sum方法来对两个数求和,
int sum = numbers.stream().reduce(0, Integer::sum);
若无初始值
reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。
- 最大值和最小值
用Lambda写就是:Lambda (x, y) -> x < y ? x : y
也可以写成:Optional<Integer> max = numbers.stream().reduce(Integer::max);
同样最小值:Optional<Integer> min = numbers.stream().reduce(Integer::min);
归约方法的优势与并行化
相比于逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!
数值流:
利用reduce方法计算流中元素的总和,但是这段代码的问题是,他有一个暗含的装箱成本,每个Integer都必须拆箱成原始类型,再进行求和,因此Stream API提供了原始类型流特化,专门处理支持数值流的方法。
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
- 原始类型流特化
IntStream
、DoubleStream
和LongStream
,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。
-
映射到数值流
int calories = menu.stream()//返回一个Stream<Dish> .mapToInt(Dish::getCalories)//返回一个IntStream .sum(); //如果流是空的,sum则默认返回0,IntStream还支持其他的方便方法,如max、min、average等
-
转回对象流
mapToObj
与boxed
的区别:???IntStream intStream = menu.stream().mapToInt(Dish::getCalories);//将Stream转换为数值流 Stream<Integer> stream = intStream.boxed(); //将数值流转换为Stream
-
默认值
OptionalInt
由于求和时默认值为0,计算IntStream中的最大值时,如何区分没有元素的流和最大值真的是0的流呢?Optional
同样可以用OptionalInt
、OptionalDouble
和OptionalLong
进行参数化。例如:找到
IntStream
中的最大元素,可以调用max方法,它会返回一个OptionalInt
:OptionalInt maxCalories = menu.stream() .mapToInt(Dish::getCalories) .max();
如果没有最大值,可以显式的处理OptionalInt
去定义一个默认值:
int max = maxCalories.orElse(1);
-
数值范围
生成成1和100之间的所有数字,Java 8引入了两个可以用于IntStream
和LongStream
的静态方法,帮助生成这种范围:range
和rangeClosed
。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed
则包含结束值。
例如:IntStream evenNumbers = IntStream.rangeClosed(1, 100) .filter(n -> n % 2 == 0); .count();//从1到100有50个偶数 IntStream evenNumbers = IntStream.range(1, 100) .filter(n -> n % 2 == 0); .count();//由于不包含100,结果为49
构建流:
-
由值创建流
使用静态方法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[] numbers = {2, 3, 5, 7, 11, 13}; int sum = Arrays.stream(numbers).sum();
-
由文件生成流
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){ }
Files.lines会得到一个流,其中每个元素都是给定文件中的一行。然后可以对line调用split方法将每行拆分成单词,这里应该使用flatMap产生扁平流,否则将会给每一行生成一个单词流。然后用distinct和count方法链接起来,就可以得到有多少各不相同的单词。
-
由函数生成流
Stream API提供了两个静态方法来从函数生成流:Stream.iterate
和Stream.generate
。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate
和generate
产生的流会用给定的函数按需创建值,一般来说,应该使用limit(n)
来对这种流加以限制,以避免打印无穷多个值。- 迭代
Stream.iterate(0, n -> n + 2) .limit(10) .forEach(System.out::println);
iterate方法接受一个初始值(在这是0),还有一个依次应用在每个产生的新值上的Lambda。这里使用Lambda n -> n + 2,返回的是前一个元素加上2。,此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。使用limit方法限制流的大小。
-
生成
Stream.generate(Math::random) .limit(5) .forEach(System.out::println);
与
iterate
方法类似,generate
方法也可让你按需生成一个无限流。但generate
不是依次对每个新生成的值应用函数的。在这里使用的供应源(指向Math.random
的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用但是在并行代码中使用有状态的供应源是不安全的
有状态和无状态操作
filter
和map
等操作是无状态的,它们并不存储任何状态。reduce
等操作要存储状态才能计算出一个值。sorted
和distinct
等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。
----摘自《java8实战》