JDK8中Stream使用解析
JDK8中Stream使用解析
现在谈及JDK8的新特新,已经说不上新了。本篇介绍的就是Stream
和Lambda
,说的Stream
可不是JDK中的IO流
,这里的Stream
指的是处理集合的抽象概念『像流一样处理集合数据』。
了解Stream
前先认识一下Lambda
。
函数式接口和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
的构成
- 括号里面的参数
- 箭头
->
- 然后是身体
- 它可以是单个表达式或java代码块。
整体表现为 (...参数) -> {代码块}
简写
下面就是函数式接口的实现简写为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
- 小括号内参数的类型可以省略;
- 没有参数时小括号不能省略,小括号中有且仅有一个参数时,不能缺省括号
- 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号(三者省略都需要一起省略)。
看到这里应该认识到了如何用Lambda
简写函数式接口
,那现在就进一步的认识一下JDK中Stream
中对函数式接口的几种大类
常用内置函数式接口
上节说明了Lambda
表达式就是为了简写函数式接口,为使用方便,JDK8提供了一些常用的函数式接口。最具代表性的为Supplier、Function、Consumer、Perdicate
,这些函数式接口都在java.util.function
包下。
这些函数式接口都是泛型类型的,下面的源码都去除了default方法,只保留真正需要实现的方法。
Function接口
这是一个转换的接口。接口有参数、有返回值,传入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
Supplier接口
这是一个对外供给的接口。此接口无需参数,即可返回结果
@FunctionalInterface
public interface Supplier<T> {
T get();
}
使用实例:定义一个函数返回“Tom”
字符串
// 供给接口,调用一次返回一个 ”tom“ 字符串
Supplier<String> tomFun = () -> "tom";
String tom = tomFun.get();
System.out.println(tom); // tom
Consumer接口
这是一个消费的接口。此接口有参数,但是没有返回值
@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
Predicate接口
这是一个断言的接口。此接口对输入的参数进行一系列的判断,返回一个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工具
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
Stream
可以看作是一个高级版的迭代器。增强了Collection
的,极大的简化了对集合的处理。
想要使用Stream
首先需要创建一个
创建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
是一次性的计算完会失效
操作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);
有这一个终端操作
就可以向下介绍大量的中间操作了
- 中间操作
中间操作filter:过滤元素
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
*/
中间操作limit:截断元素
限制集合长度不能超过指定大小
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
.limit(2)
.forEach(System.out::println);
/*
输出:
jerry
lisa
*/
中间操作skip:跳过元素(丢弃流的前n元素)
// 丢弃前2个元素
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
.skip(2)
.forEach(System.out::println);
/*
输出:
moli
tom
Demi
*/
中间操作map:转换元素
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:查看元素
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'}
*/
中间操作sorted:排序数据
// 排序数据
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'}
*/
中间操作distinct:去重
注意:必须重写对应泛型的hashCode()和equals()方法
Stream.of(2, 2, 4, 4, 3, 3, 100)
.distinct()
.forEach(System.out::println);
/*
输出:
2
4
3
100
*/
中间操作flatMap:平铺流
返回一个流,该流由通过将提供的映射函数(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
*/
终端操作max,min,count:统计
// 最大值
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;
}
终端操作findAny:返回任意一个元素
Optional<String> anyOpt = Stream.of("one", "two", "three", "four")
.findAny();
System.out.println(anyOpt.orElse(""));
/*
输出:
one
*/
终端操作findFirst:返回第一个元素
Optional<String> firstOpt = Stream.of("one", "two", "three", "four")
.findFirst();
System.out.println(firstOpt.orElse(""));
/*
输出:
one
*/
返回的Optional
容器在上面介绍过了,一般配置orElse
使用,原因就在于findAny
和findFirst
可能返回空空容器,调用get
可能会抛空指针异常
终端操作allMatch,anyMatch:匹配
// 是否全部为 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:收集元素到集合
collect
高级使用方法很复杂,常用的用法使用Collectors
工具类
- 收集成List
List<String> list = Stream.of("one", "two", "three", "four")
.collect(Collectors.toList());
System.out.println(list);
/*
输出:
[one, two, three, four]
*/
- 收集成Set『收集后有去除的效果,结果集乱序』
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
- 收集成Map
// 使用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
分组还有一个参数可以指定下级收集器,后续例子中有使用到
Steam例
下面例子用到的基础数据,如有例子特例会在例子中单独补充
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, "男", "计科"));
例1:封装一个分页函数
/**
* 分页方法
*
* @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=软工)
]
*/
例2:获取软工班全部的人员id
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]
*/
例3:收集每个班级中的人员名称列表
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]
}
*/
例4:统计每个班级中的人员个数
Map<String, Long> map = studentList.stream()
.collect(Collectors.groupingBy(
Student::getClassName,
Collectors.mapping(Function.identity(), Collectors.counting())
));
System.out.println(map);
/*
输出:
{
计科=4,
软工=5
}
*/
例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]
*/
例6:依照年龄排序
// 年龄升序排序
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=计科)
]
*/
例7:分班级依照年龄排序
该例中和例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
}
*/
例8:将数据转为ID和Name对应的数据结构Map
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
}
*/
- 情况1
上面代码,在现有的数据下正常运行,当添加多添加一条数据
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));
- 情况2
在原有的数据下增加一条特殊数据,这条特殊数据的name
为null
studentList.add(new Student(10, null, 20, "男", "计科"));
此时原始代码
和情况1
的代码都会出现空指针异常
解决方式就是toMap
的第二参数valueMapper
返回值不能为null
,下面是解决的方式
Map<Integer, String> map = studentList.stream()
.collect(Collectors.toMap(
Student::getId,
e -> Optional.ofNullable(e.getName()).orElse(""),
(o, n) -> o
));
System.out.println(map);
/*
输出:
{1=tom, 2=lisa, 3=Ada, 4=Dora, 5=Bob, 6=Farrah, 7=Helen, 8=jerry, 9=Adam, 10=}
*/
// 此时没有空指针异常了
还有一种写法(参考写法,不用idea
工具编写代码,这种写法没有意义)
public final class Func {
/**
* 当 func 执行结果为 null 时, 返回 defaultValue
*
* @param func 转换函数
* @param defaultValue 默认值
* @return
*/
public static <T, R> Function<T, R> defaultValue(@NonNull Function<T, R> func, @NonNull R defaultValue) {
Objects.requireNonNull(func, "func不能为null");
Objects.requireNonNull(defaultValue, "defaultValue不能为null");
return t -> Optional.ofNullable(func.apply(t)).orElse(defaultValue);
}
}
Map<Integer, String> map = studentList.stream()
.collect(Collectors.toMap(
Student::getId,
Func.defaultValue(Student::getName, null),
(o, n) -> o
));
System.out.println(map);
这样写是为了使用像idea
这样的工具时,Func.defaultValue(Student::getName, null)
调用第二个参数传null
会有一个告警的标识『不关闭idea
的检查就会有warning
提示』。
综上就是toMap
的使用注意点,
key
映射的id
有不能重复的限制,value
映射的name
也有不能有null
,解决方式也在下面有提及
例9:封装一下关于Stream的工具类
工作中使用Stream
最多的操作都是对于集合来的,有时Stream
使用就是一个简单的过滤filter
或者映射map
操作,这样就出现了大量的.collect(Collectors.toMap(..., ..., ...))
和.collect(Collectors.toList())
,有时还要再调用之前检测集合是否为null
,下面就是对Stream
的单个方法进行封装
public final class CollUtils {
/**
* 过滤数据集合
*
* @param collection 数据集合
* @param filter 过滤函数
* @return
*/
public static <T> List<T> filter(Collection<T> collection, Predicate<T> filter) {
if (isEmpty(collection)) {
return Collections.emptyList();
}
return collection.stream()
.filter(filter)
.collect(Collectors.toList());
}
/**
* 获取指定集合中的某个属性
*
* @param collection 数据集合
* @param attrFunc 属性映射函数
* @return
*/
public static <T, R> List<R> attrs(Collection<T> collection, Function<T, R> attrFunc) {
return attrs(collection, attrFunc, true);
}
/**
* 获取指定集合中的某个属性
*
* @param collection 数据集合
* @param attrFunc 属性映射函数
* @param filterEmpty 是否过滤空值 包括("", null, [])
* @return
*/
public static <T, R> List<R> attrs(Collection<T> collection, Function<T, R> attrFunc, boolean filterEmpty) {
if (isEmpty(collection)) {
return Collections.emptyList();
}
Stream<R> rStream = collection.stream().map(attrFunc);
if (!filterEmpty) {
return rStream.collect(Collectors.toList());
}
return rStream.filter(e -> {
if (Objects.isNull(e)) {
return false;
}
if (e instanceof Collection) {
return !isEmpty((Collection<?>) e);
}
if (e instanceof String) {
return ((String) e).length() > 0;
}
return true;
}).collect(Collectors.toList());
}
/**
* 转换为map, 有重复key时, 使用第一个值
*
* @param collection 数据集合
* @param keyMapper key映射函数
* @param valueMapper value映射函数
* @return
*/
public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<T, K> keyMapper,
Function<T, V> valueMapper) {
if (isEmpty(collection)) {
return Collections.emptyMap();
}
return collection.stream()
.collect(Collectors.toMap(keyMapper, valueMapper, (k1, k2) -> k1));
}
/**
* 判读集合为空
*
* @param collection 数据集合
* @return
*/
public static boolean isEmpty(Collection<?> collection) {
return Objects.isNull(collection) || collection.isEmpty();
}
}
如果单次使用Stream
都在一个函数中可能出现大量的冗余代码,如下
// 获取id集合
List<Integer> idList = studentList.stream()
.map(Student::getId)
.collect(Collectors.toList());
// 获取id和name对应的map
Map<Integer, String> map = studentList.stream()
.collect(Collectors.toMap(Student::getId, Student::getName, (k1, k2) -> k1));
// 过滤出 软工 班级的人员
List<Student> list = studentList.stream()
.filter(e -> Objects.equals(e.getClassName(), "软工"))
.collect(Collectors.toList());
使用工具类
// 获取id集合
List<Integer> idList = CollUtils.attrs(studentList, Student::getId);
// 获取id和name对应的map
Map<Integer, String> map = CollUtils.toMap(studentList, Student::getId, Student::getName);
// 过滤出 软工 班级的人员
List<Student> list = CollUtils.filter(studentList, e -> Objects.equals(e.getClassName(), "软工"));
工具类旨在减少单次使用Stream
时出现的冗余代码,如toMap
和toList
,同时也进行了为null
判断
总结
本篇介绍了函数式接口
,Lambda
,Optional
,方法引用
, Stream
等一系列知识点
也是工作中经过长时间积累终结下来的,比如例5中每一个操作都换一行,这样不完全是为了格式化好看
List<String> allFemaleNameList = studentList.stream()
.filter(stu -> Objects.equals("女", stu.getSex()))
.map(Student::getName)
.collect(Collectors.toList());
System.out.println(allFemaleNameList);
// 这样写 .filter 和 .map 的函数表达式中报错可以看出来是那一行
如果像下面这样写,报错是就会指示到一行上不能直接看出来是.filter
还是.map
报的错,并且这样写也显得拥挤
List<String> allFemaleNameList = studentList.stream().filter(stu -> Objects.equals("女", stu.getSex())).map(Student::getName).collect(Collectors.toList());
System.out.println(allFemaleNameList);
Stream
的使用远远不止本篇文章介绍到的,比如一些同类的IntStream
,LongStream
,DoubleStream
都是大同小异,只要把Lambda
搞熟其他用法都一样
学习Stream
流一定要结合场景来,同时也要注意Stream
需要规避的一些风险,如toMap
的注意点(例8有详细介绍)。
还有一些高级用法downstream
下游 - 收集器等(例4,例7)。
JDK8中Stream使用解析的更多相关文章
- Jdk8中Stream流的使用,让你脱离for循环
学习要求: 知道一点儿函数式接口和Lambda表达式的基础知识,有利于更好的学习. 1.先体验一下Stream的好处 需求:给你一个ArrayList用来保存学生的成绩,让你打印出其中大于60的成绩. ...
- 关于JDK8中stream的用法小总结。
import java.io.Serializable; import java.util.*; import java.util.stream.Collectors; public class Ma ...
- forEach与jdk8中的lambda, Stream
增强for循环 :forEach 反编译后可以看到实际使用的仍然是Iterator+while遍历的 forEach的优点是写法简单,缺点是不能使用xxx.remove(e)或者iter.remove ...
- 【JDK8】JDK 8 中Stream流中的去重的方法
JDK 8 中Stream流中去重的方法 1.简单的去重,可以使用distinct()方法去重,该方法是通过比较equals和hashcode值去去重, 2.复杂的去重, 例如,在一个JavaBean ...
- JDK8中JVM对类的初始化探讨
在<深入理解Java虚拟机>(第二版,周志明著)中,作者介绍了JVM必须初始化类(或接口)的五种情况,但是是针对JDK7而言的. 那么,在JDK8中,这几种情况有没有变化呢?(我猜测应该会 ...
- 关于java中Stream理解
关于java中Stream理解 Stream是什么 Stream:Java 8新增的接口,Stream可以认为是一个高级版本的Iterator.它代表着数据流,流中的数据元素的数量可以是有限的, 也可 ...
- JDK8中JVM堆内存划分
一:JVM中内存 JVM中内存通常划分为两个部分,分别为堆内存与栈内存,栈内存主要用运行线程方法 存放本地暂时变量与线程中方法运行时候须要的引用对象地址. JVM全部的对象信息都 存放在堆内存中.相比 ...
- JDK8集合类源码解析 - HashSet
HashSet 特点:不允许放入重复元素 查看源码,发现HashSet是基于HashMap来实现的,对HashMap做了一次“封装”. private transient HashMap<E,O ...
- JDK8集合类源码解析 - HashMap
java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap.Hashtable.LinkedHashMap和TreeMap HashMap ...
随机推荐
- Jsoup-基于Java实现网络爬虫-爬取笔趣阁小说
注意!仅供学习交流使用,请勿用在歪门邪道的地方!技术只是工具!关键在于用途! 今天接触了一款有意思的框架,作用是网络爬虫,他可以像操作JS一样对网页内容进行提取 初体验Jsoup <!-- Ma ...
- 微软加入字节码联盟,进一步开发支持Blazor 的WebAssembly技术
字节码联盟 (Bytecode Alliance)宣布已正式成为 501(c)(3) 非营利组织,参与组建的企业/组织包括 Fastly.英特尔.Mozilla 和微软,此外还邀请到了 Arm.DFI ...
- 1. APP移动端性能测试基础知识入门
本博客要点 生命周期 堆和栈 垃圾回收 adb命令 Activity的生命周期
- sublime text 快捷键的使用大全
多行选择后按下ctrl+/ 选择类 Ctrl+D 选中光标所占的文本,继续操作则会选中下一个相同的文本. Alt+F3 选中文本按下快捷键,即可一次性选择全部的相同文本进行同时编辑.举个栗子:快速选中 ...
- 【工具库】Java实体映射工具MapStruct
一.什么是MapStruct? MapStruct是用于代码中JavaBean对象之间的转换,例如DO转换为DTO,DTO转换为VO,或Entity转换为VO等场景,虽然Spring库和 Apache ...
- kali 中文乱码解决
在命令行输入"dpkg-reconfigure locales".进入图形化界面之后,(空格是选择,Tab是切换,*是选中),选中en_US.UTF-8和zh_CN.UTF-8,确 ...
- hdu2167 方格取数 状态压缩dp
题意: 方格取数,八个方向的限制. 思路: 八个方向的不能用最大流了,四个的可以,八个的不能抽象成二分图,所以目测只能用dp来跑,dp[i][j]表示的是第i行j状态的最优,具体看 ...
- POJ1201基础差分约束
题意: 有一条直线,直线上做多有50000个点,然后给你组关系 a b c表明a-b之间最少有c个点,问直线上最少多少个点. 思路: a-b最少有c个点可以想象a到b+1的距 ...
- 解决Failed to execute goal se.eris:notnull-instrumenter-maven-plugin:0.6.8
https://blog.csdn.net/fanrenxiang/article/details/80864908 github拉起来的项目,jdk是11,而我电脑上的jdk是1.8.原因是jdk版 ...
- windows下使用dos命令手工与ntp服务器同步系统时间
管理员模式的命令窗口 net stop w32time &w32tm /unregister &w32tm /register &net start w32time & ...