平时的开发中会经常遇到一些对象需要转换,创建了一个项目记录一些常见对象转换的方法,例如:文件转换、日期时间转换、stream 流转换、集合对象转换等,具体的示例代码见 GitHub 项目:zzycreate/java-convert-example 。

本文记录一些常用的 Stream 操作,以备需要时直接使用。

流的基本构成

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。

原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;

高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

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

而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。

顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。

Stream 流的使用基本分为三种操作:生成 Stream 流数据源、Stream 流中值操作、Stream 流结束操作。另外还有 short-circuiting 操作作为补充。

Stream 流的生成

想要获取 Stream 流数据源有很多种方式:

  1. 从集合中获取:集合对象(List、Set、Queue 等)的 stream()、parallelStream() 方法可以直接获取 Stream 对象;

  2. 从数组中获取:数据对象可以利用 Arrays.stream(T[] array) 或者 Stream.of() 的工具方法获取 Stream 对象;

  3. 从 IO 流中获取:BufferedReader 提供了 lines() 方法可以逐行获取 IO 流里面的数据;

  4. 静态工厂方法:Stream.of(Object[])、IntStream.range(int, int)、Stream.iterate(Object, UnaryOperator) 等静态工厂方法可以提供 Stream 对象;

  5. Files 类的操作路径的方法:如 list、find、walk 等;

  6. 随机数流:Random.ints();

  7. 其他诸如 Random.ints()、BitSet.stream()、Pattern.splitAsStream(java.lang.CharSequence)、JarFile.stream() 等方法;

  8. 更底层的使用 StreamSupport,它提供了将 Spliterator 转换成流的方法。

Stream 流的中间操作 (Intermediate)

一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射 / 过滤,然后返回一个新的流,交给下一个操作使用。

这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。只有在 Terminal 操作执行时才会真正的执行这些 Intermediate 操作。

常用的 Intermediate 操作有:map (mapToInt, flatMap 等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered。

Stream 流的执行操作 (Terminal)

一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。

Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

常用的 Terminal 操作有:forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator。

Short-circuiting

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

常用的 Short-circuiting 操作有:anyMatch、allMatch、noneMatch、findFirst、findAny、limit。

生成 Stream 流数据源

集合对象 -> Stream

集合对象本身提供了 stream() 和 parallelStream() ,两个方法可以直接获取 Stream 流

Stream<String> listStream = list.stream();
Stream<String> listParallelStream = list.parallelStream();
Stream<String> setStream = set.stream();
Stream<String> setParallelStream = set.parallelStream();

数组对象 -> Stream

数组对象转换需要利用工具类 Arrays、 Stream 的静态方法

Stream<String> arrayStream = Arrays.stream(array);
Stream<String> arrayStream1 = Stream.of(array);

IO 流 -> Stream

IO 流可以包装成 BufferedReader 转换为 Stream

BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("input.txt"), StandardCharsets.UTF_8));
Stream<String> stream = reader.lines();

流对象提供的构造方法

IntStream intStream = IntStream.range(1, 4);
DoubleStream doubleStream = DoubleStream.builder().add(1.1).add(2.1).add(3.1).add(4.1).build();
LongStream longStream = LongStream.of(1L, 2L, 3L, 4L);

Stream 流的 Intermediate 操作

示例代码: StreamIntermediateExample.java

map

map 的作用就是把 input Stream 的每一个元素,映射成 output Stream 的另外一个元素。

// 转大写
List<String> stringList = list.stream()
        .map(String::toUpperCase)
        .collect(Collectors.toList()); // [ABC, EFG, HIJ]
// 数据计算
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9).stream()
        .map(n -> n * n)
        .collect(Collectors.toList()); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
// 获取对象属性
List<String> list = list.stream()
        .map(Item::getDetail).map(ItemDetail::getValue)
        .collect(Collectors.toList()); // [v1, v5, v3, v2, v4]

flatMap

flatMap 把 input Stream 中的层级结构扁平化

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()); // [1, 2, 3, 4, 5, 6]

