Java8 Stream

概述

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。

Stream 的另外一大特点是,数据源本身可以是无限的。

特点:

  1. 不是数据结构,不会保存数据。

  2. 不会修改原来的数据源,它会将操作后的数据保存到另外一个对象中。(保留意见:毕竟peek方法可以修改流中元素)

  3. 惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行终止操作的时候才会进行实际的计算。

  4. 流的操作并不是一个一个链式的执行的。而是先拿出来一个元素,执行所有的操作。执行完毕之后,再拿出来一个元素进行下一次操作。

使用步骤:

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→ 数据转换 → 执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

  1. 创建Stream。
  2. 中间操作:通过一系列中间(Intermediate)方法,对数据集进行过滤、检索等数据集的再次处理。
  3. 终端操作:通过最终(terminal)方法完成对数据集中元素的处理。

流的操作类型:

对流的操作分为两种:

  1. Intermediate(中间操作):一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。

  2. Terminal(终端操作、终止操作):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果(新的集合或值),或者一个副作用(side effect)。

在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。

还有一种操作被称为 short-circuiting 。用以指:

  • 对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
  • 对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。

当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。

常见的操作可以归类如下。

  1. Intermediate:

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  1. Terminal:

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

  1. Short-circuiting:

anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

创建 Stream

从 Collection 和数组

Collection.stream()

List<String> list = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Stream<String> stream = list.stream();

Collection.parallelStream()

List<String> list = Arrays.asList("a", "b", "c");
// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();

如果流中的数据量足够大,并行流可以加快处速度。

除了直接创建并行流,还可以通过parallel()把顺序流转换成并行流。

Optional<Integer> findFirst = list.stream().parallel().filter(x->x>6).findFirst();

Arrays.stream(T array)

int[] array = {1,3,5,6,8};
IntStream stream = Arrays.stream(array);

从Stream接口的静态工厂方法

Stream.of()

of方法,其生成的Stream是有限长度的,Stream的长度为其内的元素个数。

public static<T> Stream<T> of(T... values) {
    return Arrays.stream(values);
}

public static<T> Stream<T> of(T t) {
    return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<String> stringStream = Stream.of("A");

Stream.generate()

generator方法,返回一个无限长度的Stream,其元素由Supplier接口的提供。在Supplier是一个函数接口,只封装了一个get()方法,其用来返回任何泛型的值,该结果在不同的时间内,返回的可能相同也可能不相同,没有特殊的要求。

public static<T> Stream<T> generate(Supplier<T> s) {
    Objects.requireNonNull(s);
    return StreamSupport.stream(
        new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
}
  1. 这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。
  2. 把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。
Stream<Double> generateA = Stream.generate(new Supplier<Double>() {
    @Override
    public Double get() {
        return java.lang.Math.random();
    }
});

Stream<Double> generateB = Stream.generate(()-> java.lang.Math.random());
Stream<Double> generateC = Stream.generate(java.lang.Math::random);

以上三种形式达到的效果是一样的,只不过是下面的两个采用了Lambda表达式,简化了代码,其实际效果就是返回一个随机值。一般无限长度的Stream会与filter、limit等配合使用,否则Stream会无限制的执行下去,后果可想而知,如果你有兴趣,不妨试一下。

Stream.iterate()

iterate方法,其返回的也是一个无限长度的Stream,与generate方法不同的是,其是通过函数f迭代对给指定的元素种子而产生无限连续有序Stream,其中包含的元素可以认为是:seed,f(seed), f(f(seed))无限循环。

public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
    Objects.requireNonNull(f);
    final Iterator<T> iterator = new Iterator<T>() {
        @SuppressWarnings("unchecked")
        T t = (T) Streams.NONE;

        @Override
        public boolean hasNext() {
            return true;
        }

        @Override
        public T next() {
            return t = (t == Streams.NONE) ? seed : f.apply(t);
        }
    };
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
        iterator,
        Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
}
Stream.iterate(1, item -> item + 1)
        .limit(10)
        .forEach(System.out::println); 
        // 打印结果:1,2,3,4,5,6,7,8,9,10

上面示例,种子为1,也可认为该Stream的第一个元素,通过f函数来产生第二个元素。接着,第二个元素,作为产生第三个元素的种子,从而产生了第三个元素,以此类推下去。需要主要的是,该Stream也是无限长度的,应该使用filter、limit等来截取Stream,否则会一直循环下去。

Stream.empty()

empty方法返回一个空的顺序Stream,该Stream里面不包含元素项。

public static<T> Stream<T> empty() {
    return StreamSupport.stream(Spliterators.<T>emptySpliterator(), false);
}

从 BufferedReader

BufferedReader.lines()

使用 BufferedReader.lines() 方法,将每行内容转成流。

BufferedReader reader = new BufferedReader(new FileReader("F:\\test_stream.txt"));
Stream<String> lineStream = reader.lines();
lineStream.forEach(System.out::println);

其他

  • Random.ints()
  • BitSet.stream()
  • Pattern.splitAsStream(java.lang.CharSequence)
  • JarFile.stream()

Pattern.splitAsStream()

使用 Pattern.splitAsStream() 方法,将字符串分隔成流。

Pattern pattern = Pattern.compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println)
// 打印结果:a  b  c  d

