本文介绍java 8 Stream API提供的诸多操作,利用这些操作可以让你快速的完成复制的数据查询,如筛选、切片、映射、查找、匹配和规约。包括一些特殊流的用法:数值流、文件流、数组流,无限流。

筛选和切片

用谓词筛选

Stream接口提供了filter方法,该方法接收一个谓词作为参数,并返回一个包含所有符合谓词的元素的流。

比如,筛选出所有的素菜:

1
List<Dish> dishes = menu.stream().filter(Dish::isVegetarian).collect(toList());

元素去重

Stream还支持distinct方法,它会返回不重复的元素的流(根据元素的hashCode和equals方法)。
例如,筛选出下面列表中的所有不重复的偶数:

1
2
3
4
List<Integer> numbers = Arrays.asList(1,2,5,2,6,3,3,2,4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct() // 根据元素的hashcode和equals方法过滤重复元素

截断流

Stream还支持limit方法,返回一个不超过给定长度的流。所需的长度作为参数传递给limit。如果流是有序的,则最多会返回前n个元素。
比如,筛选出热量超过300卡路里的头三道菜:

1
2
3
4
List<Dish> highCaloriesTop3 = menu.stream() // 由菜单得到一个流
.filter(dish -> dish.getCalories() > 300) // 接收Lambda,从流中排除某些元素
.limit(3) // 截断流,使元素不超过指定数量
.collect(Collectors.toList()); // 将流转换为其他形式

跳过元素

Stream还支持skip方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回空流。
比如,找出超过300卡路里的菜,但忽略前2道。

1
2
3
4
List<Dish> highCalories = menu.stream()
.filter(dish -> dish.getCalories() > 300)
.skip(2)
.collect(Collectors.toList());

映射

对流中的每个元素应用函数

Stream支持map方法,接收一个函数作为参数,会对流中的每个元素应用该函数,并映射为一个新的元素。
比如,获取每道菜的名称:

1
List<String> dishNames = menu.stream().map(Dish::getName).collect(toList());

流的扁平化

流支持flatMap方法,可以将流中的每个值都转换为一个流,然后把所有的流连接起来形成一个流。

例1:
给定如下单词列表[‘Hello’,’World’],如何得到不重复的字符?

1
2
3
4
5
6
7
List<String> strings = Arrays.asList("Hello", "World");
// Arrays::stream将一个数组转换为一个流
List<String> words = strings.stream()
.map(s -> s.split("")) // 得到字符数组
.flatMap(Arrays::stream) // 将字符数组中的每个元素都换成另外一个流,然后把所有的流连接起来形成一个新的流
.distinct()
.collect(Collectors.toList());

上面如果只是使用map方法,只能得到2个字符数组,而使用flatMap就可以将数组中的每个元素映射为一个流,然后把所有的流合起来形成一个新的流。

例2:
给定2个数字列表,[1,2,3],[4,5],如何返回所有的数对?[(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)]

1
2
3
4
5
List<Integer> numList1 = Arrays.asList(1, 2, 3);
List<Integer> numList2 = Arrays.asList(4, 5);
List<int[]> nums = numList1.stream()
.flatMap(i -> numList2.stream().map(j -> new int[]{i, j}))
.collect(Collectors.toList());

查找和匹配

查找谓词是否至少匹配一个元素

使用anyMatch()方法。
比如,菜单中是否有素菜?

1
boolean isExistVegetarian = menu.stream().anyMatch(d -> d.isVegetarian());

检查谓词是否匹配所有元素

使用allMatch()方法。

比如,是否所有的菜卡路由都小于1000?

1
boolean allLess1000 = menu.stream().allMatch(d -> d.getCalories() < 1000);

检查谓词是否券都不匹配所有元素

使用noneMatch,与allMatch对应。

比如,是否是否所欲的菜卡路由都是小于1000?

1
boolean allLess1000 = menu.stream().noneMatch(d -> d.getCalories() >= 1000);

查找元素

findAny()方法返回当前流中的任意元素,可以和其他流操作结合使用。

比如,找一道素菜(随便哪一道):

1
Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();

注意:findAny()返回的是一个Optional。它是一个容器类,代表一个值存在或不存在。用来解决null值问题。

常用方法如下:

  • isPresent
    包含值的时候返回true,否则返回false;
  • ifPresent(Consumer block)
    在存在值时执行给定的代码块。
  • get
    在存在值时返回值,否则抛出一个NoSuchElement异常
  • orElse(T other)
    在存在值时返回值,否则返回一个默认值。

比如,打印一道素菜的名称:

1
2
3
4
5
6
7
8
9
10
11
12
menu.stream().filter(Dish::isVegetarian).findAny().ifPresent(d -> System.out.println(d.getName()));
```

## 规约
### 元素求和
使用for-each求和
```java
List<Integer> numbers = Arrays.asList(1,2,3,9);
int sum = 0;
for (int x:numbers) {
sum += x;
}

使用reduce()方法

1
int sum = numbers.stream().reduct(0,(a,b) -> a+b);

第1个参数:初始值
第2个参数:一个BinaryOperator ,将2个元素结合起来产生一个新值。

求和过程:
首先,0作为Lambda的第1个参数(a),从流中获取到1作为第2个参数,0+1=1,它成为了新的累积值。
然后,在用累积值和下一个元素调用Lambda,产生新的累积值3。

综合示例
有如下交易和交易员,计算下面8个问题。
交易员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 + '\'' +
'}';
}
}