filter

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

Integer[] sixNums = {1, 2, 3, 4, 5, 6};
// 对 2 取模等于 0 的是偶数,filter 留下数字中的偶数
Integer[] evens = Stream.of(sixNums)
        .filter(n -> n % 2 == 0)
        .toArray(Integer[]::new); // [2, 4, 6]

distinct

distinct 是对元素进行去重,去重是利用了对象的 hashCode() 和 equals() 方法。

如果 distinct()正在处理有序流,那么对于重复元素,将保留以遭遇顺序首先出现的元素,并且以这种方式选择不同元素是稳定的。

在无序流的情况下,不同元素的选择不一定是稳定的,是可以改变的。distinct()执行有状态的中间操作。

在有序流的并行流的情况下,保持 distinct()的稳定性是需要很高的代价的,因为它需要大量的缓冲开销。

如果我们不需要保持遭遇顺序的一致性,那么我们应该可以使用通过 BaseStream.unordered()方法实现的无序流。

Integer[] nums = {1, 1, 2, 3, 4, 5, 4, 5, 6};
Integer[] evens = Stream.of(nums)
        .distinct()
        .toArray(Integer[]::new);// [1, 2, 3, 4, 5, 6]

sorted

sorted 方法用于排序,利用 Comparator 类的静态方法可以快速构造一个比较器实现排序。

List<Integer> list = Arrays.asList(5, 2, 4, 8, 6, 1, 9, 3, 7);
// sorted() 无参方法为自然排序
List<Integer> sorted = list.stream().sorted().collect(Collectors.toList());// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 使用 Comparator.reverseOrder() 获得一个自然逆序比较器,用于逆序排序
List<Integer> reverse = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 使用 Comparator.comparing() 获取一个自定义比较器,显现自定义对象的排序
List<Item> codeSorted = StreamIntermediateExample.newItems().stream()
        .sorted(Comparator.comparing(Item::getCode))
        .collect(Collectors.toList());
// [Item(name=Name1, code=1, number=1.1, detail=ItemDetail(id=101, value=v1)), Item(name=Name2, code=2, number=2.2, detail=ItemDetail(id=202, value=v2)), Item(name=Name3, code=3, number=3.3, detail=ItemDetail(id=303, value=v3)), Item(name=Name4, code=4, number=4.4, detail=ItemDetail(id=404, value=v4)), Item(name=Name5, code=5, number=5.5, detail=ItemDetail(id=505, value=v5))]
List<Item> codeReverse = StreamIntermediateExample.newItems().stream()
        .sorted(Comparator.comparing(Item::getCode).reversed())
        .collect(Collectors.toList());
// [Item(name=Name5, code=5, number=5.5, detail=ItemDetail(id=505, value=v5)), Item(name=Name4, code=4, number=4.4, detail=ItemDetail(id=404, value=v4)), Item(name=Name3, code=3, number=3.3, detail=ItemDetail(id=303, value=v3)), Item(name=Name2, code=2, number=2.2, detail=ItemDetail(id=202, value=v2)), Item(name=Name1, code=1, number=1.1, detail=ItemDetail(id=101, value=v1))]

unordered

某些流的返回的元素是有确定顺序的,我们称之为 encounter order。这个顺序是流提供它的元素的顺序,比如数组的 encounter order 是它的元素的排序顺序,List 是它的迭代顺序 (iteration order),对于 HashSet,它本身就没有 encounter order。

一个流是否是 encounter order 主要依赖数据源和它的中间操作,比如数据源 List 和 Array 上创建的流是有序的 (ordered),但是在 HashSet 创建的流不是有序的。

sorted() 方法可以将流转换成 encounter order 的,unordered 可以将流转换成 encounter order 的。

注意,这个方法并不是对元素进行排序或者打散,而是返回一个是否 encounter order 的流。

可以参见 stackoverflow 上的问题: stream-ordered-unordered-problems

