Java Stream--(2)常用操作

783 阅读17分钟

Stream和其它集合类的区别在于:其它集合类主要关注与有限数量的数据的访问和有效管理(增删改),而Stream并没有提供访问和管理元素的方式,而是通过声明数据源的方式,利用可计算的操作在数据源上执行,当然BaseStream.iterator() 和 BaseStream.spliterator()操作提供了遍历元素的方法。

Java Stream提供了提供了串行和并行两种类型的流,保持一致的接口,提供函数式编程方式,以管道方式提供中间操作和最终执行操作,为Java语言的集合提供了现代语言提供的类似的高阶函数操作,简化和提高了Java集合的功能。

Stream接口还包含几个基本类型的子接口如IntStream, LongStream 和 DoubleStream。

关于流和其它集合具体的区别,可以参照下面的列表:

  • 不存储数据。流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
  • 函数式编程。流的操作不会修改数据源,例如filter不会将数据源中的数据删除。
  • 延迟操作。流的很多操作如filter,map等中间操作是延迟执行的,只有到终点操作才会将操作顺序执行。
  • 可以解绑。对于无限数量的流,有些操作是可以在有限的时间完成的,比如limit(n) 或 findFirst(),这些操作可是实现"短路"(Short-circuiting),访问到有限的元素后就可以返回。
  • 纯消费。流的元素只能访问一次,类似Iterator,操作没有回头路,如果你想从头重新访问流的元素,对不起,你得重新生成一个新的流。

流的操作是以管道的方式串起来的。流管道包含一个数据源,接着包含零到N个中间操作,最后以一个终点操作结束。

过滤(Filter)

filter的定义:

Stream<T> filter(Predicate<? super T> predicate);

这个方法,传入一个Predicate的函数接口,Predicate函数接口传入一个泛型参数T,做完操作之后,返回一个boolean值;filter方法的作用,是对这个boolean做判断,返回true判断之后的对象,下面一个案例,可以看到怎么使用。

String[] dd = { "a", "b", "c" };
Stream<String> stream = Arrays.stream(dd);
stream.filter(str -> str.equals("a"))
        .forEach(System.out::println);//返回字符串为a的值

使用谓词过滤举例

留下偶数

Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens =
Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);

经过条件“被 2 整除”的 filter,剩下的数字为 {2, 4, 6}。

把单词挑出来

List<String> output = reader.lines().
 flatMap(line -> Stream.of(line.split(REGEXP))).
 filter(word -> word.length() > 0).
 collect(Collectors.toList());

这段代码首先把每行的单词用 flatMap 整理到新的 Stream,然后保留长度不为 0 的,就是整篇文章中的全部单词了。

过滤重复元素

distinct过来流中的重复元素,distinct定义如下:

Stream<T> distinct();

distinct使用的例子如下:

代码清单,对象类:

public class Emp {
    private String name;
    private Integer age;
    private Double salary;

    //省略构造函数、equals、hashCode、getter、setter方法
}

代码清单,测试类:

@RunWith(MockitoJUnitRunner.class)
public class Intermediate1Test {
    private List<Emp> list = new ArrayList<>();

    @Before
    public void init() {
        list.add(new Emp("YuanGong1", 20, 1000.0));
        list.add(new Emp("YuanGong1", 20, 1000.0));
        list.add(new Emp("YuanGong2", 25, 2000.0));
        list.add(new Emp("YuanGong3", 30, 3000.0));
        list.add(new Emp("YuanGong4", 35, 4000.0));
        list.add(new Emp("YuanGong5", 38, 5000.0));
        list.add(new Emp("YuanGong6", 45, 9000.0));
        list.add(new Emp("YuanGong7", 55, 10000.0));
        list.add(new Emp("YuanGong8", 42, 15000.0));
    }

    @Test
    public void testDistinct() {
        Arrays.asList(3, 1, 2, 1).stream().distinct().sorted().forEach(str -> System.out.println(str));

        list.stream().distinct().forEach(e -> System.out.println(e.getName()));
    }
}

上述对象类Emp中如果不添加equals方法和添加正确的equals方法,可以看出distinct的效果是不一样的。

