流是Java API的新成员,它允许你以声明性方式处理数据集合,还可以透明地并行处理;你可以把它们看成遍历数据集的高级迭代器。
简短的定义就是“从支持数据处理操作的源生成的元素序列”。
定义解释:
元素序列:就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。
源:流会使用一个提供数据的源,如集合、数组或输入/输出资源。 注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
数据处理操作:流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
特点:
流水线:很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询。
内部迭代:与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
接口定义在java.util.stream.Stream
以下示例将用Java7和Java8分别实现返回低热量的菜肴名称,并按卡路里排序:
Java7:
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()); //处理排序后的菜名列
}
Java8:
List<String> lowCaloricDishesName =
menu.stream()
.filter(d -> d.getCalories() < 400) //选出400卡路里以下的菜肴
.sorted(Comparator.comparing(Dish::getCalories)) //按照卡路里排序
.map(Dish::getName) // 提取菜肴的名称
.collect(Collectors.toList()); //将所有名称保存在list中
利用多核架构并行执行这段代码,只需要把stream()换成parallelStream();
List<String> lowCaloricDishesName =
menu.parallelStream()
.filter(d -> d.getCalories() < 400) //选出400卡路里以下的菜肴
.sorted(Comparator.comparing(Dish::getCalories)) //按照卡路里排序
.map(Dish::getName) // 提取菜肴的名称
.collect(Collectors.toList()); //将所有名称保存在list中
Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType)); //按Map里面的类别对菜肴进行分组
1、流与集合
集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中所有的值,集合中的每个元素都得先算出来才能添加到集合中。
流则是在概念上固定的数据结构(不能添加或删除元素),其元素是按需计算的。从另一个角度来说,流像是一个延迟创建的集合:只有在消费者要求的时候才会计算值。相反集合则是急切创建的。
流只能消费一次
和迭代器类似,流只能遍历一次。多次消费流时会抛出异常。
List<String> title = Arrays.asList("Java", "C", "JS");
Stream<String> s = title.stream();
s.forEach(System.out::println); //正常
s.forEach(System.out::println); //java.lang.IllegalStateException:流已被操作或关闭
外部迭代与内部迭代
使用Collection接口需要用户去做迭代(如for-each),称为外部迭代。 相反Streams库使用内部迭代。内部迭代时,项目可以透明地并行处理,或者用更优化的顺序进行处理,此外Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。
用for-each循环外部迭代:
List<String> names = new ArrayList<>();
for(Dish d: menu){
names.add(d.getName());
}
用背后的迭代器做外部迭代:
List<String> names = new ArrayList<>();
Iterator<String> 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());
2、流操作
java.util.stream.Stream中的Stream接口定义了许多操作。可以分为两大类:中间操作和终端操作。可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。
Stream定义接口如下:
Stream filter(Predicate var1);
Stream map(Function var1);
IntStream mapToInt(ToIntFunction var1);
LongStream mapToLong(ToLongFunction var1);
DoubleStream mapToDouble(ToDoubleFunction var1);
Stream flatMap(Function> var1);
IntStream flatMapToInt(Function var1);
LongStream flatMapToLong(Function var1);
DoubleStream flatMapToDouble(Function var1);
Stream distinct();
Stream sorted();
Stream sorted(Comparator var1);
Stream peek(Consumer var1);
Stream limit(long var1);
Stream skip(long var1);
...
流的使用一般包括三件事:
1)一个数据源(如集合)来执行一个查询;
2)一个中间操作链,形成一条流的流水线;
3)一个终端操作,执行流水线,并能生成结果。
流的流水线背后的理念类似于构建器模式。在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用built方法(对流来说就是终端操作)。
3、使用流
数据查询方式:筛选、切片、映射、查找、匹配和归约。
特殊的流:数值流、来自文件和数组等多种来源的流、无限流。
筛选和切片:
1)用谓词筛选filter
Streams接口支持filter方法,该操作会接受一个谓词(返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(Collectors.toList());
2)筛选各异的元素distinct
流还支持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);
3)截短流
流支持limit(n)方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit。如果流是有序的,则多会返回前n个元素。
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(Collectors.toList());
4)跳过元素
流还支持skip(n)方法,返回一个跳过了前n个元素的流。如果流中元素不足n个,则返回一个空流。注意,limit(n)和skip(n)是互补的!
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(Collectors.toList());
映射:
1)对流中每一个元素应用函数
流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。
示例:给定一个单词列表,返回每个单词的长度的列表。
List<String> words = Arrays.asList("Java", "Lambdas", "In", "Action");
List<Integer> wordLengths = words.stream()
.map(String::length)
.collect(toList());
2)流的扁平化
示例:给定单词列表 ["Hello","World"],返回里面各不相同的字符列表["H","e","l", "o","W","r","d"]?
可以使用flatMap来解决这个问题:
List<String> uniqueCharacters = words.stream()
.map(w -> w.split("")) //将每个单词转换为由其字母构成的数组
.flatMap(Arrays::stream) //将各个生成流扁平化为单个流
.distinct()
.collect(Collectors.toList());
使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。
flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
查找和匹配:
常见的数据处理套路是看数据集中的某些元素是否匹配一个给定的属性。Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。
1)检查谓词是否至少匹配一个元素
anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。
if (menu.stream().anyMatch(Dish::isVegetarian)) {
System.out.println("The menu is vegetarian!");
}
2)检查谓词是否匹配所有元素
allMatch方法可以查看流中的元素是否都能匹配给定的谓词。
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
和allMatch相对的是noneMatch,它可以确保流中没有任何元素与给定的谓词匹配。
boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
3)查找元素
findAny方法将返回当前流中的任意元素,它可以与其他流操作结合使用。
Optional<Dish> dish = menu.stream()
.filter(Dish::isVegetarian)
.findAny();
4)查找第一个元素
给定一个数字列表,找出第一个平方能被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?
答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。
规约(reduce):
原理是将流中的所有元素反复结合起来,得到一个值,这样的查询称为归约操作(将流归约成一个值)
1)元素求和
普通方式:
int sum = 0;
for (int x : numbers) {sum += x;}
lambda方式:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce函数接受两个参数:
一个是初始值0;
一个是用BinaryOperator<T>将两个元素结合产生一个新值,用的是lambda (a, b) -> a + b。
在Java 8中,可以使用Integer类的静态sum方法来对两个数求和;
int sum = numbers.stream().reduce(0, Integer::sum);
另外,reduce还有一个重载的变体,它不接受初始值,但会返回一个Optional对象:
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
为什么它返回一个Optional<Integer>呢?原因是考虑流中没有任何元素的情况,即结果不存在。reduce操作无法返回其和,因为它没有初始值。
2)最大值和最小值
可以使用规约来计算最大值和最小值。reduce操作会考虑新值和流中下一个元素,并产生一个新的大值或最小值,直到整个流消耗完,处理过程如下:
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
示例:用map和reduce方法数一数流中有多少个菜品?
int count = menu.stream()
.map(d -> 1)
.reduce(0, (a, b) -> a + b);
或
long count = menu.stream().count();
map和reduce的连接通常称为map-reduce模式,因Google用它来进行网络搜索而出名,因为它很容易并行化;
map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果,这些操作一般都是无状态的。
reduce、sum、max等操作需要内部状态来累积结果。这种情况下内部状态很小。不管流中有多少元素要处理,内部状态都是有界的。
sort或distinct等操作和filter和map类似,都是接受一个流,再生成一个流(中间操作),但有一个关键的区别:从流中排序和删除重复项时都需要知道先前的历史。如排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。我们把这些操作叫作有状态操作。
4、数据流
使用reduce方法计算流中元素的总和:
int count = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);
这段代码的问题是,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型,再进行求和。如何解决?Stream API提供了原始类型流特化,专门支持处理数值流的方法。
1)原始类型流特化
Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到大元素的max。此外还有在必要时再把它们转换回对象流的方法。注意,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性:即类似int和Integer之间的效率差异。
映射到数值流:将流转换为特化版本的常用方法是:mapToInt、mapToDouble和mapToLong。它们返回的是一个特化流,而不是Stream<T>。
用mapToInt对menu中的卡路里求和:
int calories = menu.stream().mapToInt(Dish::getCalories).sum();
转换回对象流:
把原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法,如下所示:
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); //将Stream转换为数值流
Stream<Integer> stream = intStream.boxed(); //将数值流转换为Stream
默认值OptionalInt:
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); //如果没有最大值的话,显式提供一个默认最大值
2)数值范围
Java 8引入了两个可以用于IntStream和LongStream的静态方法,生成数据范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。
区别:range不包含结束值,而rangeClosed包含结束值。
IntStream nums = IntStream
.rangeClosed(1, 100).filter(n -> n % 2 == 0); //一个从1到100的偶数流,表示范围 [1, 100]
System.out.println(nums.count()); //从1到100有50个偶数
3)数值流应用:勾股数
勾股数的三元数(a, b, c)满足公式a * a + b * b = c * c,其中a、b、c都是整数。
第一步:创建三元组:使用map把每个元素转换成一个勾股数组
stream.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});
第二步:生成b的值区间1-100
IntStream.rangeClosed(1, 100)
.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.boxed() //从rangeClosed返回的IntStream生成一个Stream<Integer>
.mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)}); //map方法只能为流中的每个元素返回另一个int,mapToObj方法会返回一个对象值流
第三步:生成值
根据已知a的值,生成勾股数的流:
Stream pythagoreanTriples = IntStream.rangeClosed(1, 100).boxed() //创建一个从1到100的数值范围来生成a的值
.flatMap(a ->IntStream.rangeClosed(a, 100) //对每个给定的a值,创建一个三元数流,b的范围从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)}) //产生三元数
); //flatMap方法在做映射的同时,会把所有生成的三元数流扁平化成一个流(三元数流)
第四步:运行代码
pythagoreanTriples.limit(5).forEach(t ->System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
结果:
3, 4, 5
5, 12, 13
6, 8, 10
7, 24, 25
8, 15, 17
最优方案:先生成所有的三元数(a*a, b*b, a*a+b*b),然后再筛选符合条件的。
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)); //元组中的第三个元素必须是整数
5、构建流
1)由值创建流
使用静态方法Stream.of,通过显式值创建一个流,它可以接受任意数量的参数。
Stream<String> stream = Stream.of("Java 8 ", "Lambdas "); stream.map(String::toUpperCase).forEach(System.out::println);
使用empty得到一个空流:
Stream<String> emptyStream = Stream.empty();
2)由数组创建流
使用静态方法Arrays.stream从数组创建一个流,它接受一个数组作为参数。
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
3)由文件生成流
java.nio.file.Files中的很多静态方法都会返回一个流。Files.lines方法会返回一个由指定文件中的各行构成的字符串流,然后对line调用split方法将行拆分成单词。
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){} //若出现异常需进行处理
4)由函数生成流:创建无限流
Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,可以无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
迭代iterate:
Stream.iterate(0, n -> n + 2) //返回前一个元素加上2,生成一个无限流
.limit(10).forEach(System.out::println);
iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator<t>类型)。
斐波纳契元组序列示例:
斐波纳契数列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55…数列中开始的两个数字是0和1,后续的每个数字都是前两个数字之和。
斐波纳契元组序列与之类似,是数列中数字和其后续数字组成的元组构成的序列:(0, 1), (1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21) …
用iterate方法生成斐波纳契元组序列中的前10个元素:
Stream.iterate(new int[]{0, 1},t -> new int[]{t[1], t[0]+t[1]})
.limit(10)
.forEach(t -> System.out.println("(" + t[0] + "," + t[1] +")"));
结果:
(0,1)
(1,1)
(1,2)
(2,3)
(3,5)
(5,8)
(8,13)
(13,21)
(21,34)
(34,55)
若只想打印正常的斐波纳契数列,可以用map提取每个元组中的第一个元素:
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);
结果:
0
1
1
2
3
5
8
13
21
34
生成generate:
与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。
Stream.generate(Math::random).limit(5).forEach(System.out::println); //生成五个0到1之间的随机双精度数
IntStream可以避免装箱操作,IntStream的generate方法会接受一个IntSupplier,而不是Supplier<t>。
IntStream ones = IntStream.generate(() -> 1); //生成一个全是1的无限流
创建一个在调用时返回下一个斐波纳契项的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);
IntSupplier对象有可变的状态:它在两个实例变量中记录了前一个斐波纳契项和当前的斐波纳契项。getAsInt在调用时会改变对象的状态,在每次调用时产生新值。相比iterate方法则是不变的:它没有修改现有状态,但在每次迭代时会创建新的元组。你应该始终采用不变的方法,以便并行处理流,并保持结果正确。
注意:对于处理无限流,必须使用limit操作来显式限制它的大小;否则终端操作(如forEach)将永远计算下去。同样也不能对无限流做排序或归约,因为所有元素都需要处理,这是不可能的。
--以上示例摘自《Java8实战》