除此之外,一个操作可能会影响流的有序,比如 map 方法,它会用不同的值甚至类型替换流中的元素,所以输入元素的有序性已经变得没有意义了,但是对于 filter 方法来说,它只是丢弃掉一些值而已,输入元素的有序性还是保障的。

对于串行流,流有序与否不会影响其性能,只是会影响确定性 (determinism),无序流在多次执行的时候结果可能是不一样的。

对于并行流,去掉有序这个约束可能会提高性能,比如 distinct、groupingBy 这些聚合操作。

peek

peek() 接受一个 Consumer 消费者方法,而 map() 接受一个 Function 方法;Consumer 方法返回值是 void,而 Function 方法有返回值,peek 和 map 方法的区别主要在于流处理过程中返回值的不同。

peek() 方法是 Intermediate 方法,而 forEach() 方法是 Terminal 方法;如果 peek 方法后没有 Terminal 方法,则 peek 并不会真正的执行,forEach 方法则会立即执行。

forEach 和 peek 都是接受 Consumer 对象的,因此如果在 Stream 流处理的过程中做一些数据操作或者打印操作,选择 peek 方法,该方法还会返回 Stream 流,用于下一步处理;如果已经是处理的最后一步,则选择 forEach 用于最终执行整个流。

// [Abc, efG, HiJ] -> [Abc, efG, HiJ]
List<String> peek = StreamIntermediateExample.newStringList().stream()
        .peek(str -> {
            if ("Abc".equals(str)) {
                str = str.toUpperCase();
            }
        }).collect(Collectors.toList());

peek 方法对对象的修改,会影响到集合里面的元素,但如果集合中是 String 这种,则不会改变,

因为修改后的 String 在常量池中是另一个对象,由于 Consumer 无法返回该对象,Stream 内的元素仍然指向原来的 String。

对对象的修改则是改变堆中对象的数据,对象的引用并没有发生变化,Stream 中的元素任然指向原对象,只是对象内部已经发生了改变。

// [Name1, Name5, Name3, Name2, Name4] -> [xxx, Name5, Name3, Name2, Name4]
List<String> peek1 = StreamIntermediateExample.newItems().stream()
        .peek(item -> {
            if (item.getCode() == 1) {
                item.setName("xxx");
            }
        })
        .map(Item::getName)
        .collect(Collectors.toList());

limit

limit 方法会对一个 Stream 进行截断操作,获取其前 N 个元素,如果原 Stream 中包含的元素个数小于 N,那就获取其所有的元素;

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// 截取指定元素位置以内的元素
List<Integer> limit2 = numbers.stream().limit(2).collect(Collectors.toList());// [1, 2]
List<Integer> limit6 = numbers.stream().limit(6).collect(Collectors.toList());// [1, 2, 3, 4, 5, 6]
List<Integer> limit8 = numbers.stream().limit(8).collect(Collectors.toList());// [1, 2, 3, 4, 5, 6]

skip

skip 方法会返回一个丢弃原 Stream 的前 N 个元素后剩下元素组成的新 Stream,如果原 Stream 中包含的元素个数小于 N,那么返回空 Stream;

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// 忽略指定元素位置以内的元素
List<Integer> skip2 = numbers.stream().skip(2).collect(Collectors.toList());// [3, 4, 5, 6]
List<Integer> skip6 = numbers.stream().skip(6).collect(Collectors.toList());// []
List<Integer> skip8 = numbers.stream().skip(8).collect(Collectors.toList());// []

parallel

parallel stream 是基于 fork/join 框架的,简单点说就是使用多线程来完成的,使用 parallel stream 时要考虑初始化 fork/join 框架的时间,

如果要执行的任务很简单,那么初始化 fork/join 框架的时间会远多于执行任务所需时间,也就导致了效率的降低。

根据附录 doug Lee 的说明,任务数量*执行方法的行数》=10000 或者执行的是消耗大量时间操作(如 io/ 数据库)才有必要使用

