概述
Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
特点:
不是数据结构,不会保存数据。
不会修改原来的数据源,它会将操作后的数据保存到另外一个对象中。(保留意见:毕竟peek方法可以修改流中元素)
惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行终止操作的时候才会进行实际的计算。
现在谈及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{ R apply(T t); }
使用实例:定义一个转换函数『将字符串转为数字,再平方』
// 将字符串转为数字,再平方 FunctionstrConvertToIntAndSquareFun = (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 get(); }
使用实例:定义一个函数返回“Tom”
字符串
// 供给接口,调用一次返回一个 ”tom“ 字符串 SuppliertomFun = () -> "tom"; String tom = tomFun.get(); System.out.println(tom); // tom
Consumer接口
这是一个消费的接口。此接口有参数,但是没有返回值
@FunctionalInterface public interface Consumer{ void accept(T t); }
使用实例:定义一个函数传入数字,打印一行相应数量的A
// 重复打印 ConsumerprintA = (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{ boolean test(T t); }
使用实例:定义一个函数传入一个字符串,判断是否为A
字母开头且Z
字母结尾
// 判断是否为`A`字母开头且`Z`字母结尾 PredicatestrAStartAndZEnd = (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[]
可以
IntFunctionarrFun = 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 staticOptional empty() { @SuppressWarnings("unchecked") Optional t = (Optional ) EMPTY; return t; }
of
返回一个不包含null值的Optional
容器,传null值报空指针异常
public staticOptional of(T value) { return new Optional<>(value); }
ofNullable
返回一个可能包含null值的Optional
容器
public staticOptional ofNullable(T value) { return value == null ? empty() : of(value); }
可以使用的Optional
的方法
ifPresent
方法,参数是一个Consumer
,当容器内的值不为null是执行Consumer
Optionalopt = 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()方法 Listlist = 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
ListarrList = 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:统计
// 最大值 OptionalmaxOpt = Stream.of(2, 4, 3, 100) .max(Comparator.comparing(e -> e)); System.out.println(maxOpt.get()); // 100 // 最小值 Optional 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
看源码就可以看出来
staticFunction identity() { return t -> t; }
终端操作findAny:返回任意一个元素
OptionalanyOpt = Stream.of("one", "two", "three", "four") .findAny(); System.out.println(anyOpt.orElse("")); /* 输出: one */
终端操作findFirst:返回第一个元素
OptionalfirstOpt = 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
Listlist = Stream.of("one", "two", "three", "four") .collect(Collectors.toList()); System.out.println(list); /* 输出: [one, two, three, four] */
收集成Set『收集后有去除的效果,结果集乱序』
Setset = 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; } Mapmap = 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 staticCollector > toMap(Function super T, ? extends K> keyMapper, Function super T, ? extends U> valueMapper, BinaryOperator 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> 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例
下面例子用到的基础数据,如有例子特例会在例子中单独补充
ListstudentList = 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 staticList page(Collection 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 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
ListidList = 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> 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:统计每个班级中的人员个数
Mapmap = studentList.stream() .collect(Collectors.groupingBy( Student::getClassName, Collectors.mapping(Function.identity(), Collectors.counting()) )); System.out.println(map); /* 输出: { 计科=4, 软工=5 } */
例5:获取全部女生的名称
ListallFemaleNameList = studentList.stream() .filter(stu -> Objects.equals("女", stu.getSex())) .map(Student::getName) .collect(Collectors.toList()); System.out.println(allFemaleNameList); /* 输出: [lisa, Ada, Dora, Farrah, Helen] */
例6:依照年龄排序
// 年龄升序排序 ListstuList1 = 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 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> 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相同的功能
Mapmap = studentList.stream() .collect( Collectors.groupingBy( Student::getClassName, Collectors.collectingAndThen(Collectors.toList(), arr -> { return (long) arr.size(); }) ) ); /* 输出: { 计科=4, 软工=5 } */
例8:将数据转为ID和Name对应的数据结构Map
Mapmap = 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);
代码修改后如下
Mapmap = 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));
在原有的数据下增加一条特殊数据,这条特殊数据的name
为null
studentList.add(new Student(10, null, 20, "男", "计科"));
此时原始代码
和情况1
的代码都会出现空指针异常
解决方式就是toMap
的第二参数valueMapper
返回值不能为null
,下面是解决的方式
Mapmap = 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 staticFunction defaultValue(@NonNull Function func, @NonNull R defaultValue) { Objects.requireNonNull(func, "func不能为null"); Objects.requireNonNull(defaultValue, "defaultValue不能为null"); return t -> Optional.ofNullable(func.apply(t)).orElse(defaultValue); } } Map 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 staticList filter(Collection collection, Predicate filter) { if (isEmpty(collection)) { return Collections.emptyList(); } return collection.stream() .filter(filter) .collect(Collectors.toList()); } /** * 获取指定集合中的某个属性 * * @param collection 数据集合 * @param attrFunc 属性映射函数 * @return */ public static List attrs(Collection collection, Function attrFunc) { return attrs(collection, attrFunc, true); } /** * 获取指定集合中的某个属性 * * @param collection 数据集合 * @param attrFunc 属性映射函数 * @param filterEmpty 是否过滤空值 包括("", null, []) * @return */ public static List attrs(Collection collection, Function attrFunc, boolean filterEmpty) { if (isEmpty(collection)) { return Collections.emptyList(); } Stream 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 Map toMap(Collection collection, Function keyMapper, Function 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集合 ListidList = studentList.stream() .map(Student::getId) .collect(Collectors.toList()); // 获取id和name对应的map Map map = studentList.stream() .collect(Collectors.toMap(Student::getId, Student::getName, (k1, k2) -> k1)); // 过滤出 软工 班级的人员 List list = studentList.stream() .filter(e -> Objects.equals(e.getClassName(), "软工")) .collect(Collectors.toList());
使用工具类
// 获取id集合 ListidList = CollUtils.attrs(studentList, Student::getId); // 获取id和name对应的map Map map = CollUtils.toMap(studentList, Student::getId, Student::getName); // 过滤出 软工 班级的人员 List list = CollUtils.filter(studentList, e -> Objects.equals(e.getClassName(), "软工"));
工具类旨在减少单次使用Stream
时出现的冗余代码,如toMap
和toList
,同时也进行了为null
判断
总结
本篇介绍了函数式接口
,Lambda
,Optional
,方法引用
, Stream
等一系列知识点
也是工作中经过长时间积累终结下来的,比如例5中每一个操作都换一行,这样不完全是为了格式化好看
ListallFemaleNameList = studentList.stream() .filter(stu -> Objects.equals("女", stu.getSex())) .map(Student::getName) .collect(Collectors.toList()); System.out.println(allFemaleNameList); // 这样写 .filter 和 .map 的函数表达式中报错可以看出来是那一行
如果像下面这样写,报错是就会指示到一行上不能直接看出来是.filter
还是.map
报的错,并且这样写也显得拥挤
ListallFemaleNameList = 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使用的资料请关注脚本之家其它相关文章!