跳过元素

skip(n)跳过Stream中前面的n个元素。注意limit(n)与skip(n)是互补的。下面的例子过滤出热量大于300卡路里的食物后,抛弃前两个元素,保留后面的结果流。

List<Dish> dishes = menu.stream()
    .filter(d -> d.getCalories() > 300)
    .skip(2)
    .collect(toList());

排序(sort)

Stream的sort有两个实现:

//对流中元素进行自然顺序排序,如果流中元素不可比较,会抛出异常
//这是有状态的中间操作(stateful intermediate operation)
Stream<T> sorted();

//根据提供的Comparator对流中元素进行排序
//这是有状态的中间操作(stateful intermediate operation)
Stream<T> sorted(Comparator<? super T> comparator);

排序的例子:

@Test
public void testSorted() {
    // 对list里的emp对象,取出薪水,并对薪水进行排序,然后输出薪水
    list.stream().map(emp -> emp.getSalary()).sorted()
            .forEach(salary -> System.out.println(salary));
    // 根据emp的年龄属性,进行排序
    list.stream().sorted(Comparator.comparing(Emp::getAge))
            .forEach(e -> System.out.println(e));
}

转换(Mapping)

一个常用的操作方式是从某个对象中摘取信息,比如从数据表中选取某个字段内容。Stream API提供了类似的设施:map和flatMap。当然,map和flatMap不但能够进行信息提取,也能进行对象信息转换。

为流中的每个元素使用函数(map)

map方法定义:

//无状态的中间操作
//param R:map操作后返回的新Stream的元素类型
//param mapper:应用到Stream中每个元素上的无状态的函数
//return:新的Stream
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

map方法接收一个函数类型的参数,这个函数应用到Stream中的每个元素上,产生一个新的元素。例如下面的例子返回每道菜的菜名。

private List<Dish> menu = Arrays.asList(
    new Dish("seasonal fruit", true, 120, Dish.Type.OTHER),
    new Dish("prawns", false, 300, Dish.Type.FISH),
    new Dish("rice", true, 350, Dish.Type.OTHER),
    new Dish("chicken", false, 400, Dish.Type.MEAT),
    new Dish("french fries", true, 530, Dish.Type.OTHER));
    
@Test
public void testMap1() {
    List<String> dishNames = menu.stream()
        .map(Dish::getName)
        .collect(Collectors.toList());
    dishNames.stream().forEach(d -> System.out.println(d));
}

上面的例子中,map函数中传递了一个方法引用,来取得菜名,最终通过收集器,将菜(Dish)的列表变成了菜名(String)的列表。 不知道看到方法引用时有没有疑惑?Dish::getName是一个无参返回String的函数,明显跟map的参数无法匹配,为什么这儿能够编译通过并且获得期望的运行效果呢? 答案在于方法引用只是lambda表达式的简写语法糖,并不是说方法引用中的方法类型需要匹配map的参数类型。上述的方法引用,实际上是以下形式的简写:

    List<String> dishNames = menu.stream()
        .map(d -> d.getName())
        .collect(Collectors.toList());

现在,lambda表达式d -> d.getName()是map的参数,这个lambda表达式是符合Function这个函数式接口的定义的。查阅下java源代码,可以看到这个接口的定义如下:

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

从上述源代码中可以看出Function接口中只有apply一个抽象方法,所以符合函数式接口的定义。我们随之可以得出结论:任何接受某个类型的单个参数,并返回某个类型的函数定义都是符合map方法的参数类型的函数定义。我们上面的例子中,函数接收一个Dish类型的参数,返回String类型的结果。 当然,map操作不仅仅像上述例子中采用类似SQL中投影操作,也可以有其他形式的转化操作,只要符合Function类型的定义即可。 下面的例子从Integer转化为String。

@Test
public void testMap3() {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
    List<String> words = numbers.stream()
        .map(i -> "word" + i)
        .collect(Collectors.toList());
    words.stream().forEach(wl -> System.out.println(wl));
}

很自然的我们就能想到,普通的企业应用中,使用map处理entity与DTO(VO)的转换也是没有问题的。