Java 8 为 ForkJoinPool 添加了一个通用线程池,这个线程池用来处理那些没有被显式提交到任何线程池的任务。

它是 ForkJoinPool 类型上的一个静态元素,它拥有的默认线程数量等于运行计算机上的处理器数量。

当调用 Arrays 类上添加的新方法时,自动并行化就会发生。

比如用来排序一个数组的并行快速排序,用来对一个数组中的元素进行并行遍历。自动并行化也被运用在 Java 8 新添加的 Stream API 中。

并行流是 JDK8 对多线程的应用,但是难以控制,要想用好并行流,需要深入理解 ForkJoinPool 。

以下的例子详阅:深入浅出 parallelStream

System.out.println("Hello World!");
// 构造一个 10000 个元素的集合
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
    list.add(i);
}
// 统计并行执行 list 的线程
Set<Thread> threadSet = new CopyOnWriteArraySet<>();
// 并行执行
list.stream().parallel().forEach(integer -> {
    Thread thread = Thread.currentThread();
    // System.out.println(thread);
    // 统计并行执行 list 的线程
    threadSet.add(thread);
});
System.out.println("threadSet 一共有" + threadSet.size() + "个线程"); // 6
System.out.println("系统一个有" + Runtime.getRuntime().availableProcessors() + "个 cpu"); // 8
List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
    list1.add(i);
    list2.add(i);
}
Set<Thread> threadSetTwo = new CopyOnWriteArraySet<>();
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread threadA = new Thread(() -> {
    list1.stream().parallel().forEach(integer -> {
        Thread thread = Thread.currentThread();
        // System.out.println("list1" + thread);
        threadSetTwo.add(thread);
    });
    countDownLatch.countDown();
});
Thread threadB = new Thread(() -> {
    list2.stream().parallel().forEach(integer -> {
        Thread thread = Thread.currentThread();
        // System.out.println("list2" + thread);
        threadSetTwo.add(thread);
    });
    countDownLatch.countDown();
});
threadA.start();
threadB.start();
countDownLatch.await();
System.out.println("threadSetTwo 一共有" + threadSetTwo.size() + "个线程"); // 9
System.out.println("---------------------------");
// [Thread[main,5,main],
// Thread[ForkJoinPool.commonPool-worker-3,5,main],
// Thread[ForkJoinPool.commonPool-worker-1,5,main],
// Thread[ForkJoinPool.commonPool-worker-4,5,main],
// Thread[ForkJoinPool.commonPool-worker-5,5,main],
// Thread[ForkJoinPool.commonPool-worker-2,5,main]]
System.out.println(threadSet);
// [Thread[ForkJoinPool.commonPool-worker-6,5,main],
// Thread[ForkJoinPool.commonPool-worker-7,5,main],
// Thread[Thread-0,5,],
// Thread[ForkJoinPool.commonPool-worker-5,5,main],
// Thread[Thread-1,5,],
// Thread[ForkJoinPool.commonPool-worker-4,5,main],
// Thread[ForkJoinPool.commonPool-worker-3,5,main],
// Thread[ForkJoinPool.commonPool-worker-2,5,main],
// Thread[ForkJoinPool.commonPool-worker-1,5,main]]
System.out.println(threadSetTwo);
System.out.println("---------------------------");
threadSetTwo.addAll(threadSet);
// [Thread[ForkJoinPool.commonPool-worker-6,5,main],
// Thread[ForkJoinPool.commonPool-worker-7,5,main],
// Thread[Thread-0,5,],
// Thread[ForkJoinPool.commonPool-worker-5,5,main],
// Thread[Thread-1,5,],
// Thread[ForkJoinPool.commonPool-worker-4,5,main],
// Thread[ForkJoinPool.commonPool-worker-3,5,main],
// Thread[ForkJoinPool.commonPool-worker-2,5,main],
// Thread[ForkJoinPool.commonPool-worker-1,5,main],
// Thread[main,5,main]]
// 执行 forEach 本身的线程也作为线程池中的一个工作线程
System.out.println(threadSetTwo);
System.out.println("threadSetTwo 一共有" + threadSetTwo.size() + "个线程");
System.out.println("系统一个有" + Runtime.getRuntime().availableProcessors() + "个 cpu");

