|
WHAT
什么是流
流是“从支持数据处理操作的源生成的一系列元素”。
流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。可以把它们看成遍历数据集的高级迭代器。流还可以透明地并行处理,你无需写任何多线程代码了
WHY
1. 集合操作却远远算不上完美
集合是Java中使用最多的API。几乎每个Java应用程序都会制造和处理集合。集合对于很多编程任务来说都是非常基本的:它们可以让你把数据分组并加以处理。但集合操作却远远算不上完美.
很多业务逻辑都涉及类似于数据库的操作,大部分数据库都允许你声明式地指定这些操作。比如,以下SQL查询语句SELECT name FROM dishes WHERE calorie < 400 。你看,你不需要实现如何根据菜肴的属性进行筛选(比如利用迭代器和累加器),你只需要表达你想要什么。这个基本的思路意味着,你用不着担心怎么去显式地实现这些查询语句——都替你办好了!集合这里就不能这样?
要处理大量元素,为了提高性能,你需要并行处理,并利用多核架构。但写并行代码比用迭代器还要复杂,而且调试起来也够受的!
2. Stream的好处
Java 8中的Stream API可以让你写出这样的代码
- 声明性——更简洁,更易读
- 可复合——更灵活
- 可并行——性能更好
Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。 HOW
简介
Java 8中的集合支持一个新的stream 方法,它会返回一个流 。
java.util.stream.Stream 概念
从支持数据处理操作的源生成的元素序列
元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如 ArrayList 与 LinkedList )。但流的目的在于表达计算,比如你前面见到的filter 、 sorted 和 map 。集合讲的是数据,流讲的是计算。
源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如 filter 、 map 、 reduce 、 find 、 match 、 sort 等。流操作可以顺序执行,也可并行执行。
流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询。
内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
流与集合
概念区别
Java现有的集合概念和新的流概念都提供了接口,来配合代表元素型有序值的数据接口。所谓有序,就是说我们一般是按顺序取用值,而不是随机取用的。
集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。
流则是在概念上固定的数据结构,其元素则是按需计算的。这是一种生产者-消费者的关系。从另一个角度来说,流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值(用管理学的话说这就是需求驱动,甚至是实时制造)。 流的特点
集合和流的另一个关键区别在于它们遍历数据的方式。
- 1.只能遍历一次
流只能遍历一次。遍历完之后,这个流已经被消费掉。
以下代码会抛出一个异常,说流已被消费掉
List<String> title = Arrays.asList(&#34;Java8&#34;, &#34;In&#34;, &#34;Action&#34;);
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);
//java.lang.IllegalStateException:流已被操作或关闭- 2.外部迭代与内部迭代
使用 Collection 接口需要用户去做迭代(比如用 for-each ),这称为外部迭代。 相反,Streams库使用内部迭代——内部把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。迭代通过 filter 、 map 、 sorted 等操作被抽象掉了 //外部迭代
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(toList());

内部迭代与外部迭代
流的操作
连接起来的流操作称为中间操作,关闭流的操作称为终端操作
java.util.stream.Stream 中的 Stream 接口定义了许多操作。
可以分为两大类:
- filter 、 map 和 limit 可以连成一条流水线;
- collect 触发流水线执行并关闭它;
List<String> names = menu.stream() //从菜单获得流
.filter(d -> d.getCalories() > 300).map(Dish::getName).limit(3) //中间操作 filter 、 map 和 limit可以连成一条流水
.collect(toList()); //将 Stream 转换为 List collect 触发流水线执行并关闭它