中间操作

筛选与切片

filter

filter:过滤流中的某些元素。filter 对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。

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

eg: 留下偶数

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}。

limit

limit(n):获取n个元素。limit 返回 Stream 的前面 n 个元素。

Stream<T> limit(long maxSize);
Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);
Stream<Integer> newStream = stream.limit(4);
newStream.forEach(System.out::println);
// 打印结果:6  4  6  7

skip

skip(n):跳过n元素,skip 则是扔掉前 n 个元素,配合limit(n)可实现分页。

skip方法将过滤掉原Stream中的前N个元素,返回剩下的元素所组成的新Stream。如果原Stream的元素个数大于N,将返回原Stream的后(原Stream长度-N)个元素所组成的新Stream;如果原Stream的元素个数小于或等于N,将返回一个空Stream。

Stream<T> skip(long n);
Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);
Stream<Integer> newStream = stream.skip(4);
newStream.forEach(System.out::println);
// 打印结果:3  9  8  10  12  14  14
skip 和 limit 结合使用
Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);
Stream<Integer> newStream = stream.limit(7).skip(4);
newStream.forEach(System.out::println);
// 打印结果:3  9  8
Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);
Stream<Integer> newStream = stream.skip(4).limit(2);
newStream.forEach(System.out::println);
// 打印结果:3  9
skip 和 limit 实现分页
list = list.stream().skip((pageNo - 1) * pageSize).limit(pageSize).collect(Collectors.toList())

distinct

distinct:通过流中元素的 hashCode() 和 equals() 去除重复元素。

Stream<T> distinct();
Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);
Stream<Integer> newStream = stream.distinct();
newStream.forEach(System.out::println);
// 打印结果:6  4  7  3  9  8  10  12  14

映射

映射,可以将一个流的元素按照一定的映射规则映射到另一个流中。分为mapflatMap

map

map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

map方法将对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。为了提高处理效率,官方已封装好了,三种变形:mapToDouble,mapToInt,mapToLong。其实很好理解,如果想将原Stream中的数据类型,转换为double, int或者是long是可以调用相对应的方法。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

IntStream mapToInt(ToIntFunction<? super T> mapper);

LongStream mapToLong(ToLongFunction<? super T> mapper);

转换大写

String[] strArr = { "abcd", "bcdd", "defde", "fTr" };
List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList());
// 打印结果:[ABCD, BCDD, DEFDE, FTR]

平方数

List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream().map(n -> n * n).collect(Collectors.toList());
// 打印结果:[1, 4, 9, 16]

flatMap

flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

map 生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要 flatMap。

