概要
流让你从外部迭代转向内部迭代。这样,你就用不着写下面这样的代码来显式地管理数据集合的迭代(外部迭代)了:
List<Dish> vegetarianDishes = new Arraylist<>();
for(Dish d: menu){
if(d.isVegetarian()){
vegetarianDishes.add(d);
}
}
现在可以使用支持filter和collect操作的Stream API(内部迭代)管理对集合数据的迭代。你只需要将筛选行为操作参数传递给fileter方法就行了:
List<Dish> vegetarianDishes = menu.stream()
.filter((d)->{d.isVegetarian}) //这里的类型可以省略
.collect(toList());
这种处理数据的方式很有用,因为你让Stream API管理如何处理数据。这样Stream API就可以在背后进行多种优化。此外,使用内部迭代的话,Stream API可以决定并行运行你的代码。这要是用外部迭代的话就办不到了,因为你只能用单一的线程挨迭代。
筛选和切片
用谓词筛选
Streams接口支持filter方法。该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。列如:
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(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)方法,该方法会返回一个不超过给定长度的流。所需的长度座位参数传递给limit。如果留是有序的,则最多会返回前n个元素。比如,你可以建立一个List,选出热量超过300卡路里的头三道菜:
List<Dish> dishes = menu
.stream()
.filter(d->d.getCalories()>300)
.limit(3)
.collect(toList());
可以看到,该方法只选出了符合谓词的头三个元素,然后就立即返回了结果。
请注意limit也可以用再无序流上,比如源是一个Set。这种情况下,limit的结果不会以任何顺序排列。
跳过元素
流和支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。下面的代码将跳过超过300卡路里的头两道菜,并返回剩下的。
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories > 300)
.skip(2)
.coolect(toList());
映射
对流中的每一个元素应用函数
流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的差别在于它是“创建一个新版本”而不是“修改”)。列如,下面代码提取流中菜肴的名称:
List<String> dishNames = menu.stream()
.map(s->s.getName())
.collect(toList());
流的扁平化
已经看到如何使用map方法返回列表中每个单词的长度了。让我们拓展一下:对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?列如,给定单词表["hello","world"], 你想要返回列表["h","e","l","o","w","r","d"].
可能会认为很容易,可以把每个单词映射成一张字符表,然后调用distinct来过滤重复的字符。
words.stream().
map(word ->world.split(""))
.distinct()
.collect(toList());
这个方法问题在于,传递给map方法的Lambda为每个单词返回了一个String。因此,map返回的流实际上是Stream<String[]>类型的。你真正想要的是用Stream<String>来表示一个字符流。
可以使用FlatMap来解决这个问题:
List<String> uniqueCharacters =
word.stream()
.map(w -> w.split(""))//将每个单词转为由其字母构成的数组
.flatMap(Array::stream)//将各个生成流扁平化为单个流
.distinct()
.collect(toList());
使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。下图说明了flatMap的效果
flatMap方法让你把一个流中的每个值都换成一个流,然后把所有的流连接起来成为一个流。
查找和匹配
另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream API通过allMatch,anyMatch,noneMatch,findFirst和findAny方法提供了这样的工具。
检查谓词是否至少匹配一个元素 anyMatch
anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。比如,你可以用它来看看菜单里面是否有素食可以选择
if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("there is any vegetarian foot");
}
anyMatch方法返回一个boolean,因此是一个终端操作
检查谓词是否匹配所有的元素 allMatch
allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。比如,你可以用它来看看菜品是否都有利于健康(卡路里小于1000卡路里):
boolean isHealthy = menu.stream().allMatch(s -> s.getCalories() < 1000);
noneMatch
和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。比如你可以用noneMatch重写前面的列子:
boolean isHealthy = menu.stream().noneMatch(s -> s.getCalories() >= 1000);
anyMatch,allMatch和noneMatch这三个操作都用到了我们所谓的短路,这就是大家熟悉的java中的&&和||运算符短路在流中的版本。
查找元素 findAny
findAny方法将返回当前流中的任意元素。它可以与其他流操作结合使用。比如,你想找到一道素食菜肴。你可以结合使用filter和findAny方法来实现这个查询:
Optionnal<Dish> dish = menu.stream().filter(Dish:isVegetarian).findAny();
流水线将在后台进行优化使其只需走一遍,并在李勇短路找到结果时立即结束。
Optional简介
Optionnal<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在。在上面的代码中。findAny可能什么元素都没找到。java8的库设计人员引入了Optional<T>,这样就不用返回中所周知的null了。
Optional里面几种可以迫使你显示的检查值是否存在或处理值不存在的情形的方法也不错。
- isPresent() 将在Optional包含值的时候返回true,否则返回false.
- ifPresent(Consumer<T> block)会在值存在的时候执行给定的代码块。介绍了Consumer函数式接口;它让你传递一个接收T类型的参数,并返回void的lambda表达式
- T get()会在值存在的时候返回值,否则抛出一个NoSuchElement异常
- T orElse(T other) 会在值存在时返回值,否则返回一个默认值。
查找第一个元素
找到第一个平法能被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,因为它在使用并行流时限制更少。
归约
到目前为止,你见到过的终端操作都是返回一个boolean(allMatch之类的),void(forEach)或Optionnal对象(findAny)。也见过了使用collect来将流中的所有元素组合成一个List。
本节中,你将看到如何把一个流中的流中的元素组合起来,使用reduce操作来表达更复杂的查询。
元素求和
在研究reduce方法之前,先来看看如何使用for-each循环来对数字列表中的元素求和:
int sum = 0;
for (int x:numbers){
sum += x;
}
要是还能把所有的数字相乘,而不必去复制粘贴这段代码,岂不是更好?这正是reduce操作的用武之地,它对这种重复应用的模式做了抽象。可以以下面这样对流中所有的元素求和:
int sum = numbers.stream().reduce(0,(a,b)->a+b);
reduce接收两个参数:
- 一个初始值,这里是0;
- 一个BinaryOperater<T> 来将连个 元素结合起来产生一个新值。
如果把所有的元素相乘也很简单
int sum = numbers.stream().reduce(1,(a,b)->a * b);
无初始值
reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:
Optional<Integer> sum = numbers.stream().reduce((a,b) -> (a + b));
为什么它返回一个Optional<Integer>呢?考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。
最大值和最小值
reduce操作会考虑新值和流中下一个元素,并产生一个新的最大值,值到整个流消耗完。可以用下面的代码来计算最大值
int maxNumber = numbers.stream().reduce(0,(a,b) -> Interger.max(a,b));
终端操作和中间操作
数值流
原始类型流特性
java8 引入了三个原始类型特化流接口来解决这个问题:IntStream,DoubleStream和LongStream,分别将流中的元素特化为int,long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值规约的新方法,比如对数值流求和的sum,找到最大元素的max。==这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差==异。
映射到数值流
将流转换为特化版本的常用方法是mapToInt,mapToDouble和mapToLong。这些方法和前面说的map方法的功能做方式一样,只是他们返回的是一个特化流,而不是Stream<T>。列如,你可以像下面这样用mapToInt对menu中的卡路里求和:
int totalCalories = menu.stream()
.mapToInt((s) -> s.getCalories)
.sum();
这里,mapToInt会从每道菜中提取热量(用一个Integer表示),并返回一个IntStream(而不是一个Stream<Integer>)。然后你就可以调用Integer接口中定义的sum方法,对卡路里进行求和。==请注意,如果流是空的,sum默认返回0==.IntStream还支持其他的方便方法,如max,min,average等等。
转换回对象流
同样,一旦有了数值流,你可能会想把它转换回非特化流。列如,IntStream上的操作只能产生原始整数:IntStream的map操作接收的Lambda必须接收int并且返回int(一个IntUnaryOperator)。但是你可能想要生成另一类值,比如Dish。为此,你需要访问Stream接口中定义的那些更广义的操作。要把原始流转换成一般的流(每个int都会装箱成一个Integer),可以使用boxed方法。
IntStream intStream = menu.stream().mapToInt((s)->s.getCalories())
Stream<Integer> stream = intStream.boxed();
构建流
由值创建流
可以使用静态方法Stream.of,通过显示值创建一个流。它可以接受任意数量的参数。列如,一下代码直接使用Stream.of创建了一个字符串流。然后你可以将字符串转化为答谢,再一个个的打印出来:
Stream<Stream> stream = Stream.of("java8","lambdas","in","action");
Stream.map((s)->s.toUpperCase).forEach((s)->System.out.println(s));
你可以使用empty得到一个空流,如下所示:
Stream<String> emptyStream = Stream.empty();
由数组创建流
可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。例如,可以讲一个原始类型int的数组转换成一个IntStream。
int[] numbers ={1,2,3,4,5,6,7}
int sum = Arrays.stream(numbers).sum();
由文件生成流
java中用于处理文件等I/O操作的NIO API(非阻塞I/O)已经更新,以便利用Sream API
java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有的方法是File.lines,它会返回一个由指定文件中的各行构成的字符串流。可以yoga这个方法看看一个文件中有多少各不相同的词:
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(IOExcepiton e){
}
由函数生成流:创建无限流
Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流的那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
迭代 iterate
先看一个简单的iterate的列子,然后再解释:
Stream.iterate(0,n -> n + 2)
.limit(10)
.forEach(System.out::println);
这个流将会无限的打印,2,4,,6,8,10.此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。
斐波那契序列
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);
生成 generate
与iterate方法类似,generate方法也可以让你按需生成一个无限流。但是generate不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。
Stream.generate(Math::random) //不会对生成的值计算。
.limit(5)
.forEach(System.out::println);