flatMap

map用于Stream中元素的一对一转换,如果Stream中的每个元素在转换过程中是1对多的关系并且最终需要将转换后的多个值放置于单一结果流中,就需要使用flatMap。 老规矩,看flatMap的方法签名:

//无状态的中间操作
//@param <R> :由旧的Stream的元素类型转换后的新Stream的元素类型
//@param mapper:Function函数接口,由输入参数T生成新值序列的Stream
//@return : (一个)新的Stream
 <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

跟map相同的是,flatMap方法的参数仍旧是Function这个函数式接口,并且Function都是接收T类型的参数,不同的是map的Function参数返回一个新的类型R,flatMap的Function参数返回一个R的Stream! 考虑这个例子,由{"Hello","World"}这个字符串列表生成字符列表{"H","e","l","l","o","W","o","r","l", "d"}。 我们的思路很自然的是将Hello拆分为{"H","e","l","l","o"},将World拆分为{"W","o","r","l", "d"},然后再合并起来。如果使用map方法,字符串到字符的拆分结果为两个字符数组,在类型匹配上遇到困境。

@Test
public void testFlatMap() {
    List<String> words = Arrays.asList("Hello", "World");
    //下面的cs变量只能是字符串数组的列表,否则出现编译错误
    List<String[]> cs = words.stream()
            .map(word -> word.split(""))
            .collect(Collectors.toList());
}
![](http://sptan-pic.oss-cn-shanghai.aliyuncs.com/javastream/flatMap1.jpg)
简单的时序图例子 使用flatMap,可以把Stream元素转化时产生的一对多的元素序列扁平化到一个Stream中,代码如下:
@Test
public void testFlatMap1() {
    List<String> words = Arrays.asList("Hello", "World");
    List<String> cs = words.stream()
            .flatMap(word -> Stream.of(word.split("")))
            .collect(Collectors.toList());
    cs.stream().forEach(c -> System.out.println(c));
}

核心的地方就是lambda表达式:word -> Stream.of(word.split(""))。word代表原始的输入字符串,这里是"Hello"和"World",word.split("")是普通的map方法转换结果,转换结果是每个单词拆分后的字符构成的字符序列,这个字符序列可以构造一个Stream,便成为了flatMap的参数构成部分。根据Stream的构造方式不同,以下的lambda表达式都可以替代上述的lambda,效果都是一样的。

//可替代方式
word -> Arrays.stream(word.split(""))

我们思考一下,只要符合flatMap定义的所有形式都是可以使用,所以我们甚至可以分步骤,先是使用map,转换出字符数组的列表,然后再对这个结果应用flatMap方法,如下:

@Test
public void testFlatMap1_3() {
    List<String> words = Arrays.asList("Hello", "World");
    List<String> cs = words.stream()
        .map(word -> word.split(""))
        .flatMap(wa -> Arrays.stream(wa))
        .collect(Collectors.toList());
    cs.stream().forEach(c -> System.out.println(c));
}

lambda表达式wa -> Arrays.stream(wa)可以替换为方法引用的形式:

Arrays::stream

flatMap方法对于一些初学者有些疑惑,通过上述的剖析,发现牢牢抓住函数签名,理解之后对于问题的解决思路就会开阔很多。

查找与匹配(Finding and Matching)

另一个常用操作是查找数据集中的元素,Stream通过提供allMatch, anyMatch, noneMatch, findFirst, and findAny这些方法对此功能提供支持。

匹配

希望确定流中的元素是否匹配Predicate时,Stream接口定义了anyMatch、allMatch与noneMatch,每种方法返回一个布尔值。 三个方法的签名如下:

/**
 * 短路终结操作.
 * Stream为空恒定返回true。
 */
boolean allMatch(Predicate<? super T> predicate);
/**
 * 短路终结操作.
 * Stream为空恒定返回true。
 */
boolean noneMatch(Predicate<? super T> predicate);
/**
 * 短路终结操作.
 * Stream为空恒定返回false。
 */
boolean anyMatch(Predicate<? super T> predicate);

注意:Stream为空时,上述几个方法不论Predicate为什么,返回值都是恒定的。 这几个方法用途不言自明。 下面我们举一个质数计算机的例子说明上述三个方法的使用。 大于或者等于2的自然数中,如果一个数无法1和该自然数之外的其他数整除,则这个数为质数(prime number), 否则为合数(composite number)。在数学上,一个简单的做法是遍历N能否能被从2到sqrt(N)之间的素数整除。 若不能则为素数。我们这里采取一种效率稍低但是简单的算法,不是采用从2到sqrt(N)之间的素数,而是采用从2到sqrt(N)之间的整数

public class Primes {
    /**
     * 判断一个整数是否为质数.
     *
     * @param num
     * @return
     */
    public boolean isPrime(int num) {
        int limit = (int) (Math.sqrt(num) + 1);
        return num == 2 ||
            num > 1 && IntStream.range(2, limit).noneMatch(divisor -> num % divisor == 0);
    }
}

上面我们借助noneMatch方法,质数校验易如反掌。 利用新做成的isPrime方法,下面是两种质数校验方案:

@Test
public void testIsPrimeUsingAllMatch() {
    Assert.assertTrue(IntStream.of(2, 3, 5, 7, 11, 13, 17, 19)
        .allMatch(calculator::isPrime));
}

@Test
public void testIsPrimeWithComposites() {
    Assert.assertFalse(Stream.of(4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20)
        .anyMatch(calculator::isPrime));
}

第一个测试调用已知质数流中的allMatch方法,仅当所有值均为质数时返回true。 第二个测试对一个合数集合使用anyMatch,认定集合中的所有数字均不满足谓词。 特别注意上述三个方法在处理空集合时返回值忽略谓词结果,可能不太直观。 下面的测试用例均能通过,需要特别注意。

@Test
public void testEmptyStream() {
    Assert.assertTrue(Stream.empty().allMatch(e -> false));
    Assert.assertTrue(Stream.empty().allMatch(e -> true));
    Assert.assertTrue(Stream.empty().noneMatch(e -> false));
    Assert.assertTrue(Stream.empty().noneMatch(e -> true));
    Assert.assertTrue(Stream.empty().allMatch(e -> false));
    Assert.assertTrue(Stream.empty().allMatch(e -> true));
    Assert.assertFalse(Stream.empty().anyMatch(e -> false));
    Assert.assertFalse(Stream.empty().anyMatch(e -> true));
}

查找元素

Stream接口定义的findFirst方法返回流中第一个符合条件的元素的Optional,而findAny方法返回流中某个元素的Optional。两个方法都没有参数,它们的函数定义非常简单。

//短路终结操作
Optional<T> findFirst();
//短路终结操作
Optional<T> findAny();

下面的例子查找流中的第一个偶数:

@Test
public void testFindFirst() {
    Optional<Integer> firstEven = Stream.of(3, 1, 4, 1, 5, 9, 8, 7, 6)
        .filter(n -> n % 2 == 0)
        .findFirst();
    System.out.println(firstEven); //print Optional[4]
}

如果流为空或者查找不到匹配元素,会返回空的Optional,如下例所示:

@Test
public void testFindFirstNG() {
    Optional<Integer> firstEvenNG = Stream.of(3, 1, 4, 1, 5, 9, 8, 7, 6)
        .filter(n -> n > 10)
        .filter(n -> n % 2 == 0)
        .findFirst();
    System.out.println(firstEvenNG); //print Optional.empty
}

我们这里采用的流存在出现顺序,因此无论采用顺序流还是并行流搜索,第一个偶数始终是4。

@Test
public void testFindFirstParallel() {
    Optional<Integer> firstEven = Stream.of(3, 1, 4, 1, 5, 9, 8, 7, 6)
        .parallel()
        .filter(n -> n % 2 == 0)
        .findFirst();
    System.out.println(firstEvenNG); //总是返回 Optional[4]
}

规约(reduce)

这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于

Integer sum = integers.reduce(0, (a, b) -> a+b); 或

Integer sum = integers.reduce(0, Integer::sum);

也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。 reduce 的用例:

// 字符串连接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat); 
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); 
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 无起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 过滤,字符串连接,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F").
 filter(x -> x.compareTo("Z") > 0).
 reduce("", String::concat);