中间操作与终端操作
- 1.中间操作
中间操作会返回另一个流,让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理。 短路的技巧
循环合并:尽管 filter 和 map 是两个独立的操作,但它们合并到同一次遍历中了
- 2.终端操作
终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如 List 、 Integer ,甚至 void 。 使用流
流的流水线背后的理念类似于构建器模式.
在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链)
,接着是调用 built 方法(对流来说就是终端操作)
XXXEntity.builder().createTime(createTime).build();流的使用一般包括三件事
- 一个数据源(如集合)来执行一个查询;
- 一个中间操作链,形成一条流的流水线;
- 一个终端操作,执行流水线,并能生成结果
关键词
筛选、切片和匹配 查找、匹配和归约 使用数值范围等数值流 多个源创建流 无限流
筛选和切片
用谓词筛选,选择流中的元素,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度。
- filter
- distinct
- limit 截短流
- skip(n) 跳过元素 limit(n) 和 skip(n) 互补
// filter 方法 方法引用检查菜肴是否适合素食者
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 也可以用在无序流上,比如源是一个 Set 。这种情况下, limit 的结果不会以任何顺序排列。
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300).limit(3)
.collect(toList());
// skip(n) 返回一个扔掉了前 n 个元素的流如果流中元素不足 n 个,则返回一个空流。 limit(n) 和 skip(n) 是互补的!
limit 也可以用在无序流上,比如源是一个 Set 。这种情况下, limit 的结果不会以任何顺序排列 映射
Stream API也通过 map 和 flatMap 方法从某些对象中选择信息.
- [1]map 方法
对流中每一个元素应用函数,并将其映射成一个新的元素,它是“创建一个新版本”而不是去“修改”
- [2] flatMap 流的扁平化
map(Arrays::stream) 时生成的单个流都被合并起来,扁平化为一个流,flatmap 方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
//把方法引用 Dish::getName 传给了 map 方法,来提取流中菜肴的名称
// getName 方法返回一个 String ,所以 map 方法输出的流的类型就是 Stream<String>
List<String> dishNames = menu.stream().map(Dish::getName)
.collect(toList());
List<Integer> dishNameLengths = menu.stream()
.map(Dish::getName).map(String::length)
.collect(toList());
//对于一张单词 表 , 如 何 返 回 一 张 列 表 , 列 出 里 面 各 不 相 同 的 字 符
//使用 flatMap 各个数组并不是分别映射成一个流,而是映射成流的内容.
List<String> uniqueCharacters =
words.stream().map(w -> w.split(&#34;&#34;)) //将每个单词转换为由其字母构成的数组
.flatMap(Arrays::stream) //将各个生成流扁平化为单个流
.distinct()
.collect(Collectors.toList());- [3] 查找和匹配
匹配 StreamAPI通过 allMatch 、 anyMatch 、 noneMatch 、 findFirst 和 findAny 方法.
- 否至少匹配一个元素 anyMatch
- 是否匹配所有元素 allMatch
- 没有任何元素匹配 noneMatch
if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println(&#34;The menu is (somewhat) vegetarian friendly!!&#34;);
}
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);查找元素
流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。
何时使用 findFirst 和 findAny?
你可能会想,为什么会同时有 findFirst 和 findAny 呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用 findAny ,因为它在使用并行流时限制较少。
- [3] 归约
归约操作: 可以理解为计算相关的操作,求和 最大值和最小值
//元素求和 一个初始值,
//这里是0 一个 BinaryOperator<T> 来将两个元素结合起来产生一个新值
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int sum = numbers.stream().reduce(0, Integer::sum);
//不接受初始值,但是会返回一个 Optional 对象
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
//(x, y) -> x < y ? x : y
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
//内置 count 方法可用来计算流中元素的个数
long count = menu.stream().count();map 和 reduce 的连接通常称为 map-reduce 模式
long count = menu.stream().count(); 实践
@Data////
@AllArgsConstructor
public class Trader{
private final String name;
private final String city;
}
@Data
@AllArgsConstructor
public class Transaction{
private final Trader trader;
private final int year;
private final int value;
public static void main(String ...args){
Trader raoul = new Trader(&#34;Raoul&#34;, &#34;Cambridge&#34;);
Trader mario = new Trader(&#34;Mario&#34;,&#34;Milan&#34;);
Trader alan = new Trader(&#34;Alan&#34;,&#34;Cambridge&#34;);
Trader brian = new Trader(&#34;Brian&#34;,&#34;Cambridge&#34;);
List<Transaction> transactions = Arrays.asList(
new Transaction(brian, 2011, 300),
new Transaction(raoul, 2012, 1000),
new Transaction(raoul, 2011, 400),
new Transaction(mario, 2012, 710),
new Transaction(mario, 2012, 700),
new Transaction(alan, 2012, 950)
);
//找出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());
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(&#34;Cambridge&#34;))
.distinct() //确保没有任何重复
.sorted(comparing(Trader::getName))
.collect(toList());
//返回所有交易员的姓名字符串,按字母顺序排序
Sring traderStr =
transactions.stream()
.map(transaction -> transaction.getTrader().getName())
.distinct()
.sorted()
.reduce(&#34;&#34;, (n1, n2) -> n1 + n2); //逐个拼接每个名字,得到一个将所有名字连接起来的 String
//此解决方案效率不高(所有字符串都被反复连接,每次迭代的时候都要建立一个新的 String 对象)
String traderStr =transactions.stream()
.map(transaction -> transaction.getTrader().getName())
.distinct()
.sorted()
.collect(joining());
//有没有交易员是在米兰工作的
boolean milanBased = transactions.stream()
.anyMatch(transaction -> transaction.getTrader() .getCity().equals(&#34;Milan&#34;));
//把一个谓词传递给 anyMatch ,检查是否有交易员在米兰工作
//打印生活在剑桥的交易员的所有交易额
transactions.stream()
.filter(t -> &#34;Cambridge&#34;.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));
} 数值流
int calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum); 这段问题是,它有一个暗含的装箱成本。每个 Integer 都必须拆箱成一个原始类型,再进行求和
1 原始类型流特化
Java 8引入了三个原始类型特化流接口来解决装箱拆箱问题, IntStream 、 DoubleStream 和LongStream ,分别将流中的元素特化为 int 、 long 和 double ,从而避免了暗含的装箱成本。
每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的 sum ,找到最大元素的 max 。此外还有在必要时再把它们转换回对象流的方法。这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似 int 和 Integer 之间的效率差异。
常用方法是 mapToInt 、 mapToDouble 和 mapToLong ,只是它们返回的是一个特化流,而不是Stream<T>
int calories = menu.stream()
.mapToInt(Dish::getCalories) //返回一个IntStream
.sum();
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
Optional 可以用Integer 、 String 等参考类型来参数化
三种原始流特化,也分别有一个 Optional 原始类型特化版本: OptionalInt 、 OptionalDouble 和 OptionalLong 。
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();
int max = maxCalories.orElse(1); //如果没有最大值的话,显式提供一个默认最大值
2 数值范围
Java 8引入了两个可以用于 IntStream 和 LongStream 的静态方法,帮助生成这种范围:range 和 rangeClosed 。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。
range 是不包含结束值的,而 rangeClosed 则包含结束值.
//表 示 范 围[1, 100]
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0); //一个从1到100的偶数流
System.out.println(evenNumbers.count()); //从1到100有50个偶数-- 数值流应用
勾股数
//三元组
stream.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});
IntStream.rangeClosed(1, 100)
.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.boxed()
.map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});
IntStream.rangeClosed(1, 100)
.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});
//生成值
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)}));
pythagoreanTriples.limit(5)
.forEach(t ->
System.out.println(t[0] + &#34;, &#34; + t[1] + &#34;, &#34; + t[2]));
//最终版本
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));构建流
1 由值创建流 Stream.of
Stream<String> stream = Stream.of(&#34;Java 8 &#34;, &#34;Lambdas &#34;, &#34;In &#34;, &#34;Action&#34;);
stream.map(String::toUpperCase).forEach(System.out::println);
//你可以使用 empty 得到一个空流,如下所示:
Stream<String> emptyStream = Stream.empty();2 由数组创建流
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();3 由文件生成流
Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files 中的很多静态方法都会返回一个流 long uniqueWords = 0;
try(Stream<String> lines =
Files.lines(Paths.get(&#34;data.txt&#34;), Charset.defaultCharset())){
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(&#34; &#34;)))
.distinct() //删除重复项
.count(); //数一数有多少各不相同的单词
}
catch(IOException e){ } //如果打开文件时出现异常则加以处理
4 由函数生成流:创建无限流
Stream API提供了两个静态方法来从函数生成流: Stream.iterate 和 Stream.generate 。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由 iterate和 generate 产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用 limit(n) 来对这种流加以限制,以避免打印无穷多个值。
在需要依次生成一系列值的时候应该使用 iterate //1. 迭代 这种 iterate 操作基本上是顺序的,因为结果取决于前一次应用
//这里只选择了前10个偶数。然后可以调用 forEach 终端操作来消费流,并分别打印每个元素。
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
//iterate 方法接受一个初始值(在这里是 0 ),还有一个依次应用在每个产生的新值上的
//Lambda( UnaryOperator<t> 类型)。这里,我们使用Lambda n -> n + 2 ,返回的是前一个元
//素加上2
//斐波纳契元组序列
/* 斐波纳契数列是著名的经典编程练习。下面这个数列就是斐波纳契数列的一部分: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 方法生成斐波纳契元组序列中的前20个元素。*/
Stream.iterate(new int[]{0, 1}, ???)
.limit(20)
.forEach(t -> System.out.println(&#34;(&#34; + t[0] + &#34;,&#34; + t[1] +&#34;)&#34;));
//2. 生成
//generate 方法也可让你按需生成一个无限流。但 generate 不是依次
//对每个新生成的值应用函数的。它接受一个 Supplier<T> 类型的Lambda提供新的值。
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
// IntStream 的 generate 方
//法会接受一个 IntSupplier ,而不是 Supplier<t> 。
//使用 IntStream 说明避免装箱操作的代码
IntStream ones = IntStream.generate(() -> 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);在并行代码中使用有状态的供应源是不安全的
其他要点
[1] 归约方法的优势与并行化
使用 reduce 的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行 reduce 操作。而迭代式求和例子要更新共享变量 sum ,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。
[2] 流操作:无状态和有状态
诸如 map 或 filter 等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是 无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)
但诸如 reduce 、 sum 、 max 等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。管流中有多少元素要处理,内部状态都是有界的.
诸如 sort 或 distinct 等操作一开始都和 filter 和 map 差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。这些操作叫作 有状态操作

中间操作和终端操作 的状态
[3] Optional 简介
Optional<T> 类( java.util.Optional )是一个容器类,代表一个值存在或不存在。在上面的代码中, findAny 可能什么元素都没找到。Java 8的库设计人员引入了 Optional<T> ,这样就不用返回众所周知容易出问题的 null 了。
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()); |
|