flatMap方法与map方法类似,都是将原Stream中的每一个元素通过转换函数转换,不同的是,该换转函数的对象是一个Stream,也不会再创建一个新的Stream,而是将原Stream的元素取代为转换的Stream。如果转换函数生产的Stream为null,应由空Stream取代。flatMap有三个对于原始类型的变种方法,分别是:flatMapToInt,flatMapToLong和flatMapToDouble。

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);

IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);

LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);

eg1:

Stream<List<Integer>> inputStream = Stream.of(
    Arrays.asList(1),
    Arrays.asList(2, 3),
    Arrays.asList(4, 5, 6)
);
Stream<Integer> outputStream = inputStream.flatMap((childList) -> childList.stream());
outputStream.forEach(System.out::println);
// 打印结果:1  2  3  4  5  6

flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。

eg2:

List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Stream<Integer> outputStream = nums.stream().flatMap(n -> Stream.of(n, 6*n));
outputStream.forEach(System.out::println);
// 打印结果:1  6  2  12  3  18  4  24

排序

sorted

对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。

sorted():自然排序,流中元素需实现Comparable接口

Stream<T> sorted();
// 自然正序
Stream.of(5, 4, 3, 2, 1).sorted().forEach(System.out::println);
// 打印结果:1  2  3  4  5

Arrays.asList("aa", "ff", "dd").stream().sorted().forEach(System.out::println);
// 打印结果:aa  dd  ff

// 自然逆序
Stream.of(5, 4, 3, 2, 1).sorted(Comparator.reverseOrder()).forEach(System.out::println);
// 打印结果:5  4  3  2  1

Arrays.asList("aa", "ff", "dd").stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);
// 打印结果:ff  dd  aa

sorted(Comparator com):定制排序,自定义Comparator排序器

Stream<T> sorted(Comparator<? super T> comparator);
// 定制排序,正序
list.stream().sorted(Comparator.comparing(User::getAge)).forEach(System.out::println);

// 定制排序,逆序(注意两种写法对比)
list.stream().sorted(Comparator.comparing(User::getAge).reversed()).forEach(System.out::println); //先以属性升序, 结果进行属性降序

list.stream().sorted(Comparator.comparing(User::getAge, Comparator.reverseOrder())).forEach(System.out::println); //以属性降序

按多字段排序

注意以下写法对比

list.stream().sorted(Comparator.comparing(User::getAge).reversed().thenComparing(User::getId).reversed()).forEach(System.out::println);//先以属性一升序,升序结果进行属性一降序,再进行属性二升序,结果进行属性一降序属性二降序,最终结果是属性一升序、属性二降序,并非想要的属性一降序、属性二降序。

list.stream().sorted(Comparator.comparing(User::getAge).reversed().thenComparing(User::getId, Comparator.reverseOrder())).forEach(System.out::println);//先以属性一升序,升序结果进行属性一降序,再进行属性二降序

list.stream().sorted(Comparator.comparing(User::getAge, Comparator.reverseOrder()).thenComparing(User::getId, Comparator.reverseOrder())).forEach(System.out::println); //先以属性一降序,再进行属性二降序

通过以上例子我们可以发现

  1. Comparator.comparing(类::属性一).reversed();

  2. Comparator.comparing(类::属性一, Comparator.reverseOrder());

两种排序是完全不一样的,一定要区分开来。 ① 是得到排序结果后再排序,② 是直接进行排序

很多人会混淆导致理解出错,② 更好理解,建议使用 ②

消费

peek

peek:如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,没有返回值。

Stream<T> peek(Consumer<? super T> action);

peek主要被用在debug用途。

Stream.of("one", "two", "three","four").filter(e -> e.length() > 3)
                .peek(e -> System.out.println("Filtered value: " + e))
                .map(String::toUpperCase)
                .peek(e -> System.out.println("Mapped value: " + e))
                .collect(Collectors.toList());

上面的例子输出:

Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR

上面的例子我们输出了stream的中间值,方便我们的调试。

终端操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。

查找与匹配

allMatch

检查是否匹配所有元素,接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false。

boolean allMatch(Predicate<? super T> predicate);
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
boolean b1 = nums.stream().allMatch(e -> e < 5); //true
boolean b2 = nums.stream().allMatch(e -> e < 3); //false

