前言
Java8中有两大最为重要的改变。第一个是Lambda表达式,还不了解的可以去这看看详解Java8特性之Lambda表达式
另外一个则是Stream API,在java.util.stream
下
是什么
Stream呢,很多地方说到它是处理集合的关键抽象概念,这样子说实在是太抽象了。这个Stream并不是我们以前认识的IO流,而是一个数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。集合专注的是数据,流专注的是计算。
有什么用
它可以让你对集合执行非常复杂的查找、过滤和映射数据等操作。通过使用Stream API对集合数据进行操作,就类似于使用 SQL 执行的数据库查询,也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
说这么多还是直接来个例子体会一下吧
@Test
public void test() {
List<Integer> list = Arrays.asList(2, 3, 5, 4, 1, 8, 10, 9, 7, 6);
// 传统方式
for (Integer num : list) {
if (num > 5)
System.out.println(num);
}
System.out.println("-------------");
// Stream方式
list.stream()//
.filter((e) -> e > 5)//
.forEach(System.out::println);
}
输出结果
8
10
9
7
6
-------------
8
10
9
7
6
在这个例子中,我把1-10个数作为集合的元素,然后分别通过传统迭代方式和Stream方式把大于5的数字打印出来。
先不用知道Stream方式为什么是这么做的,后面会讲到,只是简单先示范一下Stream操作集合。
还有就是在用Stream方式进行操作时我们可以使用链式编程,何谓链式,就是多个操作连着来,不用中间声明变量。像我们最熟悉的StringBuffer
。
StringBuffer buffer = new StringBuffer();
buffer.append("aaa").append("bbb").append("ccc");
Stream的中间操作都是返回一个Stream,所以可以使用链式编程来使用Stream,至于中间操作后面会讲到。
还有就是在eclipse中,可以在一行代码后面加上//
来防止格式化代码时链式编程被格式化成一行,如果不加这个的话,像上面的链式编程可能就会被格式化成下面这样子,不便于阅读。
list.stream().filter((e) -> e > 5).forEach(System.out::println);
怎么用
使用Stream操作有三个步骤
- 创建 Stream:从一个数据源(如集合、数组)中获取一个流
- 中间操作:一个或多个中间操作,对数据源的数据进行处理
- 终止操作:执行中间操作链,并产生结果
创建 Stream
由集合创建 Stream
Java8 中的 Collection 接口被扩展了,提供了两个获取流的方法:
- default Stream stream() : 返回一个顺序流
- default Stream parallelStream() : 返回一个并行流
这两个方法都是接口默认方法,不了解的可以去这看看详解Java8特性之接口默认方法
可以像下面这样子直接调用stream()
方法获取流
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream(); //获取一个顺序流
Stream<String> parallelStream = list.parallelStream(); //获取一个并行流
由数组创建流
Java8 中的 Arrays
的静态方法 stream()
可以从数组获取流:
- static Stream stream(T[] array): 返回一个流
还有重载形式,能够处理对应基本类型的数组:
- public static IntStream stream(int[] array)
- public static LongStream stream(long[] array)
- public static DoubleStream stream(double[] array)
例子:
Integer[] nums = new Integer[8];
Stream<Integer> stream = Arrays.stream(nums);
由值创建流
可以使用静态方法 Stream.of(), 通过显示值创建一个流。它可以接收任意数量的参数。
- public static Stream of(T… values) : 返回一个流
例子:
Stream<Integer> stream = Stream.of(1,2,3,4,5);
由函数创建流
可以使用静态方法 Stream.iterate() 和Stream.generate(), 创建无限流。
- public static Stream iterate(final T seed, final UnaryOperator f)
- public static Stream generate(Supplier s) :
所谓无限流,顾名思义就是无限的.
以iterate()
方法举例,第一个参数seed
其实就是一个起始值,而第二个参数是UnaryOperator
类型实例,而这个UnaryOperator
是一个函数式接口,继承了函数式接口Function
,所以我们可以直接把他当做Function
。关于函数式接口,可以去这里看看详解Java8特性之Lambda表达式
下面是iterate()
方法的具体使用例子
@Test
public void test2() {
Stream.iterate(1, (x) -> x + 1).forEach(System.out::println); }
这段代码的意思是这样的,第一个参数我指定的是1
,意思是1
作为起始值,第二参数是Lambda表达式创建一个UnaryOperator
其实也可以说是Function
实例,将它的唯一抽象方法实现为接受一个参数返回该参数+1的值。至于forEach()
则是终止操作,在后面会讲到。
运行这段代码,你会发现,控制台一直在输出数字,不会停(这就是无限的体现),而且数字总是等差递增,差值为1。
再来看看generate()
方法的例子,该方法接受一个Supplier
实例的参数
@Test
public void test2() {
Stream.generate(() -> Math.random()).forEach(System.out::println); }
在generate()
方法参数中我用Lambda表达式创建了一个Supplier
实例,该实例的方法实现功能为返回一个随机数。
运行这段代码,你会发现,控制台一直在输出大于等于0小于1的随机数。
中间操作
在创建出流以后,我们就可以进行中间操作了。多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值” ,这个在后面我会演示一下。
中间操作又分四类
- 筛选
- 切割
- 映射
- 排序
筛选
筛选操作有两个方法
filter(Predicate p), 接收Predicate
实例,根据该实例的test
方法进行筛选,如
@Test
public void test() {
List<Integer> list = Arrays.asList(2, 3, 5, 4, 1, 8, 10, 9, 7, 6);
list.stream()//
.filter((e) -> e > 5)//
.forEach(System.out::println);
}
输出结果
8
10
9
7
6
用Lambda表达式创建Predicate
实例,其中(e) -> e > 5
便是test
方法的实现,具体为接收一个Integer
类型参数e,该参数如果大于5则test方法返回true,否则false。
distinct(),这个方法大家应该能看出来就是去重复的,根据流所生成元素的 hashCode() 和 equals() 去除重复元素。如
@Test
public void test() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 4, 4, 5, 6);
list.stream()//
.distinct()//
.forEach(System.out::println);
}
输出结果
1
2
3
4
5
6
切割
切割也有两个方法
limit(long maxSize),截断流,使其元素不超过给定数量,跟SQL语句很像。如
@Test
public void test() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
list.stream()//
.limit(3)//
.forEach(System.out::println);
}
输出结果
1
2
3
skip(long n) ,返回一个去掉了前 n 个元素的流。若流中元素
不足 n 个,则返回一个空流。如
@Test
public void test() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
list.stream()//
.skip(3)//
.forEach(System.out::println);
}
输出结果
4
5
6
可以看到这个方法刚好跟limit方法互补了,limit去尾,skip去头。
映射
map(Function f) ,接收一个Function
实例,Function
的抽象方法apply
是接收一个参数然后返回一个值,这个返回值就是映射的结果。如
@Test
public void test() {
List<Integer> list = Arrays.asList(1, 2, 3);
list.stream()//
.map(x -> x*x)
.forEach(System.out::println);
}
输出结果
1
4
9
可以发现流中的每个元素映射出来的新值是该元素的平方。
mapToDouble(ToDoubleFunction f),这个函数跟map
很像,只不过映射的结果一定是要Double
类型。如
@Test
public void test() {
List<Integer> list = Arrays.asList(1, 2, 3);
list.stream()//
.mapToDouble(x -> x+0.1)
.forEach(System.out::println);
}
输出结果
1.1
2.1
3.1
可以发现Integer
类型的元素被映射成Double
类型了
类似的还有mapToInt(ToIntFunction f)和mapToLong(ToLongFunction f)方法,这里就不去演示了。
flatMap(Function f),将流中的每个元素映射成一个流。如
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.junit.Test;
public class TestStreamAPI {
@Test
public void test() {
List<String> list = Arrays.asList("abc", "efg", "xyz");
list.stream()//
.flatMap(TestStreamAPI::string2Stream)//
.forEach(System.out::println);
}
/** * 接收一个字符串将其所以的字符添加到list中然后返回stream * @param str * @return */
public static Stream<Character> string2Stream(String str) {
List<Character> list = new ArrayList<>();
char[] charArray = str.toCharArray();
for (char c : charArray) {
list.add(c);
}
return list.stream();
}
}
输出结果
a
b
c
e
f
g
x
y
z
其实这也不难理解,就像上面mapToDouble
方法把流中的每个元素映射成一个Double
类型的值一样,现在只是映射成一个Stream。
排序
sorted(),可以将流中元素按自然顺序排序。如
@Test
public void test() {
List<String> list = Arrays.asList("d", "a", "c");
list.stream()//
.sorted()//
.forEach(System.out::println);
}
输出结果
a
c
d
sorted(Comparator comp),可以将流中元素按比较器顺序排序。如
@Test
public void test() {
List<String> list = Arrays.asList("d", "a", "c");
list.stream()//
.sorted((x,y) -> -x.compareTo(y))//
.forEach(System.out::println);
}
输出结果
d
c
a
终止操作
终止操作会从流的中间操作流水线生成结果。其结果可以是任何不是流的值,例如: List
、 Integer
,甚至是 void
。
其中终止操作又有三类
- 查找与匹配
- 归约
- 收集
查找与匹配
查找与匹配有下面这么多方法,我只演示几个典型的。
- allMatch(Predicate p) 检查是否匹配所有元素
- anyMatch(Predicate p) 检查是否至少匹配一个元素
- noneMatch(Predicate p) 检查是否没有匹配所有元素
- findFirst() 返回第一个元素
- findAny() 返回当前流中的任意元素
- count() 返回流中元素总数
- max(Comparator c) 返回流中最大值
- min(Comparator c) 返回流中最小值
- forEach(Consumer c) 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反, Stream API 使用内部迭代)
allMatch(Predicate p)例子
@Test
public void test() {
List<Integer> list = Arrays.asList(10, 5, 7, 3);
boolean allMatch = list.stream()//
.allMatch(x -> x > 2);//是否全部元素都大于2
System.out.println(allMatch);
}
输出结果
true
findFirst()例子
@Test
public void test() {
List<Integer> list = Arrays.asList(10, 5, 7, 3);
Optional<Integer> first = list.stream()//
.findFirst();
Integer val = first.get();
System.out.println(val);//输出10
}
其中Optional
是一个容器类,具体可要看看这里详解Java8特性之Optional类
max(Comparator c)例子
@Test
public void test() {
List<Integer> list = Arrays.asList(10, 5, 7, 3);
Optional<Integer> first = list.stream()//
.min(Integer::compareTo);
Integer val = first.get();
System.out.println(val);//输出3
}
forEach(Consumer c)例子上面用过很多了,这里就不再演示了。
归约
归约操作有下面两个方法
- reduce(T iden, BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 T
- reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 Optional
reduce(T iden, BinaryOperator b)例子
@Test
public void test() {
List<Integer> list = Arrays.asList(10, 5, 7, 3);
Integer result = list.stream()//
.reduce(2, Integer::sum);
System.out.println(result);//输出27,其实相当于2+10+5+7+3,就是一个累加
}
reduce(BinaryOperator b)例子
@Test
public void test() {
List<Integer> list = Arrays.asList(10, 5, 7, 3);
Optional<Integer> optional = list.stream()//
.reduce(Integer::sum);
Integer result = optional.get();
System.out.println(result);//输出25,其实相当于10+5+7+3,就是一个累加
}
收集
collect(Collector c) 将流转换为其他形式。接收一个Collector
接口的
实现,用于给流中元素做汇总的方法。
下面是具体例子
@Test
public void test() {
List<Integer> list = Arrays.asList(10, 5, 7, 3);
// 将流中元素收集到List中
List<Integer> resultList = list.stream()//
.collect(Collectors.toList());
// 将流中元素收集到Set中
Set<Integer> resultSet = list.stream()//
.collect(Collectors.toSet());
System.out.println(resultList);// 输出[10, 5, 7, 3]
System.out.println(resultSet);// 输出[3, 5, 7, 10]
}
上面的代码分别将流中的元素收集到List
和Set
中,其实还可以收集到其类型中,如Map
、Optional
甚至是Integer
等。在收集过程中还可以做点其它操作,比如下面例子,至于其它更多的操作大家还是去看API吧
@Test
public void test() {
List<Integer> list = Arrays.asList(10, 5, 7, 3);
//计算出流中的元素个数
Long count = list.stream()//
.collect(Collectors.counting());
System.out.println(count);//输出4
//计算流中Integer类型数据的总和
Integer sum = list.stream()//
.collect(Collectors.summingInt(x -> x));
System.out.println(sum);//输出25
}
Stream延迟执行
上面“中间操作”那节我说到了“惰性求值”这个东西,即Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
来个例子就知道了。
先准备一个实体类Person
,注意了,在getAge()
方法中我输出了一句话
public class Person {
private String name;
private int age;
public Person(String name,int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
System.out.println("getAge()执行");
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
然后是测试方法
@Test
public void test() {
List<Person> list = Arrays.asList(//
new Person("Jason", 18), //
new Person("Hank", 46), //
new Person("Alice", 23));
// 过滤操作,找出age大于20的Person对象,但是没有终止操作
list.stream()//
.filter(x -> x.getAge() > 20);
}
运行这个测试方法,你会发现没有输出任何东西,因为它并没有终止操作,所以中间操作并未执行。下面加多一个终止操作
@Test
public void test() {
List<Person> list = Arrays.asList(//
new Person("Jason", 18), //
new Person("Hank", 46), //
new Person("Alice", 23));
// 过滤操作,找出age大于20的Person对象,但是没有终止操作
list.stream()//
.filter(x -> x.getAge() > 20)//
.forEach(System.out::println);
}
输出结果
getAge()执行
getAge()执行
Person [name=Hank, age=46]
getAge()执行
Person [name=Alice, age=23]
可以发现getAge()
方法执行了,而且查找出两个age
大于20的Person
对象并输出