Java基础知识--Stream接口的理解与应用
声明:本文内容摘抄自: https://www.cnblogs.com/fengzheng/p/13083115.html
声明:本文中涉及到的代码由于文本限制,并不完全,其他可以参考: https://github.com/LoveWK/JavaBase.git /src/com/wk/stream/查看详细完全的代码
Stream 是 Java 8 中集合数据处理的利器,很多本来复杂、需要写很多代码的方法,比如过滤、分组等操作,往往使用 Stream 就可以在一行代码搞定,当然也因为 Stream 都是链式操作,一行代码可能会调用好几个方法。
Collection接口提供了 stream()方法,让我们可以在一个集合方便的使用 Stream API 来进行各种操作。值得注意的是,我们执行的任何操作都不会对源集合造成影响,你可以同时在一个集合上提取出多个 stream 进行操作。
我们看 Stream 接口的定义,继承自 BaseStream,几乎所有的接口声明都是接收方法引用类型的参数,比如 filter方法,接收了一个 Predicate类型的参数,它就是一个函数式接口,常用来作为条件比较、筛选、过滤用,JPA中也使用了这个函数式接口用来做查询条件拼接。
1 public interface Stream<T> extends BaseStream<T, Stream<T>> { 2 3 Stream<T> filter(Predicate<? super T> predicate); 4 5 // 其他接口 6 }
下面就来看看 Stream 常用 API。

of:可接收一个泛型对象或可变成泛型集合,构造一个 Stream 对象。
1 public class StreamDemo { 2 public static void main(String[] args) { 3 //调用of方法 4 Stream streamOf = createStream(); 5 System.out.println("调用of方法创建对象后:"+streamOf.count()); 6 7 } 8 //of方法:可接收一个泛型对象或可变成泛型集合,构造一个 Stream 对象。 9 private static Stream createStream(){ 10 Stream<String> stream = Stream.of("a","b","c"); 11 return stream; 12 } 13 }
测试结果:

empty方法:创建一个空的 Stream 对象
1 //empty方法:创建一个空的 Stream 对象。 2 private static Stream StreamEmpty(){ 3 Stream<String> stream = Stream.empty(); 4 return stream; 5 }
测试结果:

concat方法:连接两个 Stream ,不改变其中任何一个 Steam 对象,返回一个新的 Stream 对象。
//concat方法:连接两个 Stream ,不改变其中任何一个 Steam 对象,返回一个新的 Stream 对象。 2 private static Stream StreamConcat(){ 3 Stream<String> stream1 = Stream.of("a","b","c"); 4 Stream<String> stream2 = Stream.of("c","d"); 5 Stream<String> stream3 = Stream.concat(stream1,stream2); 6 return stream3; 7 }
测试结果:

max方法:一般用于求数字集合中的最大值,或者按实体中数字类型的属性比较,拥有最大值的那个实体。它接收一个 Comparator<T>,它是一个函数式接口类型,专门用作定义两个对象之间的比较,例如下面这个方法使用了 Integer::compareTo这个方法引用。
1 //max方法:一般用于求数字集合中的最大值,或者按实体中数字类型的属性比较,拥有最大值的那个实体。 2 //它接收一个 Comparator<T>,它是一个函数式接口类型,专门用作定义两个对象之间的比较, 3 //例如下面这个方法使用了 Integer::compareTo这个方法引用。 4 private static Integer StreamMax(){ 5 Stream<Integer> integerStream = Stream.of(2,2,100,5); 6 Integer max = integerStream.max(Integer::compareTo).get(); 7 return max; 8 }
测试结果:

当然,我们也可以自己定制一个 Comparator,顺便复习一下 Lambda 表达式形式的方法引用。
1 //自己定义一个Comparator 2 private static Integer MaxMine(){ 3 Stream<Integer> integerStream = Stream.of(2,2,100,5); 4 Comparator<Integer> comparator = (x,y) -> (x.intValue() < y.intValue()) ? -1 : ((x.equals(y) ? 0 :1)); 5 Integer max = integerStream.max(comparator).get(); 6 return max; 7 }
测试结果:

min方法:与max方法用法一致,只不过是求最小值:
findFirst方法:获取 Stream 中的第一个元素。
1 //findFirst方法,获取stream中的第一个元素 2 private static Optional<String> findFirst(){ 3 Stream<String> stringStream = Stream.of("a","b","c"); 4 Optional<String> first = stringStream.findFirst(); 5 return first; 6 }
测试结果:

findAny方法:获取 Stream 中的某个元素,如果是串行情况下,一般都会返回第一个元素,并行情况下就不一定了。
1 //findAny方法:获取 Stream 中的某个元素,如果是串行情况下,一般都会返回第一个元素,并行情况下就不一定了。 2 private static Optional<String> findAny(){ 3 Stream<String> stringStream = Stream.of("a","b","c"); 4 Optional<String> any = stringStream.findAny(); 5 return any; 6 }
测试结果:

peek方法:建立一个通道,在这个通道中对 Stream 的每个元素执行对应的操作,对应 Consumer<T>的函数式接口,这是一个消费者函数式接口,顾名思义,它是用来消费 Stream 元素的,
比如下面这个方法,把每个元素转换成对应的大写字母并输出。
1 //peek方法:建立一个通道,在这个通道中对 Stream 的每个元素执行对应的操作,对应 Consumer<T>的函数式接口, 2 // 这是一个消费者函数式接口,顾名思义,它是用来消费 Stream 元素的, 3 // 比如下面这个方法,把每个元素转换成对应的大写字母并输出。 4 private static void streamPeek(){ 5 Stream<String> stream = Stream.of("a","b","c"); 6 List<String> list = stream.peek(e -> System.out.println(e.toUpperCase())).collect(Collectors.toList()); 7 }
测试结果:

forEach方法:和 peek 方法类似,都接收一个消费者函数式接口,可以对每个元素进行对应的操作,但是和 peek 不同的是,forEach 执行之后,这个 Stream 就真的被消费掉了,之后这个 Stream 流就没有了,不可以再对它进行后续操作了,而 peek操作完之后,还是一个可操作的 Stream 对象。正好借着这个说一下,我们在使用 Stream API 的时候,都是一串链式操作,这是因为很多方法,比如接下来要说到的 filter方法等,返回值还是这个 Stream 类型的,也就是被当前方法处理过的 Stream 对象,所以 Stream API 仍然可以使用。
1 //forEach方法:和 peek 方法类似,都接收一个消费者函数式接口,可以对每个元素进行对应的操作, 2 // 但是和 peek 不同的是,forEach 执行之后,这个 Stream 就真的被消费掉了,之后这个 Stream 流就没有了, 3 // 不可以再对它进行后续操作了,而 peek操作完之后,还是一个可操作的 Stream 对象。 4 // 正好借着这个说一下,我们在使用 Stream API 的时候,都是一串链式操作,这是因为很多方法, 5 // 比如接下来要说到的 filter方法等,返回值还是这个 Stream 类型的,也就是被当前方法处理过的 Stream 对象, 6 // 所以 Stream API 仍然可以使用。 7 private static void streamForEach(){ 8 Stream<String> stream = Stream.of("a","b","c"); 9 stream.forEach(e -> System.out.println(e.toUpperCase())); 10 }
测试结果:

forEachOrdered方法:功能与 forEach是一样的,不同的是,forEachOrdered是有顺序保证的,也就是对 Stream 中元素按插入时的顺序进行消费。为什么这么说呢,当开启并行的时候,forEach和 forEachOrdered的效果就不一样了。
1 // forEachOrdered方法:功能与 forEach是一样的,不同的是,forEachOrdered是有顺序保证的, 2 // 也就是对 Stream 中元素按插入时的顺序进行消费。 3 // 为什么这么说呢,当开启并行的时候,forEach和 forEachOrdered的效果就不一样了。 4 private static void streamForEachOrdered(){ 5 System.out.println("调用forEach方法:"); 6 Stream<String> streamForEach = Stream.of("a","b","c","d"); 7 streamForEach.parallel().forEach(e -> System.out.println(e.toUpperCase())); 8 //当使用上面的代码时,输出的结果可能是 B、A、C、D 或者 A、C、D、B或者A、B、C、D, 9 // 而使用下面的代码,则每次都是 A、 B、C、D 10 System.out.println("调用forEachOrdered方法:"); 11 Stream<String> streamOrdered = Stream.of("a","b","c","d"); 12 streamOrdered.parallel().forEachOrdered(e -> System.out.println(e.toUpperCase())); 13 14 }
测试结果:

limit方法:获取前 n 条数据,类似于 MySQL 的limit,只不过只能接收一个参数,就是数据条数。
1 //limit方法:获取前 n 条数据,类似于 MySQL 的limit,只不过只能接收一个参数,就是数据条数。 2 private static void streamLimit(){ 3 Stream<String> stream = Stream.of("a","b","c"); 4 stream.limit(2).forEach(e -> System.out.println(e)); 5 }
测试结果:

skip方法:跳过前 n 条数据,也可以理解成获取截取前面几条后,剩余的数据;
1 //skip方法:跳过前 n 条数据,也可以理解成获取截取前面几条后,剩余的数据; 2 private static void streamSkip(){ 3 Stream<String> stream = Stream.of("a","b","c"); 4 stream.skip(2).forEach(e -> System.out.println(e)); 5 }
distinct方法:元素去重,例如下面方法返回元素是 a、b、c,将重复的 b 只保留了一个。
1 //distinct方法:元素去重,例如下面方法返回元素是 a、b、c,将重复的 b 只保留了一个。 2 private static void streamDistinct(){ 3 Stream<String> stream = Stream.of("a","b","c","b","a"); 4 stream.distinct().forEach(e -> System.out.println(e)); 5 }
测试结果:

sorted方法:有两个重载,一个无参数,另外一个有个 Comparator类型的参数。
无参类型的按照自然顺序进行排序,只适合比较单纯的元素,比如数字、字母等。
1 //sorted方法:有两个重载,一个无参数,另外一个有个 Comparator类型的参数。 2 //无参类型的按照自然顺序进行排序,只适合比较单纯的元素,比如数字、字母等。 3 private static void streamSorted(){ 4 Stream<String> stream = Stream.of("a","c","b"); 5 stream.sorted().forEach(e -> System.out.println(e)); 6 }
有参数的需要自定义排序规则,例如下面这个方法,按照第二个字母的大小顺序排序,最后输出的结果是 a1、b3、c6。
1 //有参数的需要自定义排序规则,例如下面这个方法,按照第二个字母的大小顺序排序,最后输出的结果是 a1、b3、c6。 2 private static void sortedWithComparator() { 3 Stream<String> stream = Stream.of("a1", "c6", "b3"); 4 stream.sorted((x, y) -> Integer.parseInt(x.substring(1)) > Integer.parseInt(y.substring(1)) ? 1 : -1).forEach(e -> System.out.println(e)); 5 }
测试结果:
![]()
为了下面的示例好展示,准备一些基础数据;
1 /** 2 * 模拟客户信息 3 * @return 4 */ 5 private static List<User> getUserData(){ 6 Random random = new Random(); 7 List<User> users = new ArrayList<>(); 8 for(int i=1; i<=10; i++){ 9 User user = new User(); 10 user.setUserId(i); 11 user.setUserName(String.format("古时的风筝%s号",i)); 12 user.setAge(random.nextInt(100)); 13 user.setGender(i%2); 14 user.setPhone("18311111111"); 15 user.setAddress("无"); 16 users.add(user); 17 } 18 return users; 19 }
filter方法:用于条件筛选过滤,筛选出符合条件的数据,
例如:下面这个方法,筛选出性别为 0,年龄大于 50 的记录。
1 //filter方法:用于条件筛选过滤,筛选出符合条件的数据, 2 //例如:下面这个方法,筛选出性别为 0,年龄大于 50 的记录。 3 private static void streamFilter(){ 4 List<User> users = getUserData(); 5 Stream<User> stream = users.stream(); 6 stream.filter(user -> user.getGender()==0 && user.getAge()>50).forEach(e-> System.out.println(e)); 7 8 /** 9 * 等同于下面这种形式,匿名内部类 10 */ 11 /*stream.filter(new Predicate<User>() { 12 @Override 13 public boolean test(User user) { 14 return user.getGender() ==0 && user.getAge()>50; 15 } 16 }).forEach(e-> System.out.println(e));*/ 17 }
测试结果:

map方法:map方法的接口方法声明如下,接受一个 Function函数式接口,把它翻译成映射最合适了,通过原始数据元素,映射出新的类型。
1 <R> Stream<R> map(Function<? super T, ? extends R> mapper);
而 Function的声明是这样的,观察 apply方法,接受一个 T 型参数,返回一个 R 型参数。用于将一个类型转换成另外一个类型正合适,这也是 map的初衷所在,用于改变当前元素的类型,例如将 Integer 转为 String类型,将 DAO 实体类型,转换为 DTO 实例类型。
当然了,T 和 R 的类型也可以一样,这样的话,就和 peek方法没什么不同了。
1 @FunctionalInterface 2 public interface Function<T, R> { 3 4 /** 5 * Applies this function to the given argument. 6 * 7 * @param t the function argument 8 * @return the function result 9 */ 10 R apply(T t); 11 }
例如下面这个方法,应该是业务系统的常用需求,将 User 转换为 API 输出的数据格式。
1 //map方法:接受一个 Function函数式接口,把它翻译成映射最合适了,通过原始数据元素,映射出新的类型。 2 private static void streamMap(){ 3 List<User> users = getUserData(); 4 Stream<User> stream = users.stream(); 5 List<UserDto> userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList()); 6 userDtos.forEach(userDto -> System.out.println(userDto)); 7 }
1 /** 2 * 把User对象属性传给UserDto对象 3 * @param user 4 * @return 5 */ 6 private static UserDto dao2Dto(User user){ 7 UserDto dto = new UserDto(); 8 try { 9 BeanUtils.CopySameBean(user,dto); 10 } catch (IllegalAccessException e) { 11 e.printStackTrace(); 12 } 13 //其他额外处理 14 return dto; 15 }
1 /** 2 * 〈一句话功能简述〉<br> 3 * 〈Bean拷贝工具类〉 4 * 5 * @author wangkai_wb 6 * @create 2020/6/11 7 * @since 1.0.0 8 */ 9 public class BeanUtils { 10 /** 11 * 将一个实体类复制到另一个实体类中 12 * @param fromBean 13 * @param toBean 14 * @throws IllegalAccessException 15 * @throws IllegalArgumentException 16 * @throws Exception 17 */ 18 public static void CopySameBean(Object fromBean, Object toBean) throws NullPointerException, IllegalArgumentException, IllegalAccessException { 19 if(fromBean == toBean) { 20 return; 21 } 22 if(fromBean != null) { 23 // 得到类对象 24 Class fromBeanClass = fromBean.getClass(); 25 Class toBeanClass = toBean.getClass(); 26 27 /** 28 * 得到类中的所有属性集合 29 */ 30 Field[] fbc = fromBeanClass.getDeclaredFields(); 31 Field[] tbc = toBeanClass.getDeclaredFields(); 32 for(int i = 0;i < fbc.length;i++) { 33 Field f = fbc[i]; 34 35 f.setAccessible(true); // 设置些属性是可以访问的 36 Object fVal = f.get(fromBean);// 得到此属性的值 37 // System.out.println("name:" + f.getName() + "\t value = " + fVal); 38 for(int j = 0;j < tbc.length;j++) { 39 Field t = tbc[i]; 40 41 t.setAccessible(true); // 设置些属性是可以访问的 42 // 属性名称和属性类型必须全部相同,才能赋值 43 if(f.getName().equals(t.getName()) && f.getType().toString().equals(t.getType().toString())){ 44 t.set(toBean, fVal); 45 } 46 } 47 } 48 } else { 49 throw new NullPointerException("FromBean is null"); 50 } 51 } 52 }
测试结果:

mapToInt方法:将元素转换成 int 类型,在 map方法的基础上进行封装。
mapToLong方法:将元素转换成 Long 类型,在 map方法的基础上进行封装。
mapToDouble方法:将元素转换成 Double 类型,在 map方法的基础上进行封装。
flatMap方法:这是用在一些比较特别的场景下,当你的 Stream 是以下这几种结构的时候,需要用到 flatMap方法,用于将原有二维结构扁平化。
1.Stream<String[]>
2.Stream<Set<String>>
3.Stream<List<String>>
以上这三类结构,通过 flatMap方法,可以将结果转化为 Stream<String>这种形式,方便之后的其他操作。
比如下面这个方法,将List<List<User>>扁平处理,然后再使用 map或其他方法进行操作。
1 //flatMap方法:用于将原有二维结构扁平化 2 private static void flatMap(){ 3 List<User> users = getUserData(); 4 List<User> users1 = getUserData(); 5 List<List<User>> userList = new ArrayList<>(); 6 userList.add(users); 7 userList.add(users1); 8 Stream<List<User>> stream = userList.stream(); 9 List<UserDto> userDtos = stream.flatMap(subUserList -> subUserList.stream()).map(user -> dao2Dto(user)).collect(Collectors.toList()); 10 for (UserDto userDto : userDtos){ 11 System.out.println(userDto.toString()); 12 } 13 }
测试结果:

flatMapToInt方法:用法参考 flatMap,将元素扁平为 int 类型,在 flatMap方法的基础上进行封装。
flatMapToLong方法:用法参考 flatMap,将元素扁平为 Long 类型,在 flatMap方法的基础上进行封装。
flatMapToDouble方法:用法参考 flatMap,将元素扁平为 Double 类型,在 flatMap方法的基础上进行封装。
collection方法:在进行了一系列操作之后,我们最终的结果大多数时候并不是为了获取 Stream 类型的数据,而是要把结果变为 List、Map 这样的常用数据结构,而 collection就是为了实现这个目的。
就拿 map 方法的那个例子说明,将对象类型进行转换后,最终我们需要的结果集是一个 List<UserDto >类型的,使用 collect方法将 Stream 转换为我们需要的类型。
下面是 collect接口方法的定义:
1 <R, A> R collect(Collector<? super T, A, R> collector);
下面这个例子演示了将一个简单的 Integer Stream 过滤出大于 7 的值,然后转换成 List<Integer>集合,用的是 Collectors.toList()这个收集器。
1 // 在进行了一系列操作之后,我们最终的结果大多数时候并不是为了获取 Stream 类型的数据,而是要把结果变为 List、Map 这样的常用数据结构, 2 // 而 collection就是为了实现这个目的。 3 private static void streamCollect(){ 4 Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33); 5 List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(Collectors.toList()); 6 for (Integer l : list){ 7 System.out.println(l); 8 } 9 }
测试结果:

很多同学表示看不太懂这个 Collector是怎么一个意思,来,我们看下面这段代码,这是 collect的另一个重载方法,你可以理解为它的参数是按顺序执行的,这样就清楚了,这就是个 ArrayList 从创建到调用 addAll方法的一个过程。
1 //这是 collect的另一个重载方法,你可以理解为它的参数是按顺序执行的,这样就清楚了, 2 // 这就是个 ArrayList 从创建到调用 addAll方法的一个过程。 3 private static void collectList(){ 4 Stream<Integer> integerStream1 = Stream.of(1,2,5,7,8,12,33); 5 List<Integer> list1 = integerStream1.filter(s -> s.intValue()>7).collect(ArrayList::new,ArrayList::add,ArrayList::addAll); 6 for (Integer ls : list1){ 7 System.out.println(""+ls); 8 } 9 }
测试结果:

我们在自定义 Collector的时候其实也是这个逻辑,不过我们根本不用自定义, Collectors已经为我们提供了很多拿来即用的收集器。比如我们经常用到Collectors.toList()、Collectors.toSet()、Collectors.toMap()。另外还有比如Collectors.groupingBy()用来分组,比如下面这个例子,按照 userId 字段分组,返回以 userId 为key,List 为value 的 Map,或者返回每个 key 的个数。
groupingBy方法:按照 userId 字段分组,返回以 userId 为key,List 为value 的 Map.
1 //groupingBy方法:按照 userId 字段分组,返回以 userId 为key,List 为value 的 Map 2 private static void groupingBy(){ 3 List<User> users = getUserData(); 4 User user = new User(); 5 user.setUserId(1); 6 user.setUserName("新加的风筝1号"); 7 user.setAge(12); 8 user.setGender(1); 9 user.setPhone("13500011111"); 10 user.setAddress("无"); 11 users.add(user); 12 User user1 = new User(); 13 user1.setUserId(2); 14 user1.setUserName("新加的风筝2号"); 15 user1.setAge(15); 16 user1.setGender(0); 17 user1.setPhone("13700011111"); 18 user1.setAddress("无"); 19 users.add(user1); 20 Map<Integer,List<User>> map = users.stream().collect(Collectors.groupingBy(User::getUserId)); 21 map.forEach((key,value) -> System.out.println("key:"+key+" value:"+value)); 22 }
测试结果:

groupingBy方法:返回分组后每个 key 的个数:
1 //groupingBy方法:返回每个 key 的个数 2 private static void groupingByCount(){ 3 List<User> users = getUserData(); 4 User user = new User(); 5 user.setUserId(1); 6 user.setUserName("新加的风筝1号"); 7 user.setAge(12); 8 user.setGender(1); 9 user.setPhone("13500011111"); 10 user.setAddress("无"); 11 users.add(user); 12 User user1 = new User(); 13 user1.setUserId(2); 14 user1.setUserName("新加的风筝2号"); 15 user1.setAge(15); 16 user1.setGender(0); 17 user1.setPhone("13700011111"); 18 user1.setAddress("无"); 19 users.add(user1); 20 Map<Integer,Long> map = users.stream().collect(Collectors.groupingBy(User::getUserId,Collectors.counting())); 21 map.forEach((key, value)-> System.out.println("key:"+key+" value:"+value)); 22 }
测试结果:

toArray方法:collection是返回列表、map 等,toArray是返回数组,有两个重载,一个空参数,返回的是 Object[]。
另一个接收一个 IntFunction<R>类型参数。
1 @FunctionalInterface 2 public interface IntFunction<R> { 3 4 /** 5 * Applies this function to the given argument. 6 * 7 * @param value the function argument 8 * @return the function result 9 */ 10 R apply(int value); 11 }
比如像下面这样使用,参数是 User[]::new也就是new 一个 User 数组,长度为最后的 Stream 长度。
1 //toArray方法:返回 User 数组,长度为最后的 Stream 长度。 2 private static void streamToArray(){ 3 List<User> users = getUserData(); 4 Stream<User> stream = users.stream(); 5 User[] userArray = stream.filter(user -> user.getGender() ==0 && user.getAge()>50).toArray(User[]::new); 6 Arrays.asList(userArray).forEach(user -> System.out.println(user)); 7 }
reduce方法:它的作用是每次计算的时候都用到上一次的计算结果,比如求和操作,前两个数的和加上第三个数的和,再加上第四个数,
一直加到最后一个数位置,最后返回结果,就是 reduce的工作过程。
//reduce方法:它的作用是每次计算的时候都用到上一次的计算结果,比如求和操作,前两个数的和加上第三个数的和, // 再加上第四个数,一直加到最后一个数位置,最后返回结果,就是 reduce的工作过程。 private static void streamReduce(){ Stream<Integer> stream = Stream.of(1,2,5,7,8,12,33); Integer sum = stream.reduce(0,(x,y)->x+y); System.out.println("累加的和:"+sum); }
测试结果:

另外 Collectors好多方法都用到了 reduce,比如 groupingBy、minBy、maxBy等等。
并行 Stream
Stream 本质上来说就是用来做数据处理的,为了加快处理速度,Stream API 提供了并行处理 Stream 的方式。通过 users.parallelStream()或者users.stream().parallel() 的方式来创建并行 Stream 对象,支持的 API 和普通 Stream 几乎是一致的。
并行 Stream 默认使用 ForkJoinPool线程池,当然也支持自定义,不过一般情况下没有必要。ForkJoin 框架的分治策略与并行流处理正好契合。
虽然并行这个词听上去很厉害,但并不是所有情况使用并行流都是正确的,很多时候完全没这个必要。
什么情况下使用或不应使用并行流操作呢?
- 必须在多核 CPU 下才使用并行 Stream,听上去好像是废话。
- 在数据量不大的情况下使用普通串行 Stream 就可以了,使用并行 Stream 对性能影响不大。
- CPU 密集型计算适合使用并行 Stream,而 IO 密集型使用并行 Stream 反而会更慢。
- 虽然计算是并行的可能很快,但最后大多数时候还是要使用
collect合并的,如果合并代价很大,也不适合用并行 Stream。 - 有些操作,比如 limit、 findFirst、forEachOrdered 等依赖于元素顺序的操作,都不适合用并行 Stream。


浙公网安备 33010602011771号