noneMatch

检查是否没有匹配所有元素,接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false。

boolean noneMatch(Predicate<? super T> predicate);
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
boolean b1 = nums.stream().noneMatch(e -> e < 5); //false
boolean b2 = nums.stream().noneMatch(e -> e < 3); //false

anyMatch

检查是否至少匹配一个元素,接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false。

boolean anyMatch(Predicate<? super T> predicate);
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
boolean b1 = nums.stream().anyMatch(e -> e < 5); //true
boolean b2 = nums.stream().anyMatch(e -> e < 3); //true

findFirst

返回流中第一个元素。

Optional<T> findFirst();
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> first = nums.stream().findFirst();
System.out.println(first.orElse(null)); // 1

Object obj = Arrays.asList().stream().findFirst().orElse(null);
System.out.println(obj); // null

findAny

返回流中的任意元素。

该方法可以获取Stream中的任意一个元素。大多数时候看起来像是findAny()总是返回第一个元素?但是它并不保证每次返回的都是第一个元素,尤其是在使用并发流的情况下,总之,findAny()方法可能返回Stream中的任意一个元素。

Optional<T> findAny();
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> first = nums.stream().findAny();
System.out.println(first.orElse(null));

Object obj = Arrays.asList().stream().findAny().orElse(null);
System.out.println(obj); // null

count

返回流中元素的总个数。

long count();
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
long count = nums.stream().count();
System.out.println(count); // 4

max

返回流中元素最大值。

Optional<T> max(Comparator<? super T> comparator);
// 自然排序
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().max(Integer::compareTo);
System.out.println(max.orElse(null)); // 4

// 定制排序
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().max(new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
});
System.out.println(max.orElse(null)); // 4

List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().max((n1, n2) -> n1.compareTo(n2));
System.out.println(max.orElse(null)); // 4

min

返回流中元素最小值。

Optional<T> min(Comparator<? super T> comparator);
// 自然排序
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().min(Integer::compareTo);
System.out.println(max.orElse(null)); // 1

// 定制排序
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().min(new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
});
System.out.println(max.orElse(null)); // 1

List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().min((n1, n2) -> n1.compareTo(n2));
System.out.println(max.orElse(null)); // 1

forEach

内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了)

void forEach(Consumer<? super T> action);

void forEachOrdered(Consumer<? super T> action);

forEachOrdered 将按流源指定的顺序处理流元素,而不管流是顺序的还是并行的。

List<Integer> nums = Arrays.asList(1, 2, 3, 4);
nums.stream().forEach(System.out::println);
外部迭代

最传统的方法是用Iterator,当然还以用for i、增强for循环等等。这一类方法叫做外部迭代,意为显式地进行迭代操作,即集合中的元素访问是由一个处于集合外部的东西来控制的,在这里控制着循环的东西就是迭代器。

简单理解外部迭代就是由用户来决定 “做什么”和“怎么做”的操作。比如 “做什么”(把大写转成小写)与“怎么做”(通过 Iterator 遍历)是由用户来决定的。

内部迭代

顾名思义,这种方式的遍历将在集合内部进行,我们不会显式地去控制这个循环。无需关心遍历元素的顺序,我们只需要定义对其中每一个元素进行什么样的操作。注意在这种设定下可能无法直接获取到当前元素的下标。

内部迭代我们只需要提供 “做什么”,把“怎么做”的任务交给了 JVM

外部循环的代码非常直接,但它有如下问题:

  1. Java 的 for 循环是串行的,而且必须按照集合中元素的顺序进行依次处理;

  2. 集合框架无法对控制流进行优化,例如通过排序、并行、短路(short-circuiting)求值以及惰性求值改善性能。

尽管有时 for-each 循环的这些特性(串行,依次)是我们所期待的,但它对改善性能造成了阻碍。实际上,我们可以使用内部迭替代外部迭代,用户把对迭代的控制权交给类库,并向类库传递迭代时所需执行的代码。用户把对操作的控制权交还给类库,从而允许类库进行各种各样的优化(例如乱序执行、惰性求值和并行等等)。总的来说,内部迭代使得外部迭代中不可能实现的优化成为可能。