交易

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Transaction
{
private final Trader trader;
private final int year;
private final int value;

public Transaction(Trader trader, int year,int value) {
this.value = value;
this.year = year;
this.trader = trader;
}

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 +
'}';
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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,2011,700),
new Transaction(alan,2012,950)
);

// 找出2011年发生的所有交易,并按交易额排序(从低到高)
transactions.stream().filter(t -> t.getYear() == 2011).sorted(Comparator.comparing(Transaction::getValue)).forEach(System.out::println);
System.out.println("1.=======================");
// 交易员都在哪些不同的城市工作过
transactions.stream().map(t -> t.getTrader().getCity()).distinct().forEach(System.out::println);
System.out.println("2.=======================");
// 查找所有来自剑桥的交易员,并按姓名排序
transactions.stream().filter(t -> "Cambridge".equals(t.getTrader().getCity())).map(t -> t.getTrader().getName()).distinct().sorted().forEach(System.out::println);
// 或者
transactions.stream().map(t -> t.getTrader()).filter(t -> t.getCity().equals("Cambridge")).distinct().sorted(Comparator.comparing(Trader::getName)).forEach(System.out::println);
System.out.println("3.=======================");
// 返回所有交易员的姓名字符串,按字母顺序排序
transactions.stream().map(t -> t.getTrader().getName()).distinct().sorted().forEach(System.out::println);
System.out.println("4.=======================");
// 有没有交易员是在米兰工作的
transactions.stream().filter(t -> "Milan".equals(t.getTrader().getCity())).findAny().ifPresent(System.out::println);
System.out.println("5.=======================");
// 打印生活在剑桥的交易员的所有交易额
transactions.stream().filter(t -> "Cambridge".equals(t.getTrader().getCity())).map(t -> t.getValue()).reduce(Integer::sum).ifPresent(System.out::println);
//或 mapToInt转化为int流,然后用IntStream的sum求和
transactions.stream().filter(t -> "Cambridge".equals(t.getTrader().getCity())).mapToInt(Transaction::getValue).sum();
System.out.println("6.=======================");
// 所有交易额中,最高的交易额是多少
transactions.stream().map(t -> t.getValue()).reduce(Integer::max).ifPresent(System.out::println);
System.out.println("7.=======================");
// 找到交易额最少的交易
transactions.stream().filter(t -> t.getValue() == transactions.stream().map(a -> a.getValue()).reduce(Integer::min).get()).forEach(System.out::println);
// 或
transactions.stream().reduce((t1,t2) -> t1.getValue() < t2.getValue() ? t1 : t2).ifPresent(System.out::println);
// 或
transactions.stream().min(Comparator.comparing(Transaction::getValue)).ifPresent(System.out::println);

数值流

原始类型流特化

在java 8中,引入了3个原始类型特化流接口:IntStream,DoubleStream和LongStream,分别将流中的元素特化为int,double和long,从而避免暗含的装箱(int->Integer)成本。
每个接口都带有常用数值规约的方法,比如对数值就和的sum,找到最大元素的max。
还有在必要时将它们转换为流对象的方法。

映射到数值流
将流转换为特化版本的常用方法是mapToInt,mapToDouble和mapToLong。这些方法和前面说的流工作方式一样,只是返回的是一个特化流,而不是Stream
例如,使用mapToInt对菜单中所有菜肴的卡路里求和。

1
2
3
int sum = menu.stream()
.mapToInt(Dish::getCalories) // 转换为IntStream<T>
.sum(); // 使用IntStream<T>提供的sum方法求和。

转换到对象流
有了数值流,你还可以将数值流转换回对象流。将原始流转换为对象流,可以使用boxed方法。

1
2
IntStream is = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> s = is.boxed();

OptionalInt
IntStream/DoubleStream/LongStream对max、min等计算返回的是OptionalInt/OptionalDouble/OptionalLong。之前说过Optional的用法,这些OptionalXXX用法是一样的。
比如,求最大值:

1
2
3
// OptionalInt
OptionalInt max = transactions.stream().mapToInt(Transaction::getValue).max();
max.ifPresent(System.out::println);