上面代码例如第一个示例的 reduce(),第一个参数(空白字符)即为起始值,第二个参数(String::concat)为 BinaryOperator。这类有起始值的 reduce() 都返回具体的对象。而对于第四个示例没有起始值的 reduce(),由于可能没有足够的元素,返回的是 Optional,请留意这个区别。

求和

最大值与最小值

min和max属于终结操作(相对于中间操作),是规约(reduce)的一种特殊形式。先看定义:

/*
 * 根据参数中的比较器比较Stream中元素,返回最小的元素.
 * @param comparator:函数式接口java.util.Comparator,用于比较Stream中的元素.
 * @return: Optional封装的最小值,注意Stream中为空时实际不存在最小值,防止返回null使用了Optional封装
 */
Optional<T> min(Comparator<? super T> comparator);

/*
 * 根据参数中的比较器比较Stream中元素,返回最大的元素.
 * @param comparator:函数式接口java.util.Comparator,用于比较Stream中的元素.
 * @return: Optional封装的最大值,注意Stream中为空时实际不存在最大值,防止返回null使用了Optional封装
 */
Optional<T> max(Comparator<? super T> comparator);

使用也非常简单,下面是例子:

@Test
public void testMinMax1() {
    List<String> strs = Arrays.asList("d", "b", "a", "c", "e");
    Optional<String> min = strs.stream().min(Comparator.comparing(Function.identity()));
    Optional<String> max = strs.stream().max(Comparator.naturalOrder());
    System.out.println(String.format("min:%s; max:%s", min.get(), max.get()));// min:a; max:e
}