归约

reduce

归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。

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

Optional<T> reduce(BinaryOperator<T> accumulator);

T reduce(T identity, BinaryOperator<T> accumulator);

<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

我们先看第一个形式,Optional<T> reduce(BinaryOperator<T> accumulator); 其接受一个函数接口 BinaryOperator,而这个接口又继承于BiFunction<T, T, T>,BinaryOperator接口,可以看到reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。要注意的是:第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素。这个方法返回值类型是Optional。

List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional accResult = nums.stream().reduce((acc, item) -> {
    System.out.println("acc : "  + acc);
    acc += item;
    System.out.println("item: " + item);
    System.out.println("acc+ : "  + acc);
    System.out.println("--------");
    return acc;
});
System.out.println("accResult: " + accResult.orElse(null));

上述例子将输出:

acc : 1
item: 2
acc+ : 3
--------
acc : 3
item: 3
acc+ : 6
--------
acc : 6
item: 4
acc+ : 10
--------
accResult: 10

第二个形式,T reduce(T identity, BinaryOperator<T> accumulator); 与第一种形式相同的是都会接受一个BinaryOperator函数接口,不同的是其会接受一个identity参数,用来指定Stream循环的初始值。如果Stream为空,就直接返回该值。另一方面,该方法不会返回Optional,因为该方法不会出现null。

List<Integer> nums = Arrays.asList(1, 2, 3, 4);
int  accResult = nums.stream().reduce(0, (acc, item) -> {
    System.out.println("acc : "  + acc);
    acc += item;
    System.out.println("item: " + item);
    System.out.println("acc+ : "  + acc);
    System.out.println("--------");
    return acc;
});
System.out.println("accResult: " + accResult);

上述例子将输出:

acc : 0
item: 1
acc+ : 1
--------
acc : 1
item: 2
acc+ : 3
--------
acc : 3
item: 3
acc+ : 6
--------
acc : 6
item: 4
acc+ : 10
--------
accResult: 10

第三个形式, <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner); 在串行流(stream)中,该方法跟第二个方法一样,即第三个参数combiner不会起作用。在并行流(parallelStream)中,我们知道流被fork join出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity,accumulator)一样,而第三个参数combiner函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)流程进行归约。

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24);

Integer v2 = list.stream().reduce(0,
                                  (x1, x2) -> {
                                      System.out.println("stream accumulator: x1:" + x1 + "  x2:" + x2);
                                      return x1 - x2;
                                  },
                                  (x1, x2) -> {
                                      System.out.println("stream combiner: x1:" + x1 + "  x2:" + x2);
                                      return x1 * x2;
                                  });
System.out.println(v2); // -300

Integer v3 = list.parallelStream().reduce(0,
                                          (x1, x2) -> {
                                              System.out.println("parallelStream accumulator: x1:" + x1 + "  x2:" + x2);
                                              return x1 - x2;
                                          },
                                          (x1, x2) -> {
                                              System.out.println("parallelStream combiner: x1:" + x1 + "  x2:" + x2);
                                              return x1 * x2;
                                          });

收集

collect

将流转换为其他形式。接收一个 Collector接口的 实现,用于给Stream中元素做汇总的方法。

<R, A> R collect(Collector<? super T, A, R> collector);

Collector 接口中方法的实现决定了如何对流执行收集操作(如收 集到 List、Set、Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例。具体方法如下:

归集(toList、toSet、toMap)

因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toListtoSettoMap比较常用,另外还有toCollectiontoConcurrentMap等复杂一些的用法。

下面用一个案例演示toListtoSettoMap

List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20);
List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList());
Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet());

List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));

Map<?, Person> map = personList.stream().filter(p -> p.getSalary() > 8000)
    .collect(Collectors.toMap(Person::getName, p -> p));
