Java8笔记(1)
流
流是什么
流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不
是临时编写一个实现)。你可以把它们看成遍历数据集的高级迭代器。
用到的数据例子:
Dish 类
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return name;
}
public boolean isVegetarian() {
return vegetarian;
}
public int getCalories() {
return calories;
}
public Type getType() {
return type;
}
@Override
public String toString() {
return "Dish{" +
"name='" + name + '\'' +
", vegetarian=" + vegetarian +
", calories=" + calories +
", type=" + type +
'}';
}
public enum Type { MEAT, FISH, OTHER }
}
数据生成类:
public class Data {
public static List create(){
List menu = Arrays.asList(
new Dish("pork", false, 800, Dish.Type.MEAT),
new Dish("beef", false, 700, Dish.Type.MEAT),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH) );
return menu;
}
}
比较
比如你想要找出低热量的菜肴名称
Java 7 :
public class M1 {
public static void main(String[] args) {
List foods = CreateFoodData.create();
List lowCaloricfoods = new ArrayList<>();
for(Food food : foods){
if (food.getCalory() < 400){
lowCaloricfoods.add(food);
}
}
// 用匿名类对
//菜肴排序
Collections.sort(lowCaloricfoods, new Comparator() {
@Override
public int compare(Food o1, Food o2) {
return Integer.compare(o1.getCalory(),o2.getCalory());
}
});
System.out.println(lowCaloricfoods);
// 用了一个“垃圾变量” lowCaloricfoods 。它唯一的作用就是作为一次
//性的中间容器
}
}
Java 8 :
public class M2 {
public static void main(String[] args) {
List foods = CreateFoodData.create();
List lowCaloricfoodName =
foods.stream()
.filter(d -> d.getCalory() < 400)
.sorted(Comparator.comparing(Food::getCalory))
.map(Food::getName)
.collect(Collectors.toList());
System.out.println(lowCaloricfoodName);
}
}
新的方法有几个显而易见的好处:
代码是以声明性方式写的:说明想要完成什么(筛选热量低的菜肴)而不是说明如何实现一个操作(利用循环和 if 条件等控制流语句)。你在前面的章节中也看到了,这种方法加上行为参数化让你可以轻松应对变化的需求:你很容易再创建一个代码版本,利用Lambda表达式来筛选高卡路里的菜肴,而用不着去复制粘贴代码
你可以把几个基础操作链接起来,来表达复杂的数据处理流水线(在 filter 后面接上
sorted 、 map 和 collect 操作),同时保持代码清晰可读。 filter 的结果被传给了 sorted 方法,再传给 map 方法,最后传给 collect 方法。
因为 filter 、 sorted 、 map 和 collect 等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可能透明地充分利用你的多核架构
深入
Java 8中的集合支持一个新的stream 方法,它会返回一个流(接口定义在 java.util.stream.Stream 里)
流到底是什么呢?简短的定义就是“从支持数据处理操作的源生成的元素序列”
元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序
值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元
素(如 ArrayList 与 LinkedList )。但流的目的在于表达计算,比如你前面见到的
filter 、 sorted 和 map 。集合讲的是数据,流讲的是计算源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集
合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
- 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中
的常用操作,如 filter 、 map 、 reduce 、 find 、 match 、 sort 等。流操作可以顺序执行,也可并行执行。
流操作有两个重要的特点
- 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大
的流水线。这让我们下一章中的一些优化成为可能,如延迟和短路。流水线的操作可以
看作对数据源进行数据库式查询。
- 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行
具体例子:
public class M1 {
public static void main(String[] args) {
List menus = Data.create();
List threeHighCaloricDishNames =
menus.stream()
.filter(dish -> dish.getCalories() > 300)
.map(Dish::getName)
.limit(3)
.collect(Collectors.toList());
System.out.println(threeHighCaloricDishNames);
}
}
在本例中,我们先是对 menu 调用 stream 方法,由菜单得到一个流。数据源是菜肴列表(菜单),它给流提供一个元素序列。接下来,对流应用一系列数据处理操作: filter 、 map 、 limit和 collect
除了 collect 之外,所有这些操作都会返回另一个流,这样它们就可以接成一条流水线,于是就可以看作对源的一个查询。
最后, collect 操作开始处理流水线,并返回结果(它和别的操作不一样,因为它返回的不是流,在这里是一个 List )。在调用 collect 之前,没有任何结果产生,实际上根本就没有从 menu 里选择元素。你可以这么理解:链中的方法调用都在排队等待,直到调用 collect
操作简介:
- filter ——接受Lambda,从流中排除某些元素。在本例中,通过传递lambda d ->
d.getCalories() > 300 ,选择出热量超过300卡路里的菜肴
map ——接受一个Lambda,将元素转换成其他形式或提取信息。在本例中,通过传递方
法引用 Dish::getName ,相当于Lambda d -> d.getName() ,提取了每道菜的菜名limit ——截断流,使其元素不超过给定数量
- collect ——将流转换为其他形式。在本例中,流被转换为一个列表。
这段代码,与逐项处理菜单列表的代码有很大不同。首先,我们使用了声明性的方式来处理菜单数据,即你说的对这些数据需要做什么:“查找热量最高的三道菜的菜名。”你并没有去实现筛选( filter )、提取( map )或截断( limit )功能,Streams库已经自带了。因此,Stream API在决定如何优化这条流水线时更为灵活。例如,筛选、提取和截断操作可以一次进行,并在找到这三道菜后立即停止。
流与集合
比如说存在DVD里的电影,这就是一个集合(也许是字节,也许是帧,这个无所谓),因为它包含了整个数据结构。现在再来想想在互联网上通过视频流看同样的电影。现在这是一个流(字节流或帧流)。流媒体视频播放器只要提前下载用户观看位置的那几帧就可以了,这样不用等到流中大部分值计算出来,你就可以显示流的开始部分了(想想观看直播足球赛)。特别要注意,视频播放器可能没有将整个流作为集合,保存所需要的内存缓冲区——而且要是非得等到最后一帧出现才能开始看,那等待的时间就太长了。出于实现的考虑,你也可以让视频播放器把流的一部分缓存在集合里,但和概念上的差异不是一回事。
粗略地说,集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。(你可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分。)
相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。 这对编程有很大的好处
只能遍历一次
public class M1 {
public static void main(String[] args) {
List title = Arrays.asList(
"Java8", "In", "Action"
);
Stream stringStream = title.stream();
stringStream.forEach(System.out::println);
// Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
stringStream.forEach(System.out::println);
}
}
和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样(这里假设它是集合之类的可重复的源,如果是I/O通道就没戏了)
外部迭代与内部迭代
使用 Collection 接口需要用户去做迭代(比如用 for-each ),这称为外部迭代。 相反,Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了
流操作
例子:
List names = menu.stream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.limit(3)
.collect(toList());
两类操作:
filter 、 map 和 limit 可以连成一条流水线
collect 触发流水线执行并关闭它
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作
中间操作
诸如 filter 或 sorted 等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
为了搞清楚流水线中到底发生了什么,我们把代码改一改:
public class M1 {
public static void main(String[] args) {
List menu = Data.create();
List names =
menu.stream()
.filter(dish ->
{
System.out.println("filter " + dish.getName());
return dish.getCalories() > 300;
})
.map(dish ->
{
System.out.println("map " + dish.getName());
return dish.getName();
})
.limit(3)
.collect(Collectors.toList());
System.out.println("=====================================");
System.out.println(names);
}
}
输出:
filtering pork
mapping pork
filtering beef
mapping beef
filtering chicken
mapping chicken
[pork, beef, chicken]
你会发现,有好几种优化利用了流的延迟性质:
第一,尽管很多菜的热量都高于300卡路里,但只选出了前三个!这是因为 limit 操作和一种称为短路的技巧
第二,尽管 filter 和 map 是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环合并)
终端操作
终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如 List 、 Integer ,甚
至void
在下面的流水线中, forEach 是一个返回 void 的终端操作,它会对源中的每道菜应用一个Lambda。把 System.out.println 传递给 forEach ,并要求它打印出由 menu 生成的流中的每一个 Dish :
public class M1 {
public static void main(String[] args) {
List menu = Data.create();
menu.stream().forEach(System.out::println);
}
}
使用流
流的使用一般包括三件事:
一个数据源(如集合)来执行一个查询
一个中间操作链,形成一条流的流水线
一个终端操作,执行流水线,并能生成结果
筛选和切片
用谓词筛选
Streams 接口支持 filter 方法。该操作会接受一个谓词(一个返回boolean 的函数)作为参数,并返回一个包括所有符合谓词的元素的流。
例子:
public class M1 {
public static void main(String[] args) {
//筛选出所有素菜,创建一张素食菜单:
List menu = Data.create();
List vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());
}
}
筛选各异的元素
流还支持一个叫作 distinct 的方法,它会返回一个元素各异(根据流所生成元素的hashCode 和 equals 方法实现)的流
public class M1 {
public static void main(String[] args) {
List 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 个元素。
public class M1 {
public static void main(String[] args) {
List menu = Data.create();
// 你可以建立一个 List ,选出热量
//超过300卡路里的头三道菜:
List dishes = menu.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3)
.collect(Collectors.toList());
System.out.println(dishes);
}
}
跳过元素
流还支持 skip(n) 方法,返回一个扔掉了前 n 个元素的流。如果流中元素不足 n 个,则返回一个空流。请注意, limit(n) 和 skip(n) 是互补的!
public class M1 {
public static void main(String[] args) {
List menu = Data.create();
List dishes = menu.stream()
.filter(dish -> dish.getCalories() > 500)
.skip(2)
.collect(Collectors.toList());
System.out.println(dishes);
}
如何利用流来筛选前两个荤菜
public class Test_5_1 {
public static void main(String[] args) {
List menu = Data.create();
List dishes = menu.stream()
.filter(dish -> dish.getType() == Dish.Type.MEAT)
.collect(Collectors.toList());
System.out.println(dishes);
}
}
映射
一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API也通过 map 和 flatMap 方法提供了类似的工具。
对流中每一个元素应用函数
流支持 map 方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)
例如:
public class M1 {
// 下面的代码把方法引用 Dish::getName 传给了 map 方法,
//来提取流中菜肴的名称:
public static void main(String[] args) {
List menu = Data.create();
List names = menu.stream()
.map(Dish::getName)
.collect(Collectors.toList());
System.out.println(names);
}
}
因为 getName 方法返回一个 String ,所以 map 方法输出的流的类型就是Stream
给定一个单词列表,你想要返回另一个列表,显示每个单词中有几个字母。怎么做呢?你需要对列表中的每个元素应用一个函数。这听起来正好该用 map 方法去做!应用的函数应该接受一个单词,并返回其长度
public class M2 {
public static void main(String[] args) {
List words = Arrays.asList("Java 8", "Lambdas", "In", "Action");
List wordLengths = words.stream()
.map(String::length)
.collect(toList());
System.out.println(wordLengths);
}
}
提取菜名的例子。如果你要找出每道菜的名称有多长,怎么做
public class M3 {
public static void main(String[] args) {
List menu = Data.create();
List names = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(Collectors.toList());
System.out.println(names);
}
流的扁平化
对于一张单词 表 , 如 何 返 回 一 张 列 表 , 列 出 里 面 各 不 相 同 的 字 符 呢 ? 例 如 , 给 定 单 词 列 表["Hello","World"] ,你想要返回列表 ["H","e","l", "o","W","r","d"] 。
版本一:
public class M1 {
public static void main(String[] args) {
String[] Words = {"Goodbye", "World","kdffj"};
Arrays.stream(Words)
.map(word -> word.split(""))
.distinct()
.collect(Collectors.toList());
}
}
这个方法的问题在于,传递给 map 方法的Lambda为每个单词返回了一个 String[] ( String列表)。因此, map 返回的流实际上是 Stream
可以用 flatMap 来解决这个问题
尝试使用 map 和 Arrays.stream()
首先,你需要一个字符流,而不是数组流。有一个叫作 Arrays.stream() 的方法可以接受个数组并产生一个流
String[] arrayOfWords = {"Goodbye", "World"};
Stream streamOfwords = Arrays.stream(arrayOfWords);
把它用在前面的那个流水线里:
words.stream()
.map(word -> word.split(""))
.map(Arrays::stream)
.distinct()
.collect(toList());
当前的解决方案仍然搞不定!这是因为,你现在得到的是一个流的列表(更准确地说是Stream
使用 flatMap
如:
public class M2 {
public static void main(String[] args) {
String[] arrayOfWords = {"Goodbye", "World"};
Stream streamOfwords = Arrays.stream(arrayOfWords);
List uniqueCharacters =
streamOfwords
// 将每个单词转换为由其字母构成的数组
.map(w -> w.split(""))
// 将各个生成流扁平化为单个流
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueCharacters);
}
}
使用 flatMap 方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用 map(Arrays::stream) 时生成的单个流都被合并起来,即扁平化为一个流
一言以蔽之, flatmap 方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
给定一个数字列表,如何返回一个由每个数的平方构成的列表
如给定[1, 2, 3, 4,5],应该返回[1, 4, 9, 16, 25]
public static void main(String[] args) {
List numbers = Arrays.asList(1, 2, 3, 4, 5);
List squares =
numbers.stream()
.map( n -> n * n)
.collect(Collectors.toList());
System.out.println(squares);
}
给定两个数字列表,如何返回所有的数对
例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]
public static void main(String[] args) {
List numbers1 = Arrays.asList(1, 2, 3);
List numbers2 = Arrays.asList(3, 4);
List pairs =
numbers1.stream()
.flatMap(i -> numbers2.stream()
.map( j -> new int[]{i,j}))
.collect(Collectors.toList());
System.out.println(pairs);
}
扩展前一个例子,只返回总和能被3整除的数对
例如(2, 4)和(3, 3)是可以的
public static void main(String[] args) {
List numbers1 = Arrays.asList(1, 2, 3);
List numbers2 = Arrays.asList(3, 4);
List pairs =
numbers1.stream()
.flatMap(i ->
numbers2.stream()
.filter(j -> (i + j) % 3 == 0)
.map(j -> new int[]{i, j})
)
.collect(toList());
System.out.println(pairs);
}
查找和匹配
另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream
API通过 allMatch 、 anyMatch 、 noneMatch 、 findFirst 和 findAny 方法提供了这样的工具
检查谓词是否至少匹配一个元素
anyMatch 方法可以回答“流中是否有一个元素能匹配给定的谓词”
用它来看看菜单里面是否有素食可选择:
public class M1 {
public static void main(String[] args) {
List menu = Data.create();
if (menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("isVegetarian");
}
//anyMatch 方法返回一个 boolean ,因此是一个终端操作
}
}
检查谓词是否匹配所有元素
allMatch 方法的工作原理和 anyMatch 类似,但它会看看流中的元素是否都能匹配给定的谓词
用它来看看菜品是否有利健康(即所有菜的热量都低于1000卡路里):
public class M1 {
public static void main(String[] args) {
List menu = Data.create();
boolean isOk = menu.stream()
.allMatch(dish -> dish.getCalories() < 1000);
System.out.println(isOk);
}
}
noneMatch
和 allMatch 相对的是 noneMatch 。它可以确保流中没有任何元素与给定的谓词匹配。比如,你可以用 noneMatch 重写前面的例子
boolean isHealthy = menu.stream()
.noneMatch(d -> d.getCalories() >= 1000);
anyMatch 、 allMatch 和 noneMatch 这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java中 && 和 || 运算符短路在流中的版本
查找元素
findAny 方法将返回当前流中的任意元素。它可以与其他流操作结合使用
比如,你可能想找到一道素食菜肴。你可以结合使用 filter 和 findAny 方法来实现这个查询
public class M1 {
public static void main(String[] args) {
List menu = Data.create();
Optional dish = menu.stream()
.filter(Dish::isVegetarian)
.findAny();
System.out.println(dish);
}
}
查找第一个元素
有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由 List 或排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个 findFirst方法,它的工作方式类似于 findany
给定一个数字列表,下面的代码能找出第一个平方能被3整除的数:
public class M1 {
public static void main(String[] args) {
List someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional firstSquareDivisibleByThree =
someNumbers.stream()
.map(x -> x * x)
.filter(x -> x % 3 == 0)
.findFirst(); // 9
System.out.println(firstSquareDivisibleByThree);
}
}
归约
把一个流中的元素组合起来,使用 reduce 操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个 Integer 。这样的查询可以被归类为归约操作(将流归约成一个值)。
用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。
元素求和
如何使用 for-each 循环来对数字列表中的元素求和:
int sum = 0;
for (int x : numbers) {
sum += x;
}
numbers 中的每个元素都用加法运算符反复迭代来得到结果。通过反复使用加法,你把一个数字列表归约成了一个数字。这段代码中有两个参数:
总和变量的初始值,在这里是 0
将列表中所有元素结合在一起的操作,在这里是 +
要是还能把所有的数字相乘,而不必去复制粘贴这段代码,岂不是很好?这正是 reduce 操作的用武之地,它对这种重复应用的模式做了抽象
像下面这样对流中所有的元素求和
public static void main(String[] args) {
List numbers = Arrays.asList(
121,454,55,66
);
int sum = numbers.stream()
.reduce(0,(a,b) -> a +b );
System.out.println(sum);
}
reduce 接受两个参数:
- 一个初始值,这里是0
- 一个 BinaryOperator
来将两个元素结合起来产生一个新值,这里我们用的是
lambda (a, b) -> a + b
也很容易把所有的元素相乘,只需要将另一个Lambda: (a, b) -> a * b 传递给 reduce
操作就可以了
int product = numbers.stream().reduce(1, (a, b) -> a * b);
可以使用方法引用让这段代码更简洁。在Java 8中, Integer 类现在有了一个静态的 sum方法来对两个数求和,这恰好是我们想要的,用不着反复用Lambda写同一段代码了
int sum = numbers.stream().reduce(0, Integer::sum);
最大值和最小值
利用刚刚学到的 reduce来计算流中最大或最小的元素
reduce 接受两个参数
- 一个初始值
- 一个Lambda来把两个流元素结合起来并产生一个新值
reduce 操作会考虑新值和流中下一个元素,并产生一个新的最大值,直到整个流消耗完
public class M1 {
public static void main(String[] args) {
List numbers = Arrays.asList(
121,454,55,66
);
Optional max =
numbers.stream()
.reduce(Integer::max);
System.out.println(max);
}
}
要计算最小值,你需要把 Integer.min 传给 reduce 来替换 Integer.max :
付诸实践
使用数据:
public class Transaction {
private final Trader trader;
private final int year;
private final int value;
public Transaction(Trader trader, int year, int value) {
this.trader = trader;
this.year = year;
this.value = value;
}
public Trader getTrader() {
return trader;
}
public int getYear() {
return year;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return "Transaction{" +
"trader=" + trader +
", year=" + year +
", value=" + value +
'}';
}
}
public class Trader {
private final String name;
private final String city;
public Trader(String name, String city) {
this.name = name;
this.city = city;
}
public String getName() {
return name;
}
public String getCity() {
return city;
}
@Override
public String toString() {
return "Trader{" +
"name='" + name + '\'' +
", city='" + city + '\'' +
'}';
}
}
public class Data {
static Trader raoul = new Trader("Raoul", "Cambridge");
static Trader mario = new Trader("Mario","Milan");
static Trader alan = new Trader("Alan","Cambridge");
static Trader brian = new Trader("Brian","Cambridge");
public static List create(){
List 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)
);
return transactions;
}
}
找出2011年发生的所有交易,并按交易额排序(从低到高)
public static void main(String[] args) {
List transactions = Data.create();
List transactions2011 =
transactions.stream()
.filter(transaction -> transaction.getYear() == 2011)
.sorted(Comparator.comparing(Transaction::getValue))
.collect(Collectors.toList());
System.out.println(transactions2011);
}
交易员都在哪些不同的城市工作过
public static void main(String[] args) {
List transactions = Data.create();
List cities =
transactions.stream()
.map(transaction -> transaction.getTrader().getCity())
.distinct()
.collect(Collectors.toList());
System.out.println(cities);
// 或者
Set cities_1 =
transactions.stream()
.map(transaction -> transaction.getTrader().getCity())
.collect(toSet());
System.out.println(cities_1);
}
查找所有来自于剑桥的交易员,并按姓名排序
public static void main(String[] args) {
List transactions = Data.create();
List traders =
transactions.stream()
.map(Transaction::getTrader)
.filter(trader -> trader.getCity().equals("Cambridge"))
.distinct()
.sorted(Comparator.comparing(Trader::getName))
.collect(Collectors.toList());
System.out.println(traders);
}
返回所有交易员的姓名字符串,按字母顺序排序
public static void main(String[] args) {
List transactions = Data.create();
String traderStr =
transactions.stream()
.map(transaction -> transaction.getTrader().getName())
.distinct()
.sorted()
.reduce("",(n1,n2) -> n1 + n2);
System.out.println(traderStr);
// 请注意,此解决方案效率不高(所有字符串都被反复连接,每次迭代的时候都要建立一个新
//的 String 对象)
String traderStr_1 =
transactions.stream()
.map(transaction -> transaction.getTrader().getName())
.distinct()
.sorted()
.collect(joining());
System.out.println(traderStr_1);
}
有没有交易员是在米兰工作的
public static void main(String[] args) {
List transactions = Data.create();
boolean milanBased =
transactions.stream()
.anyMatch(transaction -> transaction.getTrader()
.getCity()
.equals("Milan"));
System.out.println(milanBased);
}
打印生活在剑桥的交易员的所有交易额
public static void main(String[] args) {
List transactions = Data.create();
transactions.stream()
.filter(transaction -> transaction.getTrader()
.getCity().equals("Cambridge"))
.forEach(System.out::println);
}
所有交易中,最高的交易额是多少
public static void main(String[] args) {
List transactions = Data.create();
Optional highestValue =
transactions.stream()
.map(Transaction::getValue)
.reduce(Integer::max);
System.out.println(highestValue);
}
找到交易额最小的交易
public static void main(String[] args) {
List transactions = Data.create();
Optional smallestTransaction =
transactions.stream()
.reduce((t1,t2) ->
t1.getValue() < t2.getValue() ? t1 : t2);
// 流支持 min 和 max 方法,它们可以接受一个 Comparator 作为参数,指定
//计算最小或最大值时要比较哪个键值:
Optional smallestTransaction_1 =
transactions.stream()
.min(comparing(Transaction::getValue));
}
数值流
可以使用 reduce 方法计算流中元素的总和。例如,你可以像下面这样计算菜单的热量:
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
这段代码的问题是,它有一个暗含的装箱成本。每个 Integer 都必须拆箱成一个原始型,再进行求和
原始类型流特化
Java 8引入了三个原始类型特化流接口来解决这个问题: IntStream 、 DoubleStream 和LongStream ,分别将流中的元素特化为 int 、 long 和 double ,从而避免了暗含的装箱成本
映射到数值流
将流转换为特化版本的常用方法是 mapToInt 、 mapToDouble 和 mapToLong
这些方法和前面说的 map 方法的工作方式一样,只是它们返回的是一个特化流,而不是 Stream
比如用 mapToInt 对 menu 中的卡路里求和:
public static void main(String[] args) {
List menu = Data.create();
int calories =
menu.stream()
.mapToInt(Dish::getCalories)
.sum();
}
这里, mapToInt 会从每道菜中提取热量(用一个 Integer 表示),并返回一个 IntStream(而不是一个 Stream
如果流是空的, sum 默认返回 0 。 IntStream 还支持其他的方便方法,如max 、 min 、 average 等
转换回对象流
同样,一旦有了数值流,你可能会想把它转换回非特化流。例如, IntStream 上的操作只能产生原始整数: IntStream 的 map 操作接受的Lambda必须接受 int 并返回 int (一个IntUnaryOperator )。但是你可能想要生成另一类值,比如 Dish
为此,你需要访问 Stream接口中定义的那些更广义的操作。要把原始流转换成一般流(每个 int 都会装箱成一个Integer ),可以使用 boxed 方法
public static void main(String[] args) {
List menu = Data.create();
IntStream intStream = menu.stream()
// 将 Stream 转换为数值流
.mapToInt(Dish::getCalories);
// 将数值流转换为 Stream
Stream stream = intStream.boxed();
System.out.println(stream.toString());
}
默认值 OptionalInt
求和的那个例子很容易,因为它有一个默认值: 0 。但是,如果你要计算 IntStream 中的最大元素,就得换个法子了,因为 0 是错误的结果。如何区分没有元素的流和最大值真的是 0 的流呢?
前面我们介绍了 Optional 类,这是一个可以表示值存在或不存在的容器。 Optional 可以用Integer 、 String 等参考类型来参数化。对于三种原始流特化,也分别有一个Optional 原始类型特化版本: OptionalInt 、 OptionalDouble 和 OptionalLong 。
要找到 IntStream 中的最大元素,可以调用 max 方法,它会返回一个 OptionalInt
public static void main(String[] args) {
List menu = Data.create();
OptionalInt maxCalories =
menu.stream()
.mapToInt(Dish::getCalories)
.max();
System.out.println(maxCalories);
}
如果没有最大值的话,你就可以显式处理 OptionalInt 去定义一个默认值了
int max = maxCalories.orElse(1);
数值范围
假设你想要生成1和100之间的所有数字。Java 8引入了两个可以用于 IntStream 和 LongStream 的静态方法,帮助生成这种范围:range 和 rangeClosed 。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range 是不包含结束值的,而rangeClosed 则包含结束值
public static void main(String[] args) {
IntStream evenNumbers = IntStream.rangeClosed(1,100)
.filter(n->n % 2 == 0);
// 从1到100有50个偶数
System.out.println(evenNumbers.count());
}
数值流应用:勾股数
创建一个勾股数流
表示三元数
第一步是定义一个三元数。虽然更恰当的做法是定义一个新的类来表示三元数,但这里你可以使用具有三个元素的 int 数组,比如 new int[]{3, 4, 5} ,来表示勾股数(3, 4, 5)。现在你就可以用数组索引访问每个元素了
筛选成立的组合
假定有人为你提供了三元数中的前两个数字: a 和 b 。怎么知道它是否能形成一组勾股数呢?你需要测试 a * a + b * b 的平方根是不是整数,也就是说它没有小数部分——在Java里可以使用 expr % 1 表示。如果它不是整数,那就是说 c 不是整数。你可以用filter 操作表达这个要求
filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
生成三元组
在筛选之后,你知道 a 和 b 能够组成一个正确的组合。现在需要创建一个三元组。你可以使用map 操作,像下面这样把每个元素转换成一个勾股数组
tream.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});
生成 b 值
需要生成 b 的值。前面已经看到, Stream.rangeClosed 让你可以在给定区间内生成一个数值流。你可以用它来给 b 提供数值,这里是1到100
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)});
你在 filter 之后调用 boxed ,从 rangeClosed 返回的 IntStream 生成一个Stream
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)});
生成值
给出了 a 的值。 现在,只要已知 a 的值,你就有了一个可以生成勾股数的流。如何解决这个问题呢?就像 b 一样,你需要为 a 生成数值!最终的解决方案如下所示
Stream 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)})
);
flatMap 又是怎么回事呢?首先,创建一个从1到100的数值范围来生成 a 的值。对每
个给定的 a 值,创建一个三元数流。要是把 a 的值映射到三元数流的话,就会得到一个由流构成的流。 flatMap 方法在做映射的同时,还会把所有生成的三元数流扁平化成一个流。这样你就得到了一个三元数流。还要注意,我们把 b 的范围改成了 a 到100。没有必要再从1开始了,否则就会造成重复的三元数,例如(3,4,5)和(4,3,5)
最终实现:
public static void main(String[] args) {
Stream 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] + ", " + t[1] + ", " + t[2]));
}
构建流
由值创建流
可以使用静态方法 Stream.of ,通过显式值创建一个流。它可以接受任意数量的参数
例子:
public class M1 {
//以下代码直接使用 Stream.of 创建了一个字符串流。然后,你可以将字符串转换为大写,再
//一个个打印出来
public static void main(String[] args) {
Stream stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
}
}
由数组创建流
可以使用静态方法 Arrays.stream 从数组创建一个流。它接受一个数组作为参数
public class M1 {
//将一个原始类型 int 的数组转换成一个 IntStream
public static void main(String[] args) {
int[] nums = {1,2,5,96,4556,5,6,5};
int sum = Arrays.stream(nums).sum();
System.out.println(sum);
}
}
由文件生成流
Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files 中的很多静态方法都会返回一个流
public class M1 {
//,一个很有用的方法是
//Files.lines ,它会返回一个由指定文件中的各行构成的字符串流。使用你迄今所学的内容,
//你可以用这个方法看看一个文件中有多少各不相同的词:
public static void main(String[] args) {
long uniqueWords = 0;
try(Stream lines =
Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()
.count();
}
catch(IOException e){
}
}
}
由函数生成流:创建无限流
Stream API提供了两个静态方法来从函数生成流: Stream.iterate 和Stream.generate
这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由 iterate和 generate 产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用 limit(n) 来对这种流加以限制,以避免打印无穷多个值
迭代
public class M1 {
public static void main(String[] args) {
Stream.iterate(0,n-> n + 2)
.limit(10)
.forEach(System.out::println);
}
// iterate 方法接受一个初始值(在这里是 0 ),还有一个依次应用在每个产生的新值上的
//Lambda( UnaryOperator 类型)。这里,我们使用Lambda n -> n + 2 ,返回的是前一个元
//素加上2。因此, iterate 方法生成了一个所有正偶数的流:流的第一个元素是初始值 0 。然后加
//上 2 来生成新的值 2 ,再加上 2 来得到新的值 4 ,以此类推。这种 iterate 操作基本上是顺序的,
//因为结果取决于前一次应用
}
此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。正如我们前面所讨论的,这是流和集合之间的一个关键区别。我们使用 limit 方法来显式限制流的大小。这里只选择了前10个偶数。然后可以调用 forEach 终端操作来消费流,并分别打印每个元素
生成
与 iterate 方法类似, generate 方法也可让你按需生成一个无限流。但 generate 不是依次对每个新生成的值应用函数的。它接受一个 Supplier
例子:
这段代码将生成一个流,其中有五个0到1之间的随机双精度数
public static void main(String[] args) {
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
}