在本节中,咱们会将迄今学到的关于流的知识付诸实践。咱们来看一个不一样的领域:执行交易的交易员。你的经理让你为八个查询找到答案。java
如下是咱们要处理的领域,一个 Traders 和 Transactions 的列表:git
Trader raoul = new Trader("Raoul", "Cambridge"); Trader mario = new Trader("Mario", "Milan"); Trader alan = new Trader("Alan", "Cambridge"); Trader brian = new Trader("Brian", "Cambridge"); 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) );
Trader和Transaction类的定义:github
public class Trader { private String name; private String city; public Trader(String n, String c){ this.name = n; this.city = c; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } @Override public String toString() { return "Trader{" + "name='" + name + '\'' + ", city='" + city + '\'' + '}'; } }
Transaction类:数组
public class Transaction { private Trader trader; private Integer year; private Integer value; public Transaction(Trader trader, Integer year, Integer value) { this.trader = trader; this.year = year; this.value = value; } public Trader getTrader() { return trader; } public void setTrader(Trader trader) { this.trader = trader; } public Integer getYear() { return year; } public void setYear(Integer year) { this.year = year; } public Integer getValue() { return value; } public void setValue(Integer value) { this.value = value; } @Override public String toString() { return "Transaction{" + "trader=" + trader + ", year=" + year + ", value=" + value + '}'; } }
List<Transaction> tr2011 = transactions.stream() // 筛选出2011年发生的全部交易 .filter(transaction -> transaction.getYear() == 2011) // 按照交易额从低到高排序 .sorted(Comparator.comparing(Transaction::getValue)) // 转为集合 .collect(Collectors.toList());
太棒了,第一个问题咱们很轻松的就解决了!首先,将transactions集合转为流,而后给filter传递一个谓词来选择2011年的交易,接着按照交易额从低到高进行排序,最后将Stream中的全部元素收集到一个List集合中。缓存
List<String> cities = transactions.stream() // 提取出交易员所工做的城市 .map(transaction -> transaction.getTrader().getCity()) // 去除已有的城市 .distinct() // 将Stream中全部的元素转为一个List集合 .collect(Collectors.toList());
是的,咱们很简单的完成了第二个问题。首先,将transactions集合转为流,而后使用map提取出与交易员相关的每位交易员所在的城市,接着使用distinct去除重复的城市(固然,咱们也能够去掉distinct,在最后咱们就要使用collect,将Stream中的元素转为一个Set集合。collect(Collectors.toSet())),咱们只须要不一样的城市,最后将Stream中的全部元素收集到一个List中。dom
List<Trader> traders = transactions.stream() // 从交易中提取全部的交易员 .map(Transaction::getTrader) // 进选择位于剑桥的交易员 .filter(trader -> "Cambridge".equals(trader.getCity())) // 确保没有重复 .distinct() // 对生成的交易员流按照姓名进行排序 .sorted(Comparator.comparing(Trader::getName)) .collect(Collectors.toList());
第三个问题,从交易中提取全部的交易员,而后进选择位于剑桥的交易员确保没有重复,接着对生成的交易员流按照姓名进行排序。ide
String traderStr = transactions.stream() // 提取全部交易员姓名,生成一个 Strings 构成的 Stream .map(transaction -> transaction.getTrader().getName()) // 只选择不相同的姓名 .distinct() // 对姓名按字母顺序排序 .sorted() // 逐个拼接每一个名字,获得一个将全部名字链接起来的 String .reduce("", (n1, n2) -> n1 + " " + n2);
这些问题,咱们都很轻松的就完成!首先,提取全部交易员姓名,生成一个 Strings 构成的 Stream而且只选择不相同的姓名,而后对姓名按字母顺序排序,最后使用reduce将名字拼接起来!函数
请注意,此解决方案效率不高(全部字符串都被反复链接,每次迭代的时候都要创建一个新
的 String 对象)。下一章中,你将看到一个更为高效的解决方案,它像下面这样使用 joining (其
内部会用到 StringBuilder ):学习
String traderStr = transactions.stream() .map(transaction -> transaction.getTrader().getName()) .distinct() .sorted() .collect(joining());
boolean milanBased = transactions.stream() // 把一个谓词传递给 anyMatch ,检查是否有交易员在米兰工做 .anyMatch(transaction -> "Milan".equals(transaction.getTrader() .getCity()));
第五个问题,依旧很简单把一个谓词传递给 anyMatch ,检查是否有交易员在米兰工做。ui
transactions.stream() // 选择住在剑桥的交易员所进行的交易 .filter(t -> "Cambridge".equals(t.getTrader().getCity())) // 提取这些交易的交易额 .map(Transaction::getValue) // 打印每一个值 .forEach(System.out::println);
第六个问题,首先选择住在剑桥的交易员所进行的交易,接着提取这些交易的交易额,而后就打印出每一个值。
Optional<Integer> highestValue = transactions.stream() // 提取每项交易的交易额 .map(Transaction::getValue) // 计算生成的流中的最大值 .reduce(Integer::max);
第七个问题,首先提取每项交易的交易额,而后使用reduce计算生成的流中的最大值。
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));
上面的八个问题,咱们经过Stream很轻松的就完成了,真是太棒了!
咱们在前面看到了可使用 reduce 方法计算流中元素的总和。例如,你能够像下面这样计
算菜单的热量:
int calories = menu.stream() .map(Dish::getCalories) .reduce(0, Integer::sum);
这段代码的问题是,它有一个暗含的装箱成本。每一个 Integer 都必须拆箱成一个原始类型,
再进行求和。要是能够直接像下面这样调用 sum 方法,岂不是更好?
int calories = menu.stream() .map(Dish::getCalories) .sum();
但这是不可能的。问题在于 map 方法会生成一个 Stream<T> 。虽然流中的元素是 Integer 类
型,但 Streams 接口没有定义 sum 方法。为何没有呢?比方说,你只有一个像 menu 那样的Stream<Dish> ,把各类菜加起来是没有任何意义的。但不要担忧,Stream API还提供了原始类型流特化,专门支持处理数值流的方法。
Java 8引入了三个原始类型特化流接口来解决这个问题: IntStream 、 DoubleStream 和
LongStream ,分别将流中的元素特化为 int 、 long 和 double ,从而避免了暗含的装箱成本。每一个接口都带来了进行经常使用数值归约的新方法,好比对数值流求和的 sum ,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的缘由并不在于流的复杂性,而是装箱形成的复杂性——即相似 int 和 Integer 之间的效率差别。
1.映射到数值流
将流转换为特化版本的经常使用方法是 mapToInt 、 mapToDouble 和 mapToLong 。这些方法和前
面说的 map 方法的工做方式同样,只是它们返回的是一个特化流,而不是 Stream<T> 。例如,咱们能够像下面这样用 mapToInt 对 menu 中的卡路里求和:
int calories = menu.stream() // 返回一个IntStream .mapToInt(Dish::getCalories) .sum();
这里, mapToInt 会从每道菜中提取热量(用一个 Integer 表示),并返回一个 IntStream
(而不是一个 Stream<Integer> )。而后你就能够调用 IntStream 接口中定义的 sum 方法,对卡
路里求和了!请注意,若是流是空的, sum 默认返回 0 。 IntStream 还支持其余的方便方法,如
max 、 min 、 average 等。
2.转换回对象流
一样,一旦有了数值流,你可能会想把它转换回非特化流。例如, IntStream 上的操做只能
产生原始整数: IntStream 的 map 操做接受的Lambda必须接受 int 并返回 int (一个
IntUnaryOperator )。可是你可能想要生成另外一类值,好比 Dish 。为此,你须要访问 Stream
接口中定义的那些更广义的操做。要把原始流转换成通常流(每一个 int 都会装箱成一个
Integer ),可使用 boxed 方法,以下所示:
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); Stream<Integer> stream = intStream.boxed();
3.默认值 OptionalInt
求和的那个例子很容易,由于它有一个默认值: 0 。可是,若是你要计算 IntStream 中的最
大元素,就得换个法子了,由于 0 是错误的结果。如何区分没有元素的流和最大值真的是 0 的流呢?
前面咱们介绍了 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);
和数字打交道时,有一个经常使用的东西就是数值范围。好比,假设你想要生成1和100之间的全部数字。Java 8引入了两个能够用于 IntStream 和 LongStream 的静态方法,帮助生成这种范围:
range 和 rangeClosed 。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但
range 是不包含结束值的,而 rangeClosed 则包含结束值。让咱们来看一个例子:
// 一个从1到100的偶数流 包含结束值 IntStream evenNumbers = IntStream.rangeClosed(1, 100) .filter(n -> n % 2 == 0); // 从1到100共有50个偶数 System.out.println(evenNumbers.count());
这里咱们用了 rangeClosed 方法来生成1到100之间的全部数字。它会产生一个流,而后你
能够连接 filter 方法,只选出偶数。到目前为止尚未进行任何计算。最后,你对生成的流调
用 count 。由于 count 是一个终端操做,因此它会处理流,并返回结果 50 ,这正是1到100(包括
两端)中全部偶数的个数。请注意,比较一下,若是改用 IntStream.range(1, 100) ,则结果
将会是 49 个偶数,由于 range 是不包含结束值的。
但愿到如今,咱们已经让你相信,流对于表达数据处理查询是很是强大而有用的。到目前为
止,你已经可以使用 stream 方法从集合生成流了。此外,咱们还介绍了如何根据数值范围建立
数值流。但建立流的方法还有许多!本节将介绍如何从值序列、数组、文件来建立流,甚至由生成函数来建立无限流!
你可使用静态方法 Stream.of ,经过显式值建立一个流。它能够接受任意数量的参数。例
如,如下代码直接使用 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 的数组转换成一个 IntStream ,以下所示:
int[] numbers = {2, 3, 5, 7, 11, 13}; // 总和41 int sum = Arrays.stream(numbers).sum();
Java中用于处理文件等I/O操做的NIO API(非阻塞 I/O)已更新,以便利用Stream API。
java.nio.file.Files 中的不少静态方法都会返回一个流。例如,一个颇有用的方法是
Files.lines ,它会返回一个由指定文件中的各行构成的字符串流。使用咱们迄今所学的内容,咱们能够用这个方法看看一个文件中有多少各不相同的词:
long uniqueWords; try (Stream<String> lines = Files.lines(Paths.get(ClassLoader.getSystemResource("data.txt").toURI()), Charset.defaultCharset())) { uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) .distinct() .count(); System.out.println("uniqueWords:" + uniqueWords); } catch (IOException e) { e.fillInStackTrace(); } catch (URISyntaxException e) { e.printStackTrace(); }
你可使用 Files.lines 获得一个流,其中的每一个元素都是给定文件中的一行。而后,你
能够对 line 调用 split 方法将行拆分红单词。应该注意的是,你该如何使用 flatMap 产生一个扁平的单词流,而不是给每一行生成一个单词流。最后,把 distinct 和 count 方法连接起来,数数流中有多少各不相同的单词。
Stream API提供了两个静态方法来从函数生成流: Stream.iterate 和 Stream.generate 。
这两个操做能够建立所谓的无限流:不像从固定集合建立的流那样有固定大小的流。由 iterate和 generate 产生的流会用给定的函数按需建立值,所以能够无穷无尽地计算下去!通常来讲,应该使用 limit(n) 来对这种流加以限制,以免打印无穷多个值。
1.迭代
咱们先来看一个 iterate 的简单例子,而后再解释:
Stream.iterate(0, n -> n + 2) .limit(10) .forEach(System.out::println);
iterate 方法接受一个初始值(在这里是 0 ),还有一个依次应用在每一个产生的新值上的
Lambda( UnaryOperator<t> 类型)。这里,咱们使用Lambda n -> n + 2 ,返回的是前一个元素加上2。所以,iterate方法生成了一个全部正偶数的流:流的第一个元素是初始值 0 。而后加上 2 来生成新的值 2 ,再加上 2 来获得新的值 4 ,以此类推。这种 iterate 操做基本上是顺序的,由于结果取决于前一次应用。请注意,此操做将生成一个无限流——这个流没有结尾,由于值是按需计算的,能够永远计算下去。咱们说这个流是无界的。正如咱们前面所讨论的,这是流和集合之间的一个关键区别。咱们使用limit方法来显式限制流的大小。这里只选择了前10个偶数。而后能够调用 forEach 终端操做来消费流,并分别打印每一个元素。
2.生成
与 iterate 方法相似, generate 方法也可以让你按需生成一个无限流。但 generate 不是依次
对每一个新生成的值应用函数的。它接受一个 Supplier<T> 类型的Lambda提供新的值。咱们先来
看一个简单的用法:
Stream.generate(Math::random) .limit(5) .forEach(System.out::println);
这段代码将生成一个流,其中有五个0到1之间的随机双精度数。例如,运行一次获得了下面
的结果:
0.8404010101858976 0.03607897810804739 0.025199243727344833 0.8368092999566692 0.14685668895309267
Math.Random 静态方法被用做新值生成器。一样,你能够用 limit 方法显式限制流的大小,
不然流将会无限长。
你可能想知道, generate 方法还有什么用途。咱们使用的供应源(指向 Math.random 的方
法引用)是无状态的:它不会在任何地方记录任何值,以备之后计算使用。但供应源不必定是无状态的。你能够建立存储状态的供应源,它能够修改状态,并在为流生成下一个值时使用。
咱们在这个例子中会使用 IntStream 说明避免装箱操做的代码。 IntStream 的 generate 方
法会接受一个 IntSupplier ,而不是 Supplier<t> 。例如,能够这样来生成一个全是1的无限流:
IntStream ones = IntStream.generate(() -> 1);
还记得第三章的笔记中,Lambda容许你建立函数式接口的实例,只要直接内联提供方法的实
现就能够。你也能够像下面这样,经过实现 IntSupplier 接口中定义的 getAsInt 方法显式传递一个对象(虽然这看起来是平白无故地绕圈子,也请你耐心看):
IntStream twos = IntStream.generate(new IntSupplier(){ @Override public int getAsInt(){ return 2; } });
generate 方法将使用给定的供应源,并反复调用 getAsInt 方法,而这个方法老是返回 2 。
但这里使用的匿名类和Lambda的区别在于,匿名类能够经过字段定义状态,而状态又能够用
getAsInt 方法来修改。这是一个反作用的例子。咱们迄今见过的全部Lambda都是没有反作用的;它们没有改变任何状态。
这一章的东西不少,收获也不少!如今你能够更高效地处理集合了。事实上,流让你能够简洁地表达复杂的数据处理查询。此外,流能够透明地并行化。如下是咱们应从本章中学到的关键概念。
这一章的读书笔记中,咱们学习和了解到了:
Github: chap5
Gitee: chap5