System.out.println("toList:" + listNew);
System.out.println("toSet:" + set);
System.out.println("toMap:" + map);

运行结果:

toList:[6, 4, 6, 6, 20]
toSet:[4, 20, 6]
toMap:{Tom=mutest.Person@5fd0d5ae, Anni=mutest.Person@2d98a335}

统计

Collectors 提供了一系列用于数据统计的静态方法:

  • 计数:counting
  • 平均值:averagingInt、averagingLong、averagingDouble
  • 最值:maxBy、minBy
  • 求和:summingInt、summingLong、summingDouble
  • 统计以上所有:summarizingInt、summarizingLong、summarizingDouble
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));

// 求总数
Long count = personList.stream().collect(Collectors.counting());
// 求平均工资
Double average = personList.stream().collect(Collectors.averagingDouble(Person::getSalary));
// 求最高工资
Optional<Integer> max = personList.stream().map(Person::getSalary).collect(Collectors.maxBy(Integer::compare));
// 求工资之和
Integer sum = personList.stream().collect(Collectors.summingInt(Person::getSalary));
// 一次性统计所有信息
DoubleSummaryStatistics collect = personList.stream().collect(Collectors.summarizingDouble(Person::getSalary));

System.out.println("员工总数:" + count);
System.out.println("员工平均工资:" + average);
System.out.println("员工工资总和:" + sum);
System.out.println("员工工资所有统计:" + collect);

运行结果

员工总数:3
员工平均工资:7900.0
员工工资总和:23700
员工工资所有统计:DoubleSummaryStatistics{count=3, sum=23700.000000, min=7000.000000, average=7900.000000, max=8900.000000}

分组(partitioningBy、groupingBy)

分区 partitioningBy

collect的一个常用操作将Stream分解成两个集合。假如一个数字的Stream,我们可能希望将其分割成两个集合,一个是偶数集合,另外一个是奇数集合。我们首先想到的就是过滤操作,通过两次过滤操作,很简单的就完成了我们的需求。

但是这样操作起来有问题。首先,为了执行两次过滤操作,需要有两个流。其次,如果过滤操作复杂,每个流上都要执行这样的操作, 代码也会变得冗余。

这里我们就不得不说Collectors库中的partitioningBy方法,它接受一个流,并将其分成两部分:使用Predicate对象,指定条件并判断一个元素应该属于哪个部分,并根据布尔值返回一个Map到列表。因此对于key为true所对应的List中的元素,满足Predicate对象中指定的条件;同样,key为false所对应的List中的元素,不满足Predicate对象中指定的条件。

public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
    return partitioningBy(predicate, toList());
}

eg: 使用partitioningBy,我们就可以将数字的Stream分解成奇数集合和偶数集合了。

Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4, 5, 6)
    	.collect(Collectors.partitioningBy(it -> it % 2 == 0));
System.out.println("collectParti : " + collectParti);
// 结果: collectParti : {false=[1, 3, 5], true=[2, 4, 6]}
分组 groupingBy

数据分组是一种更自然的分割数据操作, 与将数据分成true和false两部分不同,可以使用任意值对数据分组。

调用Stream的collect方法,传入一个收集器,groupingBy接受一个分类函数,用来对数据分组,就像partitioningBy一样,接受一个
Predicate对象将数据分成true和false两部分。我们使用的分类器是一个Function对象,和map操作用到的一样。

public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier) {
    return groupingBy(classifier, toList());
}

public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) {
    return groupingBy(classifier, HashMap::new, downstream);
}
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, "male", "New York"));
personList.add(new Person("Jack", 7000, "male", "Washington"));
personList.add(new Person("Lily", 7800, "female", "Washington"));
personList.add(new Person("Anni", 8200, "female", "New York"));
personList.add(new Person("Owen", 9500, "male", "New York"));
personList.add(new Person("Alisa", 7900, "female", "New York"));