sequential

通过 parallel() 方法可以将串行流转换成并行流,sequential() 方法将流转换成串行流。

顺序流和并行流相对,这种使用的方法很少,暂时没有研究。

Stream 流的 Terminal 操作

示例代码: StreamTerminalExample.java

collect

collect 方法是 Terminal 操作,可以将 Stream 流转换为集合,Collectors 中提供了一些便捷的生成 Collector 的方法, 例如 toList() 用于生成 List 列表, toSet() 可以用于生成 Set 堆, toMap()可以用于生成 Map。

List<String> list = Arrays.asList("apple", "orange", "banana", "pear");
List<String> collectList = list.stream().filter(s -> s.length() > 5).collect(Collectors.toList());//[orange, banana]
Set<String> collectSet = list.stream().filter(s -> s.length() > 5).collect(Collectors.toSet());// [orange, banana]

Collectors.toMap() 有三个重构方法,推荐至少使用三个参数的 toMap() 方法, BinaryOperator<U>mergeFunction这个参数有利于解决,生成 Map 时的主键重复问题,避免因为源数据问题产生问题。

Map<Integer, Item> collectMap1 = items.stream()
        .collect(Collectors.toMap(Item::getCode, Function.identity()));
Map<Integer, Item> collectMap2 = StreamConstant.newItems().stream()
        .collect(Collectors.toMap(Item::getCode, Function.identity(), (a, b) -> a));

toCollection() 可以用于生成各种各样自定义的集合结构。

Stack<String> collect = items.stream()
            .map(Item::getName)
            .collect(Collectors.toCollection(Stack::new));

Collector 包含四种不同的操作:supplier(初始构造器), accumulator(累加器), combiner(组合器), finisher(终结者)。

简单分组:

Map<Integer, List<Item>> groupingByCollect = StreamConstant.newItems().stream()
        .collect(Collectors.groupingBy(Item::getCode));

平均值:

Double average = StreamConstant.newItems().stream()
        .collect(Collectors.averagingInt(Item::getCode));

统计:

IntSummaryStatistics summaryStatistics = StreamConstant.newItems().stream()
        .collect(Collectors.summarizingInt(Item::getCode));// IntSummaryStatistics{count=5, sum=15, min=1, average=3.000000, max=5}

拼接(三个参数分别是:连接符、字符串前缀、字符串后缀):

String join = list.stream()
            .collect(Collectors.joining(" and ", "The ", " are fruits"));// The apple and orange and banana and pear are fruits

Collector.of() 方法可以创建了一个新的 collector,我们必须给这个 collector 提供四种功能:supplier, accumulator, combiner,finisher.

supplier 初始化构造分割符;accumulator 处理数据,并叠加数据;combiner 进行数据连接,finisher 生成最终数据。

Collector<Item, StringJoiner, String> personNameCollector =
        Collector.of(
                () -> new StringJoiner(" | "),               // supplier
                (j, p) -> j.add(p.getName().toUpperCase()),  // accumulator
                (j1, j2) -> j1.merge(j2),                    // combiner
                StringJoiner::toString);                     // finisher
String names = StreamConstant.newItems().stream()
        .collect(personNameCollector);
System.out.println(names);  // NAME1 | NAME5 | NAME3 | NAME2 | NAME4

toArray

toArray() 方法可以将流中的数据放入一个数组中。无参方法只能生成 Object[] 对象数组,单参方法可以指定生成的数组类型。

List<String> list = Arrays.asList("apple", "orange", "banana", "pear");
Object[] objects = list.stream().filter(s -> s.length() > 5).toArray();
String[] strings = list.stream().filter(s -> s.length() > 5).toArray(String[]::new);

forEach/forEachOrdered

forEach 方法接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。