min的含义不言自明,倒是Comparator.comparing(Function.identity())需要解释一下。分开来看,Comparator.comparing的定义如下:

//位于类java.util.Comparator,是java8新增的方法
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor);

注意Comparator.comparing的参数是一个Function,我们上述例子中的比较对象是字符串本身,所以向Comparator.comparing的参数传递这个lambda表达式是没有问题的:

t -> t

这个表达式很常用,所以java8在Function接口中提供了一个默认实现:

//这是java8 JDK中java.util.function.Function的源代码
static <T> Function<T, T> identity() {
    return t -> t;
}

这种函数式表达方式有点别扭,原因是因为把函数作为参数,而不是把普通的对象作为参数。 我们换个方式,不是拿字符串本身进行比较,而是使用字符串长度作为比较对象,感受一下比较器的用法。

@Test
public void testMinMax1_1() {
    List<String> strs = Arrays.asList("abc", "a", "ab", "abcd", "bb");
    Optional<String> min = strs.stream().min(Comparator.comparing(String::length));
    Optional<String> max = strs.stream().max(Comparator.comparing(String::length));
    System.out.println(String.format("min:%s; max:%s", min.get(), max.get()));// min:a; max:abcd
}

数值流

出于性能考虑,对应常用的基本类型int,long,double,java Stream提供了处理这些基本类型的子接口,分别为IntStream, LongStream, DoubleStream。

基本数值流转换为对象数值流

由于类型不兼容,以下代码是无法通过编译的:

IntStream.of(1,2,3,4).collect(Collectors.toList());

三种解决方案可以实现int流到Integer流。

使用boxed方法

第一种方案利用boxed方法,将IntStream转换为Stream。

public void testBoxed() {
    List<Integer> ints = IntStream.of(1, 2, 3, 4)
        .boxed()
        .collect(Collectors.toList());
}

利用mapToObj方法

第二种方案利用mapToObj方法,将基本类型流中的每个元素转换为包装类的一个实例。

@Test
public void testMapToObj() {
    List<Integer> ints = IntStream.of(1, 2, 3, 4)
        .mapToObj(Integer::valueOf)
        .collect(Collectors.toList());
}

与之相对的是mapToInt、mapToLong、mapToDouble方法,将对象流解析为与之相关的基本类型流。

采用collect方法的三参数形式

collect的三参数形式签名如下:

