当前位置 博文首页 > 口袋里的猫:JDK8中Stream使用解析
现在谈及JDK8的新特新,已经说不上新了。本篇介绍的就是Stream
和Lambda
,说的Stream
可不是JDK中的IO流
,这里的Stream
指的是处理集合的抽象概念『像流一样处理集合数据』。
了解Stream
前先认识一下Lambda
。
先看一组简单的对比
传统方式使用一个匿名内部类的写法
new Thread(new Runnable() {
@Override
public void run() {
// ...
}
}).start();
换成Lambda
的写法
new Thread(() -> {
// ...
}).start();
其实上面的写法就是简写了函数式接口
的匿名实现类
配合Lambda
,JDK8引入了一个新的定义叫做:函数式接口(Functional interfaces)
从概念上讲,有且仅有一个需要实现方法的接口称之为函数式接口。
看一个JDK给的一个函数式接口的源码
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
可以看到接口上面有一个@FunctionalInterface
注释,功能大致和@Override
类似
不写@Override
也能重写父类方法,该方法确实没有覆盖或实现了在超类型中声明的方法时编译器就会报错,主要是为了编译器可以验证识别代码编写的正确性。
同样@FunctionalInterface
也是这样,写到一个不是函数式接口的接口上面就会报错,即使不写@FunctionalInterface
注释,编译器也会将满足函数式接口定义的任何接口视为函数式接口。
写一个函数式接口加不加@FunctionalInterface
注释,下面的接口都是函数式接口
interface MyFunc {
String show(Integer i);
}
Lambda
表达式就是为了简写函数式接口
构成
看一下Lambda
的构成
->
整体表现为 (...参数) -> {代码块}
简写
下面就是函数式接口的实现简写为Lambda
的例子
interface MyFunc1 {
void func();
}
// 空实现
MyFunc1 f11 = () -> { };
// 只有一行语句
MyFunc1 f12 = () -> {
System.out.println(1);
System.out.println(2);
};
// 只有一行语句
MyFunc1 f13 = () -> {
System.out.println(1);
};
// 只有一行语句可以省略 { }
MyFunc1 f14 = () -> System.out.println(1);
interface MyFunc2 {
void func(String str);
}
// 函数体空实现
MyFunc2 f21 = (str) -> { };
// 单个参数可以省略 () 多个不可以省略
MyFunc2 f22 = str -> System.out.println(str.length());
interface MyFunc3 {
int func();
}
// 返回值
MyFunc3 f31 = () -> {return 1;};
// 如果只有一个return 语句时可以直接写return 后面的表达式语句
MyFunc3 f32 = () -> 1;
interface MyFunc4 {
int func(String str);
}
// 这里单个参数简写了{}
MyFunc4 f41 = str -> {
return str.length();
};
// 这里又简写了return
MyFunc4 f42 = str -> str.length();
// 这里直接使用了方法引用进行了简写 - 在文章后续章节有介绍到
MyFunc4 f43 = String::length;
这里可以总结出来简写规则
上面写的Lambda
表达式中参数都没有写参数类型(可以写参数类型的),so
看到这里应该认识到了如何用Lambda
简写函数式接口
,那现在就进一步的认识一下JDK中Stream
中对函数式接口的几种大类
上节说明了Lambda
表达式就是为了简写函数式接口,为使用方便,JDK8提供了一些常用的函数式接口。最具代表性的为Supplier、Function、Consumer、Perdicate
,这些函数式接口都在java.util.function
包下。
这些函数式接口都是泛型类型的,下面的源码都去除了default方法,只保留真正需要实现的方法。
这是一个转换的接口。接口有参数、有返回值,传入T类型的数据,经过处理后,返回R类型的数据。『T和R都是泛型类型』可以简单的理解为这是一个加工工厂。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
使用实例:定义一个转换函数『将字符串转为数字,再平方』
// 将字符串转为数字,再平方
Function<String, Integer> strConvertToIntAndSquareFun = (str) -> {
Integer value = Integer.valueOf(str);
return value * value;
};
Integer result = strConvertToIntAndSquareFun.apply("4");
System.out.println(result); // 16
这是一个对外供给的接口。此接口无需参数,即可返回结果
@FunctionalInterface
public interface Supplier<T> {
T get();
}
使用实例:定义一个函数返回“Tom”
字符串
// 供给接口,调用一次返回一个 ”tom“ 字符串
Supplier<String> tomFun = () -> "tom";
String tom = tomFun.get();
System.out.println(tom); // tom
这是一个消费的接口。此接口有参数,但是没有返回值
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
使用实例:定义一个函数传入数字,打印一行相应数量的A
// 重复打印
Consumer<Integer> printA = (n)->{
for (int i = 0; i < n; i++) {
System.out.print("A");
}
System.out.println();
};
printA.accept(5); // AAAAA
这是一个断言的接口。此接口对输入的参数进行一系列的判断,返回一个Boolean值。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
使用实例:定义一个函数传入一个字符串,判断是否为A
字母开头且Z
字母结尾
// 判断是否为`A`字母开头且`Z`字母结尾
Predicate<String> strAStartAndZEnd = (str) -> {
return str.startsWith("A") && str.endsWith("Z");
};
System.out.println(strAStartAndZEnd.test("AaaaZ")); // true
System.out.println(strAStartAndZEnd.test("Aaaaa")); // false
System.out.println(strAStartAndZEnd.test("aaaaZ")); // false
System.out.println(strAStartAndZEnd.test("aaaaa")); // false
除Supplier
接口外Function、Consumer、Perdicate
还有其他一堆默认方法可以用,比如Predicate接口包含了多种默认方法,用于处理复杂的判断逻辑(and, or);
上面的使用方式都是正常简单的使用函数式接口
,当函数式接口
遇见了方法引用
才真正发挥他的作用。
方法引用
的唯一存在的意义就是为了简写Lambda
表达式。
方法引用通过方法的名字来指向一个方法,可以使语言的构造更紧凑简洁,减少冗余代码。
比如上面章节使用的
MyFunc4 f43 = String::length; // 这个地方就用到了方法引用
方法引用使用一对冒号 ::
相当于将String
类的实例方法length
赋给MyFunc4
接口
public int length() {
return value.length;
}
interface MyFunc4 {
int func(String str);
}
这里可能有点问题:方法 int length()
的返回值和int func(String str)
相同,但是方法参数不同为什么也能正常赋值给MyFunc4
。
可以理解为Java实例方法有一个隐藏的参数第一个参数this(类型为当前类)
public class Student {
public void show() {
// ...
}
public void print(int a) {
// ...
}
}
实例方法show()
和print(int a)
相当于
public void show(String this);
public void print(String this, int a);
这样解释的通为什么MyFunc4 f43 = String::length;
可以正常赋值。
String::length;
public int length() {
return value.length;
}
// 相当于
public int length(String str) {
return str.length();
}
// 这样看length就和函数式接口MyFunc4的传参和返回值就相同了
不只这一种方法引用详细分类如下
方法引用分类
类型 | 引用写法 | Lambda表达式 |
---|---|---|
静态方法引用 | ClassName::staticMethod | (args) -> ClassName.staticMethod(args) |
对象方法引用 | ClassName::instanceMethod | (instance, args) -> instance.instanceMethod(args) |
实例方法引用 | instance::instanceMethod | (args) -> instance.instanceMethod(args) |
构建方法引用 | ClassName::new | (args) -> new ClassName(args) |
上面的方法就属于对象方法引用
记住这个表格,不用刻意去记,使用Stream
时会经常遇到
有几种比较特殊的方法引用,一般来说原生类型如int
不能做泛型类型,但是int[]
可以
IntFunction<int[]> arrFun = int[]::new;
int[] arr = arrFun.apply(10); // 生成一个长度为10的数组
这节结束算是把函数式接口,Lambda表达式,方法引用等概念串起来了。
Optional
工具是一个容器对象,最主要的用途就是为了规避 NPE(空指针) 异常。构造方法是私有的,不能通过new来创建容器。是一个不可变对象,具体原理没什么可以介绍的,容器源码整个类没500行,本章节主要介绍使用。
private Optional(T value) {
// 传 null 会报空指针异常
this.value = Objects.requireNonNull(value);
}
Optional
的方法empyt
返回一个包含null值的Optional
容器
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
of
返回一个不包含null值的Optional
容器,传null值报空指针异常
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
ofNullable
返回一个可能包含null值的Optional
容器
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
Optional
的方法ifPresent
方法,参数是一个Consumer
,当容器内的值不为null是执行Consumer
Optional<Integer> opt = Optional.of(123);
opt.ifPresent((x) -> {
System.out.println(opt);
});
// out: 123
get
方法,获取容器值,可能返回空
orElse
方法,当容器中值为null时,返回orElse
方法的入参值
public T orElse(T other) {
return value != null ? value : other;
}
orElseGet
方法,当容器中值为null时,执行入参Supplier
并返回值
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
// 当param为null时 返回空集合
Optional.ofNullable(param).orElse(Collections.emptyList());
Optional.ofNullable(param).orElseGet(() -> Collections.emptyList());
orElse
和orElseGet
的区别,orElseGet
算是一个惰性求值的写法,当容器内的值不为null时Supplier
不会执行。
平常工作开发中,也是经常通过 orElse
来规避 NPE 异常。
这方面不是很困难难主要是后续Stream
有些方法需要会返回一个Optional
一个容器对象。
Stream
可以看作是一个高级版的迭代器。增强了Collection
的,极大的简化了对集合的处理。
想要使用Stream
首先需要创建一个
// 方式1,数组转Stream
Arrays.stream(arr);
// 方式2,数组转Stream,看源码of就是方法1的包装
Stream.of(arr);
// 方式3,调用Collection接口的stream()方法
List<String> list = new ArrayList<>();
list.stream();
有了Stream
自然就少不了操作流
大致可以把对Stream
的操作大致分为两种类型中间操作
和终端操作
中间操作
是一个属于惰式的操作,也就是不会立即执行,每一次调用中间操作
只会生成一个标记了新的Stream
终端操作
会触发实际计算,当终端操作执行时会把之前所有中间操作
以管道的形式顺序执行,Stream
是一次性的计算完会失效操作Stream
会大量的使用Lambda
表达式,也可以说它就是为函数式编程而生
先提前认识一个终端操作forEach
对流中每个元素执行一个操作,实现一个打印的效果
// 打印流中的每一个元素
Stream.of("jerry", "lisa", "moli", "tom", "Demi").forEach(str -> {
System.out.println(str);
});
forEach
的参数是一个Consumer
可以用方法引用优化(静态方法引用),优化后的结果为
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
.forEach(System.out::println);
有这一个终端操作
就可以向下介绍大量的中间操作了
fileter
方法参数是一个Predicate
接口,表达式传入的参数是元素,返回true保留元素,false过滤掉元素
过滤长度小于3的字符串,仅保留长度大于4的字符串
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
// 过滤
.filter(str -> str.length() > 3)
.forEach(System.out::println);
/*
输出:
jerry
lisa
moli
Demi
*/
限制集合长度不能超过指定大小
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
.limit(2)
.forEach(System.out::println);
/*
输出:
jerry
lisa
*/
// 丢弃前2个元素
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
.skip(2)
.forEach(System.out::println);
/*
输出:
moli
tom
Demi
*/
map传入的函数会被应用到每个元素上将其映射成一个新的元素
// 为每一个元素加上 一个前缀 "name: "
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
.map(str -> "name: " + str)
.forEach(System.out::println);
/*
输出:
name: jerry
name: lisa
name: moli
name: tom
name: Demi
*/
peek
方法的存在主要是为了支持调试,方便查看元素流经管道中的某个点时的情况
下面是一个JDK源码中给出的例子
Stream.of("one", "two", "three", "four")
// 第1次查看
.peek(e -> System.out.println("第1次 value: " + e))
// 过滤掉长度小于3的字符串
.filter(e -> e.length() > 3)
// 第2次查看
.peek(e -> System.out.println("第2次 value: " + e))
// 将流中剩下的字符串转为大写
.map(String::toUpperCase)
// 第3次查看
.peek(e -> System.out.println("第3次 value: " + e))
// 收集为List
.collect(Collectors.toList());
/*
输出:
第1次 value: one
第1次 value: two
第1次 value: three
第2次 value: three
第3次 value: THREE
第1次 value: four
第2次 value: four
第3次 value: FOUR
*/
map
和peek
有点相似,不同的是peek
接收一个Consumer
,而map
接收一个Function
当然了你非要采用peek
修改数据也没人能限制的了
public class User {
public String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
Stream.of(new User("tom"), new User("jerry"))
.peek(e -> {
e.name = "US:" + e.name;
})
.forEach(System.out::println);
/*
输出:
User{name='US:tom'}
User{name='US:jerry'}
*/
// 排序数据
Stream.of(4, 2, 1, 3)
// 默认是升序
.sorted()
.forEach(System.out::println);
/*
输出:
1
2
3
4
*/
逆序排序
// 排序数据
Stream.of(4, 2, 1, 3)
// 逆序
.sorted(Comparator.reverseOrder())
.forEach(System.out::println
/*
输出:
4
3
2
1
*/
如果是对象如何排序,自定义Comparator
,切记不要违反自反性,对称性,传递性
原则
public class User {
public String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
// 名称长的排前面
Stream.of(new User("tom"), new User("jerry"))
.sorted((e1, e2) -> {
return e2.name.length() - e1.name.length();
})
.forEach(System.out::println);
/*
输出:
User{name='US:jerry'}
User{name='US:tom'}
*/
注意:必须重写对应泛型的hashCode()和equals()方法
Stream.of(2, 2, 4, 4, 3, 3, 100)
.distinct()
.forEach(System.out::println);
/*
输出:
2
4
3
100
*/
返回一个流,该流由通过将提供的映射函数(flatMap传入的参数)应用于每个元素而生成的映射流的内容替换此流的每个元素,通俗易懂就是将原来的Stream
中的所有元素都展开组成一个新的Stream
List<Integer[]> arrList = new ArrayList<>();
arrList.add(arr1);
arrList.add(arr2);
// 未使用
arrList.stream()
.forEach(e -> {
System.out.println(Arrays.toString(e));
});
/*
输出:
[1, 2]
[3, 4]
*/
// 平铺后
arrList.stream()
.flatMap(arr -> Stream.of(arr))
.forEach(e -> {
System.out.println(e);
});
/*
输出:
1
2
3
4
*/
// 最大值
Optional<Integer> maxOpt = Stream.of(2, 4, 3, 100)
.max(Comparator.comparing(e -> e));
System.out.println(maxOpt.get()); // 100
// 最小值
Optional<Integer> minOpt = Stream.of(2, 4, 3, 100)
.min(Comparator.comparing(Function.identity()));
System.out.println(minOpt.get()); // 2
// 数量
long count = Stream.of("one", "two", "three", "four")
.count();
System.out.println(count); // 4
上面例子中有一个点需要注意一下Function.identity()
相当于 e -> e
看源码就可以看出来
static <T> Function<T, T> identity() {
return t -> t;
}
Optional<String> anyOpt = Stream.of("one", "two", "three", "four")
.findAny();
System.out.println(anyOpt.orElse(""));
/*
输出:
one
*/
Optional<String> firstOpt = Stream.of("one", "two", "three", "four")
.findFirst();
System.out.println(firstOpt.orElse(""));
/*
输出:
one
*/
返回的Optional
容器在上面介绍过了,一般配置orElse
使用,原因就在于findAny
和findFirst
可能返回空空容器,调用get
可能会抛空指针异常
// 是否全部为 one 字符串
boolean allIsOne = Stream.of("one", "two", "three", "four")
.allMatch(str -> Objects.equals("one", str));
System.out.println(allIsOne); // false
allIsOne = Stream.of("one", "one", "one", "one")
.allMatch(str -> Objects.equals("one", str));
System.out.println(allIsOne); // true
// 是否包含 one 字符串
boolean hasOne = Stream.of("one", "two", "three", "four")
.anyMatch(str -> Objects.equals("one", str));
System.out.println(hasOne); // true
hasOne = Stream.of("two", "three", "four")
.anyMatch(str -> Objects.equals("one", str));
System.out.println(hasOne); // false
上面仅仅介绍了一个forEach
终端操作,但是业务开发中更多的是对处理的数据进行收集起来,如下面的一个例子将元素收集为一个List集合
collect
高级使用方法很复杂,常用的用法使用Collectors
工具类
List<String> list = Stream.of("one", "two", "three", "four")
.collect(Collectors.toList());
System.out.println(list);
/*
输出:
[one, two, three, four]
*/
Set<String> set = Stream.of("one", "one", "two", "three", "four")
.collect(Collectors.toSet());
System.out.println(set);
/*
输出:
[four, one, two, three]
*/
String str1 = Stream.of("one", "two", "three", "four")
.collect(Collectors.joining());
System.out.println(str1); // onetwothreefour
String str2 = Stream.of("one", "two", "three", "four")
.collect(Collectors.joining(", "));
System.out.println(str2); // one, two, three, four
// 使用Lombok插件
@Data
@AllArgsConstructor
public class User {
public Integer id;
public String name;
}
Map<Integer, User> map = Stream.of(new User(1, "tom"), new User(2, "jerry"))
.collect(Collectors.toMap(User::getId, Function.identity(), (k1, k2) -> k1));
System.out.println(map);
/*
输出:
{
1=User(id=1, name=tom),
2=User(id=2, name=jerry)
}
*/
toMap
常用的方法签名
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);
}
/*
keyMapper:Key 的映射函数
valueMapper:Value 的映射函数
mergeFunction:当 Key 冲突时,调用的合并方法
*/
@Data
@AllArgsConstructor
class User {
public Integer id;
public String name;
}
Map<String, List<User>> map = Stream.of(
new User(1, "tom"), new User(2, "jerry"),
new User(3, "moli"), new User(4, "lisa")
).collect(Collectors.groupingBy(u -> {
if (u.id % 2 == 0) {
return "奇";
}
return "偶";
}));
System.out.println(map);
/*
输出:
{
偶=[User(id=1, name=tom), User(id=3, name=moli)],
奇=[User(id=2, name=jerry), User(id=4, name=lisa)]
}
*/
分组后value 是一个集合,groupingBy
分组还有一个参数可以指定下级收集器,后续例子中有使用到
下面例子用到的基础数据,如有例子特例会在例子中单独补充
List<Student> studentList = new ArrayList<>();
studentList.add(new Student(1, "tom", 19, "男", "软工"));
studentList.add(new Student(2, "lisa", 15, "女", "软工"));
studentList.add(new Student(3, "Ada", 16, "女", "软工"));
studentList.add(new Student(4, "Dora", 14, "女", "计科"));
studentList.add(new Student(5, "Bob", 20, "男", "软工"));
studentList.add(new Student(6, "Farrah", 15, "女", "计科"));
studentList.add(new Student(7, "Helen", 13, "女", "软工"));
studentList.add(new Student(8, "jerry", 12, "男", "计科"));
studentList.add(new Student(9, "Adam", 20, "男", "计科"));
/**
* 分页方法
*
* @param list 要分页的数据
* @param pageNo 当前页
* @param pageSize 页大小
*/
public static <T> List<T> page(Collection<T> list, long pageNo, long pageSize) {
if (Objects.isNull(list) || list.isEmpty()) {
return Collections.emptyList();
}
return list.stream()
.skip((pageNo - 1) * pageSize)
.limit(pageSize)
.collect(Collectors.toList());
}
List<Student> pageData = page(studentList, 1, 3);
System.out.println(pageData);
/*
输出:
[
Student(id=1, name=tom, age=19, sex=男, className=软工),
Student(id=2, name=lisa, age=15, sex=女, className=软工),
Student(id=3, name=Ada, age=16, sex=女, className=软工)
]
*/
List<Integer> idList = studentList.stream()
.filter(e -> Objects.equals(e.getClassName(), "软工"))
.map(Student::getId)
.collect(Collectors.toList());
System.out.println(idList);
/*
输出:
[1, 2, 3, 5, 7]
*/
Map<String, List<String>> map = studentList.stream()
.collect(Collectors.groupingBy(
Student::getClassName,
Collectors.mapping(Student::getName, Collectors.toList())
));
System.out.println(map);
/*
输出:
{
计科=[Dora, Farrah, jerry, Adam],
软工=[tom, lisa, Ada, Bob, Helen]
}
*/
Map<String, Long> map = studentList.stream()
.collect(Collectors.groupingBy(
Student::getClassName,
Collectors.mapping(Function.identity(), Collectors.counting())
));
System.out.println(map);
/*
输出:
{
计科=4,
软工=5
}
*/
List<String> allFemaleNameList = studentList.stream()
.filter(stu -> Objects.equals("女", stu.getSex()))
.map(Student::getName)
.collect(Collectors.toList());
System.out.println(allFemaleNameList);
/*
输出:
[lisa, Ada, Dora, Farrah, Helen]
*/
// 年龄升序排序
List<Student> stuList1 = studentList.stream()
// 升序
.sorted(Comparator.comparingInt(Student::getAge))
.collect(Collectors.toList());
System.out.println(stuList1);
/*
输出:
[
Student(id=8, name=jerry, age=12, sex=男, className=计科),
Student(id=7, name=Helen, age=13, sex=女, className=软工),
Student(id=4, name=Dora, age=14, sex=女, className=计科),
Student(id=2, name=lisa, age=15, sex=女, className=软工),
Student(id=6, name=Farrah, age=15, sex=女, className=计科),
Student(id=3, name=Ada, age=16, sex=女, className=软工),
Student(id=1, name=tom, age=19, sex=男, className=软工),
Student(id=5, name=Bob, age=20, sex=男, className=软工),
Student(id=9, name=Adam, age=20, sex=男, className=计科)
]
*/
// 年龄降序排序
List<Student> stuList2 = studentList.stream()
// 降序
.sorted(Comparator.comparingInt(Student::getAge).reversed())
.collect(Collectors.toList());
System.out.println(stuList2);
/*
输出:
[
Student(id=5, name=Bob, age=20, sex=男, className=软工),
Student(id=9, name=Adam, age=20, sex=男, className=计科),
Student(id=1, name=tom, age=19, sex=男, className=软工),
Student(id=3, name=Ada, age=16, sex=女, className=软工),
Student(id=2, name=lisa, age=15, sex=女, className=软工),
Student(id=6, name=Farrah, age=15, sex=女, className=计科),
Student(id=4, name=Dora, age=14, sex=女, className=计科),
Student(id=7, name=Helen, age=13, sex=女, className=软工),
Student(id=8, name=jerry, age=12, sex=男, className=计科)
]
*/
该例中和例3类似的处理,都使用到了downstream
下游 - 收集器
Map<String, List<Student>> map = studentList.stream()
.collect(
Collectors.groupingBy(
Student::getClassName,
Collectors.collectingAndThen(Collectors.toList(), arr -> {
return arr.stream()
.sorted(Comparator.comparingInt(Student::getAge))
.collect(Collectors.toList());
})
)
);
/*
输出:
{
计科 =[
Student(id = 8, name = jerry, age = 12, sex = 男, className = 计科),
Student(id = 4, name = Dora, age = 14, sex = 女, className = 计科),
Student(id = 6, name = Farrah, age = 15, sex = 女, className = 计科),
Student(id = 9, name = Adam, age = 20, sex = 男, className = 计科)
],
软工 =[
Student(id = 7, name = Helen, age = 13, sex = 女, className = 软工),
Student(id = 2, name = lisa, age = 15, sex = 女, className = 软工),
Student(id = 3, name = Ada, age = 16, sex = 女, className = 软工),
Student(id = 1, name = tom, age = 19, sex = 男, className = 软工),
Student(id = 5, name = Bob, age = 20, sex = 男, className = 软工)
]
}
*/
本例中使用到的downstream
的方式更为通用,可以实现绝大多数的功能,例3中的方法JDK提供的简写方式
下面是用collectingAndThen
的方式实现和例3相同的功能
Map<String, Long> map = studentList.stream()
.collect(
Collectors.groupingBy(
Student::getClassName,
Collectors.collectingAndThen(Collectors.toList(), arr -> {
return (long) arr.size();
})
)
);
/*
输出:
{
计科=4,
软工=5
}
*/
Map<Integer, String> map = studentList.stream()
.collect(Collectors.toMap(Student::getId, Student::getName));
System.out.println(map);
/*
输出:
{
1=tom,
2=lisa,
3=Ada,
4=Dora,
5=Bob,
6=Farrah,
7=Helen,
8=jerry,
9=Adam
}
*/
上面代码,在现有的数据下正常运行,当添加多添加一条数据
studentList.add(new Student(9, "Adam - 2", 20, "男", "计科"));
这个时候id为9的数据有两条了,这时候再运行上面的代码就会出现Duplicate key Adam
也就是说调用toMap
时,假设其中存在重复的key,如果不做任何处理,会抛异常
解决异常就要引入toMap
方法的第3个参数mergeFunction
,函数式接口方法签名如下
R apply(T t, U u);
代码修改后如下
Map<Integer, String> map = studentList.stream()
.collect(Collectors.toMap(Student::getId, Student::getName, (v1, v2) -> {
System.out.println("value1: " + v1);
System.out.println("value2: " + v2);
return v1;
}));
/*
输出:
value1: Adam
value2: Adam - 2
{1=tom, 2=lisa, 3=Ada, 4=Dora, 5=Bob, 6=Farrah, 7=Helen, 8=jerry, 9=Adam}
*/
可以看出来mergeFunction
参数v1为原值,v2为新值
日常开发中是必须要考虑第3参数的mergeFunction
,一般采用策略如下
// 参数意义: o 为原值(old),n 为新值(new)
studentList.stream()
// 保留策略
.collect(Collectors.toMap(Student::getId, Student::getName, (o, n) -> o));
studentList.stream()
// 覆盖策略
.collect(Collectors.toMap(Student::getId, Student::getName, (o, n) -> n));