数值范围

在java 8中,IntStream和LongStream提供了range()和rangeClosed()用于生成一定范围内的数字。range不包括结束值,rangeClosed包括结束值。
比如,求1到100有多少个偶数?

1
IntStream.rangeClosed(1,100).filter(i -> i % 2 == 0).count();

数值流应用:勾股数
勾股数满足:aa+bb=c*c,a,b,c均为正数。比如(3,4,5)即为一组有效的勾股数。
下面使用IntStream来计算100内的勾股数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
IntStream.rangeClosed(1,100)
.boxed()
.flatMap(a -> IntStream.rangeClosed(a,100) // 从a开始,避免出现重复的数据,比如[3,4,5]和[4,3,5]
.filter(b -> Math.sqrt(a*a+b*b) % 1 == 0) // a的平方+b的平方开平方根是整数
.mapToObj(b -> new int[]{a,b,(int)Math.sqrt(a*a+b*b)})) // 返回符合条件的int数组
.forEach(i -> System.out.println(i[0] + "," + i[1] + "," + i[2]));

// 上面的方案导致计算了2次平方根,方案2先生成a*a+b*b开平方后的结果,然后再过滤掉不符合要求的数
System.out.println("========方案2===========");
IntStream.rangeClosed(1,100)
.boxed()
.flatMap(a -> IntStream.rangeClosed(a,100)
.mapToObj(b -> new double[] {a,b,Math.sqrt(a*a+b*b)}))
.filter(a -> a[2] % 1 == 0)
.forEach(a -> System.out.println(a[0] + "," + a[1] + "," + a[2]));

构建流

由值创建流

可以使用静态方法Stream.of,来创建一个流,它可以接收任意数量的参数。
比如,创建一个字符串流。

1
Stream<String> s = Stream.of("Java 8" ,"Lambda","In","Action");

由数组创建流

可以使用静态方法Arrays.stream来从数组创建一个流,它接收一个数组作为参数。
比如:

1
2
int[] numbers = {1,3,4,6,3,9};
int sum = Arrays.stream(numbers).sum(); // 对数组元素求和

由文件创建流

java中用于处理文件等I/O操作的NIO API已更新,以便使用Stream API。java.nio.file.Files中很多静态方法都会返回一个流。
比如一个很有用的方法Files.lines,它返回一个由指定文件中各行构成的字符串流。
比如:计算文件中有多少个不重复的单词。

1
2
3
4
5
6
7
8
9
// 由文件生成流
String path = TraderDemo.class.getClassLoader().getResource("").getPath() + "data.txt";
// 统计文件中不重复的单词数量
try (Stream<String> lines = Files.lines(Paths.get(path.substring(1)), Charset.forName("utf-8"))){
long count = lines.flatMap(line -> Arrays.stream(line.split(" "))).distinct().count();
System.out.println("distinct words count:" + count);
} catch (IOException e) {
e.printStackTrace();
}

由函数生成流:创建无限流

Stream API提供了2个静态方法从函数生成流:iterate和generate,这2个操作可以创建无限流。一般来说,应该使用limit(n)来加以限制,否则会无穷无尽的计算下去。

iterate
比如:

1
2
// 从0开始,生成10个数,每个数是前面的数加2
Stream.iterate(0,n -> n+2).limit(10).forEach(System.out::println); // 0,2,4,6,8...

iterate方法接收一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda。

利用iterate实现斐波那契数列

1
2
3
4
5
// 斐波那契数列(0,1,1,2,3,5,8,13,21,34,55...)除最开始的2个数字0和1外,后续的每个数字是前2个数字之和。
// 生成前20个元素(0,1),(1,1),(1,2),(2,3),(3,5),(5,8)...
Stream.iterate(new int[]{0,1},a -> new int[]{a[1],a[0] + a[1]}).limit(20).forEach(a -> System.out.println("(" + a[0] + "," + a[1] + ")"));
// 只打印数字
Stream.iterate(new int[]{0,1},a->new int[]{a[1],a[0]+a[1]}).limit(20).map(a -> a[0]).forEach(System.out::println);

generate
与iterate类似,可以生成无限流。但它不是依次对每个新生成的值应用Lambda.

例如,创建5个随机的0到1的数

1
Stream.generate(Math::random).limit(5).forEach(System.out::println);

如果用generate来实现斐波那契数列,则要这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用generate上下行斐波那契数列,不推荐,只是演示
IntSupplier fib = new IntSupplier() { // 使用匿名类保存状态(前一项和当前项)
int prev = 0;
int curr = 1;
@Override
public int getAsInt() {
int oldPrev = prev;
int nextVal = prev + curr;
prev = curr;
curr = nextVal;
return oldPrev;
}
};
IntStream.generate(fib).limit(10).forEach(System.out::println);