一般认为,forEach 和常规 for 循环的差异不涉及到性能,它们仅仅是函数式风格与传统 Java 风格的差别。

// Java 8
list.stream()
        .filter(s -> s.length() > 5)
        .forEach(System.out::println);
// Pre-Java 8
for (String s : list) {
    if (s.length() > 5) {
        System.out.println(s);
    }
}

forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。

Stream<String> stream = list.stream().filter(s -> s.length() > 5);
stream.forEach(element -> System.out.println("1: "+element));// ok
stream.forEach(element -> System.out.println("2: "+element));// java.lang.IllegalStateException: stream has already been operated upon or closed

要想实现上述类似功能,可以使用 peek 方法,peek 是中间方法,流还没有被消费掉。或者利用 Supplier 提供者,Supplier 的 get 方法可以构造新的 Stream。

Supplier<Stream<String>> streamSupplier= () -> (list.stream().filter(s -> s.length() > 5));
streamSupplier.get().forEach(element -> System.out.println("1: "+element));
streamSupplier.get().forEach(element -> System.out.println("2: "+element));

forEachOrdered 是为了保证执行后数据的有序性。

reduce

Stream 的 reduce 方法,返回单个的结果值,并且 reduce 操作每处理一个元素总是创建一个新值。常用的方法有 average、sum、min、max、count,都可以使用 reduce 方法实现。

<U> U reduce(U identity,BiFunction<U,? super T,U> accumulator,BinaryOperator<U> combiner)
#第一个参数 identity 返回实例 u,传递你要返回的 U 类型对象的初始化实例 u, 用于提供一个循环计算的初始值
#第二个参数累加器 accumulator,可以使用二元ℷ表达式(即二元 lambda 表达式),声明你在 u 上累加你的数据来源 t 的逻辑
#例如 (u,t)->u.sum(t), 此时 lambda 表达式的行参列表是返回实例 u 和遍历的集合元素 t,函数体是在 u 上累加 t
#BinaryOperator 的函数方法为 apply(U u, T t), 第一个参数为上次函数计算的返回值,第二个参数为 Stream 中的元素,函数方法会将两个值计算 apply,得到的值赋值给下次执行该方法的第一个参数
#第三个参数组合器 combiner,同样是二元ℷ表达式,(u,t)->u
#lambda 表达式行参列表同样是 (u,t),函数体返回的类型则要和第一个参数的类型保持一致

具体的一个示例:

Supplier<Stream<Integer>> supplier = () -> (Stream.of(1, 2, 3, 4).filter(p -> p > 2));
List<Integer> result = supplier.get()
        .collect(() -> new ArrayList<>(), (list, item) -> list.add(item), (one, two) -> one.addAll(two));
System.out.println(result);// [3, 4]
/* 或者使用方法引用 */
result = supplier.get().collect(ArrayList::new, List::add, List::addAll);
System.out.println(result);// [3, 4]

min/max/count

一般比较大小需要比较器 Comparator, min 和 max 返回值类型是 Optional。

count 是对满足条件的数据进行统计,计算次数。等价于 returnmapToLong(e->1L).sum();

Supplier<Stream<Integer>> supplier = () -> (Stream.of(1, 2, 3, 4).filter(p -> p > 2));
Optional<Integer> min = supplier.get().min(Integer::compareTo);// Optional[3]
Optional<Integer> max = supplier.get().max(Integer::compareTo);// Optional[4]
long count = supplier.get().count();// 2

anyMatch/allMatch/noneMatch

  • allMatch:Stream 中全部元素符合传入的 predicate,返回 true

  • anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true

  • noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true

它们都不是要遍历全部元素才能返回结果。例如 allMatch 只要一个元素不满足条件,就 skip 剩下的所有元素,返回 false。对清单 13 中的 Person 类稍做修改,加入一个 age 属性和 getAge 方法。