// 将员工按薪资是否高于8000分组
Map<Boolean, List<Person>> part = personList.stream().collect(Collectors.partitioningBy(x -> x.getSalary() > 8000));
Map<Boolean, List<Person>> group = personList.stream().collect(Collectors.groupingBy(x -> x.getSalary() > 8000));
// 将员工按性别分组
Map<String, List<Person>> group2 = personList.stream().collect(Collectors.groupingBy(Person::getSex));
// 将员工先按性别分组,再按地区分组
Map<String, Map<String, List<Person>>> group3 = personList.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea)));

System.out.println("员工按薪资是否大于8000分区情况:" + part);
System.out.println("员工按薪资是否大于8000分组情况:" + group);
System.out.println("员工按性别分组情况:" + group2);
System.out.println("员工按性别、地区:" + group3);
员工按薪资是否大于8000分区情况:{
	false=[Person(name=Jack, salary=7000, age=null, sex=male, area=Washington), Person(name=Lily, salary=7800, age=null, sex=female, area=Washington), Person(name=Alisa, salary=7900, age=null, sex=female, area=New York)], 
	true=[Person(name=Tom, salary=8900, age=null, sex=male, area=New York), Person(name=Anni, salary=8200, age=null, sex=female, area=New York), Person(name=Owen, salary=9500, age=null, sex=male, area=New York)]
}

员工按薪资是否大于8000分组情况:{
	false=[Person(name=Jack, salary=7000, age=null, sex=male, area=Washington), Person(name=Lily, salary=7800, age=null, sex=female, area=Washington), Person(name=Alisa, salary=7900, age=null, sex=female, area=New York)], 
	true=[Person(name=Tom, salary=8900, age=null, sex=male, area=New York), Person(name=Anni, salary=8200, age=null, sex=female, area=New York), Person(name=Owen, salary=9500, age=null, sex=male, area=New York)]
}

员工按性别分组情况:{
	female=[Person(name=Lily, salary=7800, age=null, sex=female, area=Washington), Person(name=Anni, salary=8200, age=null, sex=female, area=New York), Person(name=Alisa, salary=7900, age=null, sex=female, area=New York)], 
	male=[Person(name=Tom, salary=8900, age=null, sex=male, area=New York), Person(name=Jack, salary=7000, age=null, sex=male, area=Washington), Person(name=Owen, salary=9500, age=null, sex=male, area=New York)]
}

员工按性别、地区:{
	female={
		New York=[Person(name=Anni, salary=8200, age=null, sex=female, area=New York), Person(name=Alisa, salary=7900, age=null, sex=female, area=New York)], 
		Washington=[Person(name=Lily, salary=7800, age=null, sex=female, area=Washington)]
	}, 
	male={
		New York=[Person(name=Tom, salary=8900, age=null, sex=male, area=New York), Person(name=Owen, salary=9500, age=null, sex=male, area=New York)], 
		Washington=[Person(name=Jack, salary=7000, age=null, sex=male, area=Washington)]
	}
}

接合(joining)

joining 可以将stream中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串。

public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
    return joining(delimiter, "", "");
}

public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
              CharSequence prefix, CharSequence suffix) {
    return new CollectorImpl<>(
        () -> new StringJoiner(delimiter, prefix, suffix),
        StringJoiner::add, StringJoiner::merge,
        StringJoiner::toString, CH_NOID);
}
List<String> list = Arrays.asList("A", "B", "C");
String string1 = list.stream().collect(Collectors.joining("-")); // A-B-C
String string2 = list.stream().collect(Collectors.joining("-", "[", "]")); // [A-B-C]

归约

和 reduce 类似。

public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op);

public static <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op);

public static <T, U> Collector<T, ?, U> reducing(U identity, Function<? super T, ? extends U> mapper, BinaryOperator<U> op);

形式一 public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op)

List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, "male", "New York"));
personList.add(new Person("Jack", 7000, "male", "Washington"));
personList.add(new Person("Lily", 7800, "female", "Washington"));
personList.add(new Person("Anni", 8200, "female", "New York"));
personList.add(new Person("Owen", 9500, "male", "New York"));
personList.add(new Person("Alisa", 7900, "female", "New York"));

