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 }
测试结果:
![](https://img2020.cnblogs.com/blog/1291342/202006/1291342-20200611145124319-1439498673.png)
为了下面的示例好展示,准备一些基础数据;
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。