boolean b1 = StreamConstant.newItems().stream().map(Item::getCode).anyMatch(i -> i > 3);// true
boolean b2 = StreamConstant.newItems().stream().map(Item::getCode).anyMatch(i -> i > 5);// false
boolean b3 = StreamConstant.newItems().stream().map(Item::getCode).allMatch(i -> i > 3);// false
boolean b4 = StreamConstant.newItems().stream().map(Item::getCode).allMatch(i -> i > 5);// false
boolean b5 = StreamConstant.newItems().stream().map(Item::getCode).noneMatch(i -> i > 3);// false
boolean b6 = StreamConstant.newItems().stream().map(Item::getCode).noneMatch(i -> i > 5);// true

findFirst/findAny

findFirst 是一个 termimal 兼 short-circuiting 操作,它总是返回 Stream 的第一个元素,或者空。

findAny 是搜索到任何一个符合条件的结果返回,因为流可能是并行的,因此顺序可能不是确定的。如果顺序是确定的,使用 findFirst 可以方便的获取第一个元素。

Optional<String> first = StreamConstant.newStringList().stream().findFirst();// Optional[Abc]
String any = StreamConstant.newStringList().stream()
        .filter(s -> s.length() > 5).findAny().orElse("ERROR");// ERROR

有效的特殊用法

自定义去重

利用 Map 的 key 不能重复的特性进行去重,实现下方静态方法,在需要的使用结合 filter 和 distinctByKey 方法进行去重。

示例代码: DistinctByKey.java

public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
    Map<Object, Boolean> seen = new ConcurrentHashMap<>();
    // putIfAbsent 如果 map 中有值,则返回原值,新值也不会放入 map 中,如果原来没有值,则返回 null,本次 put 的值也会放入 map 中
    return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

使用的时候只需要使用 filter 过滤掉重复项:

示例代码:StreamSpecialExample.java

items.stream().filter(distinctByKey(Item::getName)).collect(Collectors.toList());

求和

BigDecimal 对象求和:

// 计算 总金额
BigDecimal totalMoney = stream().map(Item::getDetail).map(ItemDetail::getMoney)
        .reduce(BigDecimal.ZERO, BigDecimal::add);
System.err.println("totalMoney:" + totalMoney.setScale(2, RoundingMode.HALF_UP));  //totalMoney:166.50

基本类型求和:

// 计算 数量
double sum = stream().mapToDouble(Item::getNumber).sum();
System.err.println("sum:" + sum);  //sum:16.5

参考文章

  • Java 8 中的 Streams API 详解

  • Java Stream 详解 ——鸟窝

  • Java 8 Stream 教程

References

  • zzycreate/java-convert-example: https://github.com/zzycreate/java-convert-example

  • StreamIntermediateExample.java: https://github.com/zzycreate/java-convert-example/blob/master/src/main/java/io/github/zzycreate/example/stream/StreamIntermediateExample.java

  • stream-ordered-unordered-problems: https://stackoverflow.com/questions/21350195/stream-ordered-unordered-problems

  • 深入浅出 parallelStream: https://blog.csdn.net/u011001723/article/details/52794455

  • StreamTerminalExample.java: https://github.com/zzycreate/java-convert-example/blob/master/src/main/java/io/github/zzycreate/example/stream/StreamTerminalExample.java

  • DistinctByKey.java: https://github.com/zzycreate/java-convert-example/blob/master/src/main/java/io/github/zzycreate/example/stream/tools/DistinctByKey.java

  • StreamSpecialExample.java: https://github.com/zzycreate/java-convert-example/blob/master/src/main/java/io/github/zzycreate/example/stream/StreamSpecialExample.java

  • Java 8 中的 Streams API 详解: https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/index.html

  • Java Stream 详解 ——鸟窝: https://colobu.com/2016/03/02/Java-Stream/#%E6%8E%92%E5%BA%8F_Ordering

  • Java 8 Stream 教程: https://www.jianshu.com/p/0c07597d8311


本文作者赵振耀,原文链接 https://zzycreate.github.io/2019/07/14/java-convert-example-stream/,相关权利归原作者所有,本次发布已征得作者许可。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