// 查询每个地区工资最高的人
Comparator<Person> bySalary = Comparator.comparing(Person::getSalary);
Map<String, Optional<Person>> salaryByArea = personList.stream()
    .collect(Collectors.groupingBy(Person::getArea, Collectors.reducing(BinaryOperator.maxBy(bySalary))));
System.out.println(salaryByArea);

// 查询工资最高的人
Optional<Person> collect = personList.stream().collect(Collectors.reducing(BinaryOperator.maxBy(bySalary)));
System.out.println(collect);

输出结果

{New York=Optional[Person(name=Owen, salary=9500, age=null, sex=male, area=New York)], Washington=Optional[Person(name=Lily, salary=7800, age=null, sex=female, area=Washington)]}

Optional[Person(name=Owen, salary=9500, age=null, sex=male, area=New York)]

形式二 public static <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op)

和 reduce 类似。会接受一个identity参数,用来指定循环的初始值。

List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, "male", "New York"));
personList.add(new Person("Jack", 7000, "male", "Washington"));
personList.add(new Person("Lily", 7800, "female", "Washington"));
personList.add(new Person("Anni", 8200, "female", "New York"));
personList.add(new Person("Owen", 9500, "male", "New York"));
personList.add(new Person("Alisa", 7900, "female", "New York"));


// 查询每个地区工资最高的人
Comparator<Person> bySalary = Comparator.comparing(Person::getSalary);
Person identity = new Person("identity", 8500, null, null);
Map<String, Person> salaryByArea = personList.stream()
    .collect(Collectors.groupingBy(Person::getArea, Collectors.reducing(identity, BinaryOperator.maxBy(bySalary))));
System.out.println(salaryByArea);

// 查询工资最高的人
Person collect = personList.stream().collect(Collectors.reducing(identity, BinaryOperator.maxBy(bySalary)));
System.out.println(collect);

输出结果

{New York=Person(name=Owen, salary=9500, age=null, sex=male, area=New York), Washington=Person(name=identity, salary=8500, age=null, sex=null, area=null)}

Person(name=Owen, salary=9500, age=null, sex=male, area=New York)

形式三 public static <T, U> Collector<T, ?, U> reducing(U identity, Function<? super T, ? extends U> mapper, BinaryOperator<U> op)

有的时候,我们想在 reducing 的时候把 Person 的工资先处理一下(比如四舍五入)。这就需要我们做一个映射处理。定义一个 Function<? super T, ? extends U> mapper 来干这个活。

List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, "male", "New York"));
personList.add(new Person("Jack", 7000, "male", "Washington"));
personList.add(new Person("Lily", 7800, "female", "Washington"));
personList.add(new Person("Anni", 8200, "female", "New York"));
personList.add(new Person("Owen", 9500, "male", "New York"));
personList.add(new Person("Alisa", 7900, "female", "New York"));


// 定义映射 处理
Function<Person, Person> mapper = ps -> {
    Integer salary = ps.getSalary();
    ps.setSalary(salary/100);
    return ps;
};

// 查询每个地区工资最高的人
Comparator<Person> bySalary = Comparator.comparing(Person::getSalary);
Person identity = new Person("identity", 85, null, null);
Map<String, Person> salaryByArea = personList.stream()
    .collect(Collectors.groupingBy(Person::getArea, Collectors.reducing(identity, mapper, BinaryOperator.maxBy(bySalary))));
System.out.println(salaryByArea);

// 查询工资最高的人
Person collect = personList.stream().collect(Collectors.reducing(identity, mapper, BinaryOperator.maxBy(bySalary)));
System.out.println(collect);

输出结果

{New York=Person(name=Owen, salary=95, age=null, sex=male, area=New York), Washington=Person(name=identity, salary=85, age=null, sex=null, area=null)}

Person(name=identity, salary=85, age=null, sex=null, area=null)

参考连接

posted @ 2020-12-11 16:43  Lomen~  阅读(91)  评论(0编辑  收藏  举报