Java 8 Stream API
Stream API 基于Lambda表达式和函数式接口对集合操作进行了增强,极大的提高了编程效率和程序可读性。
Stream API 分为创建流,流的中间操作,终止操作。
创建流
流操作的起点,我们需要将我们所需要的数据先得到流,一般有以下几种
集合/其他 | 获取流方法 | 备注 |
---|---|---|
离散值:如 1,3,5,7 | Stream.of(…) | 静态方法 |
数组 | Arrays.stream(T[] arr); | 静态方法 |
List | list.stream() | 成员方法 |
Set | set.stream() | 成员方法 |
Map | map.entrySet.stream() | 成员方法 |
1、离散值生成Stream
Stream<Integer> intStream = Stream.of(1, 2, 3, 6); // 生成int流
Stream<String> stringStream = Stream.of("Tom", "Jerry"); // 生成string流
Stream<String> empty = Stream.empty(); // 空流
2、数组生成Stream
用Arrays工具类
String[] strArray = new String[]{"Tom", "Jerry"};
// 将数组转成流
Stream<String> strArrayStream = Arrays.stream(strArray);
// 枚举类转stream
Stream<HttpMethod> stream = Arrays.stream(HttpMethod.values());
// 字符串分割后转stream
String products = "computer,watch,pad";
Stream<String> productStream = Arrays.stream(products.split(","));
3、List生成stream
list自带stream方法
List<String> list = new ArrayList<>();
// 通过stream获取流
Stream<String> listStream = list.stream();
4、Set生成stream
Set 也同样自带 stream方法
Set<String> set = new HashSet<>();
// 通过stream获取流
Stream<String> setStream = set.stream();
5、Map生成Stream
map并没有像list或set提供 stream方法,而是需要先得到 entrySet,然通过set的stream来获取流
Map<String, String> map = new HashMap<>();
// 通过entrySet再获取到流
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
一般来说以上5种基本上满足了开发种所需要的流创建情况。
中间操作
流的中间操作是指对Stream对象进行的一系列转换操作,这些操作返回的仍然是一个Stream对象,可以链式调用多个中间操作。中间操作不会立即执行,只有在终端操作调用时才会触发执行。中间操作中大多数的参数都是函数式接口类型参数,所以可以使用lambda表达式来替代调用
filter(Predicate)
根据指定的条件过滤Stream中的元素,只保留满足条件的元素
Stream<Integer> stream = Stream.of(1, 3, 4, 6, 10);
// filter 过滤偶数
stream.filter(num -> num % 2 == 0);
// 输出流(这是终止操作)
stream1.forEach(System.out::println);
// 最终输出 4 6 10
因为流式中间操作返回的还是一个Stream,所以可以用链式写法(以下均使用链式写法)。等同于如下
Stream.of(1, 3, 4, 6, 10)
.filter(num -> num % 2 == 0)
.forEach(System.out::println);
map(Function)
对Stream中的每个元素应用指定的函数,将其转换为另一种类型
Stream.of(1, 3, 4, 6, 10)
.map(num -> num * 2) // 每个数×2
.forEach(System.out::println);
// 最终输出2 6 8 12 20
distinct()
去除Stream中重复的元素,根据元素的hashCode()和equals()方法判断是否重复
Stream.of(1, 3, 3, 4)
.distinct()
.forEach(System.out::println);
// 结果输出 1 3 4
flatMap(Function)
将多个Stream组合成新的Stream
// 统计两篇文章出现过的单词数(去重)
String article1 = "hello world";
String article2 = "hello stream";
long count = Stream.of(article1, article2) // 这里构建了字符串的流
// flatMap 对流中每个字符串用空格分割成数组,然后把数组再生成流,此时可以看成
// 合并了 Stream.of("hello", "world"), Stream.of("hello", "stream")
// 合并新的流后 等价于 Stream.of("hello","world","hello","stream")
.flatMap(art -> Arrays.stream(art.split(" ")))
.distinct()// 去重
.count(); // 终止操作,统计数量
System.out.println(count);
// 输出3
sorted()
按自然顺序排序
Stream.of(3, 4, 9, 5, 1)
.sorted()
.forEach(System.out::println);
// 输出 1 3 4 5 9
sorted(Comparator)
按指定的比较器(Comparator)进行排序,
Stream.of(3, 4, 9, 5, 1)
.sorted((a, b) -> b - a).
.forEach(System.out::println);
// 输出 9 5 4 3 1
关于Comparator的用法
- Comparator的抽象方法
int compare(T o1, T o2);
所以,对应lambda表达式形式:(a, b) -> { return (int)}
-
Comparator.comparing
这个方法可以直接让我们简单指定以某个字段作为比较对象
// comparing方法源码 public static <T, U extends Comparable<? super U>> Comparator<T> comparing( Function<? super T, ? extends U> keyExtractor) { Objects.requireNonNull(keyExtractor); return (Comparator<T> & Serializable) (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2)); }
例如,我们想对学生的成绩分数排名。
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
static class Student{
private String name;
private Integer score;
}
public static void main(String[] args) {
Student s1 = new Student("Tom", 95);
Student s2 = new Student("Jerry", 90);
// 常规写法
Stream.of(s1, s2)
.sorted((a, b) -> a.getScore() - b.getScore())
.forEach(System.out::println);
// 使用 Comparator.comparing 方法
Stream.of(s1, s2)
.sorted(Comparator.comparing(Student::getScore))
.forEach(System.out::println);
// 若根据姓名排序
Stream.of(s1, s2)
.sorted(Comparator.comparing(Student::getName))
.forEach(System.out::println);
}
-
reversed方法
当然,我们可能更多时候希望成绩是降序排序的,那么还能使用 Comparator.comparing 方法吗?还是得回归到常规写法呢?此时可以使用reversed方法
// 使用 Comparator.comparing 方法, 并用reversed降序排序(reversed 反转)
Stream.of(s1, s2)
.sorted(Comparator.comparing(Student::getScore).reversed())
.forEach(System.out::println);
细心的网友可能发现:Comparator.comparing方法要求的是一个Function接口,为什么 Student的getScore 是一个无参的方法。
那么为什么 Student::getScore 可以作为Function???后面再来解释这个问题
-
thenComparing方法
有时候我们需要多个字段排序。比如按学生成绩排名后,如果一样分数我们希望按name排序。
Stream.of(s1, s2, s3, s4)
.sorted(Comparator.comparing(Student::getScore)
.reversed()
.thenComparing(Student::getName))
.forEach(System.out::println);
输出结果
StudentScoreDemo.Student(name=Jerry, score=98)
StudentScoreDemo.Student(name=Lucy, score=98)
StudentScoreDemo.Student(name=Tom, score=98)
StudentScoreDemo.Student(name=Jack, score=95)
扩展:如果按成绩降序后,依然希望按名称也是降序的情况
Stream.of(s1, s2, s3, s4)
.sorted(Comparator.comparing(Student::getScore)
.reversed()
.thenComparing(Student::getName, Comparator.reverseOrder()))
.forEach(System.out::println);
输出结果
StudentScoreDemo.Student(name=Tom, score=98)
StudentScoreDemo.Student(name=Lucy, score=98)
StudentScoreDemo.Student(name=Jerry, score=98)
StudentScoreDemo.Student(name=Jack, score=95)
这里要注意的是这里使用的是 thenComparing的第二个参数制定了反转顺序,而不是在thenComparing 方法后跟上reversed().因为如果thenComparing执行后再加上reversed 就相当于是上一个例子的结果做了反转。
limit(long)
限制个数,只取前n个
// 输出 1 2
Stream.of(1, 2, 3, 4, 5).limit(2).forEach(System.out::println);
skip(long)
跳过前n个
// 输出 3 4 5
Stream.of(1, 2, 3, 4, 5).skip(2).forEach(System.out::println);
终止操作
终止操作是指最终将Stream中的元素处理完毕,终止操作后不再是一个stream结果。
forEach(Consumer)
针对Stream的每个元素应用指定操作,并将元素消费掉
// 将流中元素依次输出
Stream.of(1, 2, 3, 4, 5).forEach(System.out::println);
// 一般的forEach用法。
Stream.of(1, 2, 3, 4, 5).forEach(num -> {
// do something
})
collect(Collector)
将Stream中的元素进行收集。
我们使用stream操作,最终目的要么处理流,要么得到一个结果,所以收集方法也是最常用的方法之一。
关于Collector & Collectors
Collector是collect方法的参数,而Collectors则是jdk提供的生成各种Collector的工具类,类似于 Collections之于Collection。 以下主要介绍 Collectors工具类。
Collectors
1、toList()
收集为list
List<Student> list = Stream.of(s1, s2).collect(Collectors.toList());
2、toSet()
收集为set
Set<Student> set = Stream.of(s1, s2).collect(Collectors.toSet());
3、toMap() & toConcurrentMap()
收集为map 或 ConcurrentMap
Stream中元素如何拆分成Map中的key和value?这就需要收集器指定了,所以toMap 需要指定参数,Collectors提供了3个重载方法
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}
最终都会执行第三个方法,即除了指定key和value外,还需要指定key重复时的合并方式,以及指定Map容器。
我们现在将学生生成map, key为学生的名称, value为学生对象。
Map<String, Student> studentMap = Stream.of(s1, s2).collect(
Collectors.toMap(
Student::getName, // 指定name为key
Function.identity(), // 指定 student为对象
(v1, v2) -> v1, // 当key重复时 value取前一个。
HashMap::new // 指定使用新建hashMap对象
)
);
一般来说第四个参数比较不需要,当然如果你想要将收集起来的map添加到已有的map中, 可以使用Supplier提供map。
Function.identity() 相当于 表达式:v -> v , 即输入参数原样输出。
// identity() 方法源码
static <T> Function<T, T> identity() {
return t -> t;
}
建议指定key重复时value的处理方式,因为流收集时在碰到key重复时并不会以覆盖原值的方式进行处理,而是会报错。所以,最常用的toMap 方法应该是3个参数的这个方法。
4、groupingBy 分组
分组,可以看成是按某个维度将集合分割成多个集合。
例如:按班级分组,即按班级这个维度将学生这个集合分割成多个集合(每个班级一个集合),同样还可以继续按性别继续分组等等。
所以分组有2个要点
- 以什么为分组依据(Key)
- 分割后依然是集合(Collection),依然可以再次进行集合的收集操作。
所以,分组后可以使用Map来存储分组结果。
即 Map<Key, Collection>。
Collectors.groupingBy的3个重载方法
有了上面的理解,我们从源码入手
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) {
return groupingBy(classifier, HashMap::new, downstream);
}
public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream) {
// 分组逻辑,此处省略
}
3个参数:
- classifier : 指定以什么作为分组 (Key),这里的key将作为Map容器中的key。
- mapFactory: 分组存放容器,一般为 Map, (默认使用了HashMap::new, 见第二个方法)
- downstream:分组之后的元素要如何收集,(默认使用 toList(),即收集为List,见第一个方法),所以这里也可以使用Collectors.toList(), Collectors.toSet(), Collectors.toMap(),甚至可以再进行分组Collectors.groupingBy操作。
依然以学生为例
// 按学生年级分组
List<Student> list = new ArrayList<>(); //list存储所有学生。
Map<Integer, List<Student>> gradeStudentMap = list.stream().collect(Collectors.groupingBy(Student::getGrade)); // 默认收集方式 toList()
// 按年级分组后统计每个年级的学生总人数
Map<Integer, Long> countMap = list.stream().collect(Collectors.groupingBy(Student::getGrade, Collectors.counting()));
5、mapping
映射:将一个对象映射成另一个对象,mapping大多数情况配合groupingBy使用,类似于map操作,只不过它是收集时使用,没有办法再得到流。
我们把上一个栗子的需求改一下:按学生年级分组后,只需要用到学生的name
// 这是groupingBy的例子
Map<Integer, List<Student>> gradeStudentMap = list.stream().collect(Collectors.groupingBy(Student::getGrade));
// 现在只需要学生名字即 结果是 Map<Integer, List<String>>
Map<Integer, List<String>> result = list.stream()
.collect(
Collectors.groupingBy(
Student::getGrade,
// 参数1 表示Student只映射 name字段
// 参数2 表示映射后以list方式收集
Collectors.mapping(Student::getName, Collectors.toList())
));
6、joining
连接操作,将流中的字符串元素以分隔符连接。
3个重载方法
public static Collector<CharSequence, ?, String> joining() {
return new CollectorImpl<CharSequence, StringBuilder, String>(
}
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
}
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix) {
}
String str1 = Stream.of("hello", "Stream", "API").collect(Collectors.joining());
String str2 = Stream.of("hello", "Stream", "API").collect(Collectors.joining(","));
String str3 = Stream.of("hello", "Stream", "API").collect(Collectors.joining(",", "[", "]"));
System.out.println(str1); // helloStreamAPI
System.out.println(str2); // hello,Stream,API
System.out.println(str3); // [hello,Stream,API]
reduce(BinaryOperator)
将Stream中的元素逐个进行二元运算,并返回最终结果。reduce是一种递减操作,可以将n个元素最终缩减到只有一个元素,从这点上来说也可以看成是归纳操作。
reduce可以接受一个初始值开始计算。所以,jdk提供了3个reduce重载方法中的其中两个如下(第三个比较特殊)
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
- 第一个方法接受一个初始值,并提供二元操作方法
- 第二个参数没有初始值,仅提供二元操作方法,注意此次返回值是Optional包装(因为考虑到流中可能没有元素或产生空值),需要执行get()方法才能获取原值
注:BinaryOperator 的函数式接口声明如下:
public interface BinaryOperator<T> extends BiFunction<T,T,T>
栗子1:求和
Integer sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).reduce((a, b) -> {
System.out.println("a:" + a + ",b:" + b);
return a + b;
}).get();
System.out.println("sum:" + sum);
为了方便我们清楚执行过程,加了输出语句
输出结果如下:
a:1,b:2
a:3,b:3
a:6,b:4
a:10,b:5
a:15,b:6
a:21,b:7
a:28,b:8
a:36,b:9
a:45,b:10
最终结果:55
可以看到,从第二行开始,a的值等于上一行 a+b, 即每次二元操作的结果将作为下一次二元操作的第一个输入参数。
栗子2:字符串拼接
String s = Stream.of("hello", "java", "8", "stream", "api").reduce("这是我构建的语句:", (a, b) -> {
System.out.println("a:" + a + ",b:" + b);
return a + " " + b;
});
System.out.println("最终结果:" + s);
控制台输出
a:这是我构建的语句:,b:hello
a:这是我构建的语句: hello,b:java
a:这是我构建的语句: hello java,b:8
a:这是我构建的语句: hello java 8,b:stream
a:这是我构建的语句: hello java 8 stream,b:api
最终结果:这是我构建的语句: hello java 8 stream api
可以看到,当提供了初始值时,二元操作第一步会将初始值和第一个元素先执行一次。同时,结果会直接返回string,而不是Optional。
第三个reduce重载方法
此方法的第三个参数需要在并行流中才会生效。
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
参数说明:
- identity: 初始化最终返回类型的实例。
- accumulator: 声明identity(类型U)上 操作 数据来源的逻辑。并最终返回identity类型。这里与第一个重载方法不同的是,这里对应的是流中的每个元素都是在identity的基础上操作的。
- combiner: 此参数需要在并行流中才会生效,在并行流中,多线程分别执行accumulator操作,combiner可以对线程操作后的结果进行合并。
栗子:没有实际意义,通过这个栗子看下整个过程
List<Integer> initList = new ArrayList();
for (int i = 1; i < 10; i++) {
// 数字1-9
initList.add(i);
}
List<Integer> result = initList.parallelStream()
.reduce(
// 参数1: 声明返回类型是一个List,并初始化,这里使用CopyOnWriteArrayList满足并发操作
new CopyOnWriteArrayList<>(),
// 在参数1(list)的基础上对stream的元素操作逻辑
// 这里简单将元素添加到list,最终返回list
(list, num) -> {
System.out.println("accumulator 操作:list:" + list + ",num:" + num);
list.add(num);
return list;
},
// 第二个参数可以看成是一个线程操作的结果,多线程下每2个线程的操作结果做合并
(list1, list2) -> {
System.out.println("combiner 操作:list1:" + list1 + ",list2:" + list2);
// 这里为什么只返回list1?(当然返回list2也是一样)
// 因为 我们这里第二个参数用list.add,即对list来说元素已经加进去了。这里的list1, list2始终会保持一致
return list1;
});
输出结果
accumulator 操作:list:[],num:6
accumulator 操作:list:[],num:2
accumulator 操作:list:[],num:4
accumulator 操作:list:[],num:9
accumulator 操作:list:[],num:7
accumulator 操作:list:[],num:8
accumulator 操作:list:[],num:5
accumulator 操作:list:[],num:3
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5],list2:[6, 2, 4, 9, 7, 8, 5]
combiner 操作:list1:[6, 2, 4, 9, 7, 8],list2:[6, 2, 4, 9, 7, 8]
accumulator 操作:list:[],num:1
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3],list2:[6, 2, 4, 9, 7, 8, 5, 3]
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3, 1],list2:[6, 2, 4, 9, 7, 8, 5, 3, 1]
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3],list2:[6, 2, 4, 9, 7, 8, 5, 3]
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3, 1],list2:[6, 2, 4, 9, 7, 8, 5, 3, 1]
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3, 1],list2:[6, 2, 4, 9, 7, 8, 5, 3, 1]
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3, 1],list2:[6, 2, 4, 9, 7, 8, 5, 3, 1]
-
accumulator操作时,大部分的list都是空数组,说明并行流在初始操作时,启动了较多的线程。如果我们设定的线程流个数较多(比如从100个数),那么可以看到中间的过程中添加新元素时list已经有元素了。
-
combiner打印出来的list数据,一般list1和list2都一样的元素。
-
为什么combiner后面打印的list比之前打印的少?这一点没有想通。
count()
计算stream中的元素数量,返回long类型的数值
long count = Stream.of(1, 2, 3, 4, 5).count(); // 5
anyMatch(Predicate)
至少一个满足条件。判断Stream中是否存在满足指定条件的元素,如果存在则返回true,否则返回false
allMatch(Predicate)
都满足条件。判断Stream中所有元素是否都满足指定条件,如果都满足则返回true,否则返回false
noneMatch(Predicate)
都不满足条件。判断Stream中是否存在不满足指定条件的元素,如果存在则返回true,否则返回false。
boolean result1 = Stream.of(1, 2, 3, 4, 5).anyMatch(num -> num > 3); // true, 存在大于3
boolean result2 = Stream.of(1, 2, 3, 4, 5).allMatch(num -> num > 3); // false, 都大于3
boolean result3 = Stream.of(1, 2, 3, 4, 5).noneMatch(num -> num > 5); // true, 都不大于5
为什么 Student::getScore 可以作为Function
常规写法:
Function<Student, Integer> function = (student) -> student.getScore()
首先,getScore 方法是实例方法,而Student::getScore是使用类引用实例方法(一般这样的引用方式是静态方法引用),那么我们可以认为当使用 类::实例方法
引用时,是需要额外传入实例对象的,对应的lambda表达式就需要额外的参数(第一个参数)接收。
所以,Student::getScore() 可以看成需要额外 student实例, 再使用student.getScore() 方法。
即(student) -> student.getScore(), 与常规写法等价,所以
Function<Student, Integer> function = Student::getScore;
同理: Student::setScore 等价于Lambda表达式
(student, score) -> student.setScore(score);
对应的函数式接口 BiConsumer<Student, Integer>
// BiConsumer 抽象方法: void accept(T t, U u);
BiConsumer<Student, Integer> biConsumer = Student::setScore;
有兴趣的同学可以尝试更多参数的情况(参考答案写在最后):
// 这样的函数式接口(lambda)所对应的成员方法引用应该怎么实现。
@FunctionalInterface
public interface MyFunctionInterface<T, A, B, C, D, R> {
R function(T t, A a, B b, C c, D d);
}
并行流
并行流将流数据分成多块,并使用多个线程分别处理后再做合并。使用的是Fork/Join框架处理。
- 一般集合创建并行流可以使用 parallelStream()
List<String> list = new ArrayList<>();
list.stream();// 顺序流
list.parallelStream(); // 并行流
- 顺序流转并行流
Stream.of(1, 2, 3).parallel(); // 将顺序流转为并行流
简单看个栗子:求和 (将reduce的栗子改成并行流)
Integer sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).parallel().reduce((a, b) -> {
System.out.println("a:" + a + ",b:" + b);
return a + b;
}).get();
System.out.println("sum:" + sum);
输出结果
a:6,b:7
a:9,b:10
a:1,b:2
a:4,b:5
a:3,b:9
a:3,b:12
a:8,b:19
a:13,b:27
a:15,b:40
sum:55
执行顺序不再是从1开始,而且过程中已经并行流自行做了合并操作。
IDEA如何调试Stream
1、将断点打在流式语句上(行断点)。
2、Debug调试启动,当执行到断点时,点击下图所示图标
3、此时会根据当前流式操作展示流跟踪视图,顶部可以切换流操作查看逻辑
4、点击左下角可以切换视图模式:SPLIT MODE和FLAT MODE. 流调试可以方便我们在流操作过程中定位查找流操作的问题。
前面遗留的问题答案(仅做参考):
static class Test {
// 这里简化了返回值类型,指定为Integer类型
private <A, B, C, D> Integer func(A a, B b, C c, D d) {
return 0;
}
}
public static void main(String[] args) {
// 第一个参数为调用实例对象类型, 最后一个为返回值类型, 中间四个因为func方法声明的是泛型,这里可以任意指定
MyFunctionInterface<Test, String, Integer, Number, Object, Integer> myFunctionInterface = Test::func;
}
写在最后, 欢迎关注公众号" Java后端架构技术进阶 "