/**
 * 这个方法的执行效果相当于以下代码,combiner只在并行处理时合并结果使用.
 *     R result = supplier.get();
 *     for (int element : this stream)
 *         accumulator.accept(result, element);
 *     return result;
 * @param supplier: 创建新结果容器的函数
 * @param accumulator: 将元素添加到新创建的容器中的累加器函数
 * @param combiner: 并行操作时合并结果的合并函数
 * @return 规约操作的结果
 */
<R> R collect(Supplier<R> supplier,
              ObjIntConsumer<R> accumulator,
              BiConsumer<R, R> combiner);

使用collect方法的三参数形式将基本类转为包装类:

@Test
public void testCollect() {
    List<Integer> ints = IntStream.of(1, 2, 3, 4)
        .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}

如例所示,Supplier是ArrayList的构造函数;累加器(accumulator)为add方法,表示如何为列表添加单个元素;并行操作中使用的组合器(combiner)是addAll方法,它能将两个列表合二为一。

对象数值流转换为基本数值流

前面提到过,mapToInt, mapToLong和mapToDouble将对象流解析为相对应的基本类型流。

流的拼接

流的拼接有三种方案。

使用Stream.concat

Stream.concat签名如下:

//
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

concat方法创建一个惰性拼接流(lazily concatenated stream),其元素是第一个流的所有元素,后跟第二个流的所有元素。如果两个输入流均为有序流,则生成的流也是有序流,如果任一个流为并行流,则生成的流为并行流。关闭生成的流也会关闭两个输入流。 如果拼接三个流,可以嵌套使用concat。

@Test
public void testConcatThree() {
    Stream<String> first = Stream.of("a", "b", "c");
    Stream<String> second = Stream.of("X", "Y", "Z");
    Stream<String> third = Stream.of("alpha", "beta", "gamma");
    List<String> strs = Stream.concat(Stream.concat(first, second), third)
        .collect(Collectors.toList());
    List<String> expectedStrs = Arrays.asList("a", "b", "c", "X", "Y", "Z", "alpha", "beta", "gamma");
    Assert.assertEquals(expectedStrs, strs);
}

concat方法构建了流的二叉树,拼接过多容易造成堆栈溢出(抛出StackOverflowException异常)。

使用Stream.reduce

组合使用reduce和concat方法,同样可以执行多次拼接。

@Test
public void testConcatReduce() {
    Stream<String> first = Stream.of("a", "b", "c");
    Stream<String> second = Stream.of("X", "Y", "Z");
    Stream<String> third = Stream.of("alpha", "beta", "gamma");
    Stream<String> fourth = Stream.empty();
    List<String> strs = Stream.of(first, second, third, fourth)
        .reduce(Stream.empty(), Stream::concat)
        .collect(Collectors.toList());
    List<String> expectedStrs = Arrays.asList("a", "b", "c", "X", "Y", "Z", "alpha", "beta", "gamma");
    Assert.assertEquals(expectedStrs, strs);
}

代码稍微简洁了些,但是无法解决栈溢出问题。

使用Stream.flatMap

多个流拼接时,flatMap是更好的解决方案。

@Test
public void testFlatMap() {
    Stream<String> first = Stream.of("a", "b", "c").parallel();
    Stream<String> second = Stream.of("X", "Y", "Z");
    Stream<String> third = Stream.of("alpha", "beta", "gamma");
    Stream<String> fourth = Stream.empty();
    Stream<String> total = Stream.of(first, second, third, fourth)
        .flatMap(Function.identity());
    Assert.assertFalse(total.isParallel());
    List<String> strs = total.collect(Collectors.toList());
    List<String> expectedStrs = Arrays.asList("a", "b", "c", "X", "Y", "Z", "alpha", "beta", "gamma");
    Assert.assertEquals(expectedStrs, strs);
}

上述代码也同样完成了流的拼接,并且栈溢出的可能性更小。但是仍旧有缺点:flatMap返回的流不是并行流!这一点可以在流关闭前通过parallel方法对流进行转化。

total = total.parallel();

此外,流的操作中一大块内容是关于流的收集,内容比较多,另外单独讲解。