Java8流式处理(Stream)使用详解

一、概述

Jdk 8新增的Stream,配合Lambda,给操作集合(Collection)提供了极大的便利。

那么什么是StreamStream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。

Stream可以由数组或集合创建,对流的操作分为两种:

  • 中间操作,每次返回一个新的流,可以有多个。
  • 终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。

另外,Stream有几个特性:

  1. stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
  2. stream不会改变数据源,通常情况下会产生一个新的集合或一个值。
  3. stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。

二、常用方法

2.1 创建

Stream有三种创建方式

  1. 通过java.util.Collection.stream()方法用集合创建流。
  2. 使用java.util.Arrays.stream(T[] array)方法用数组创建流。
  3. 使用Stream的静态方法:of()iterate()generate()

2.1.1 通过集合创建流

List<String> list = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Stream<String> stream = list.stream();
// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();
  • 串行流:适合存在线程安全问题、阻塞任务、重量级任务,以及需要使用同一事务的逻辑。
  • 并行流:适合没有线程安全问题、较单纯的数据处理任务。

2.1.2 使用数组创建流

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

2.1.3 使用静态方法

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);

Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4);
stream2.forEach(System.out::println);

Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);

输出结果:

0 3 6 9
0.6796156909271994
0.1914314208854283
0.8116932592396652

streamparallelStream的简单区分:stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。例如: 筛选集合中的奇数,两者的处理不同之处: 如果流中的数据量足够大,并行流可以加快处速度。

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

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

关于更多的Parallel Stream,请参考:Java Parallel StreamJava Stream之Parallel Streams编程指南

2.2 中间操作

2.2.1 筛选与切片

  1. filter(Predicate): 筛选流中某些元素。
  2. limit(long val): 截断流,取流中前val个元素。
  3. skip(n): 跳过n元素,配合limit(n)可实现分页。
  4. distinct(): 通过流所生成元素的equalshashCode去重。

2.2.2 映射

  1. map(Function f): 接收流中元素,并且将其映射成为新元素,例如:从student对象中取name属性。
  2. flatMap(Function f): 将所有流中的元素并到一起连接成一个流。

2.2.3 消费

  1. peek(Consumer c): 获取流中元素,操作流中元素,与foreach不同的是不会截断流,可继续操作。

使用场景:当遍历完数组后还有后续操作时或list数组转stream时,不适合在用Iterable的foreach循环,这个时候peek就派上用场了。

2.2.4 排序

  1. sorted()/sorted(Comparator): 产生一个新流,按照自然顺序/比较器规则排序

2.3 终止操作

2.3.1 匹配/聚合操作

匹配


  1. allMatch(Predicate): 当流中每个元素都符合该断言时才返回true,否则返回false
  2. noneMatch(Predicate): 当流中每个元素都不符合该断言时才返回true,否则返回false
  3. anyMatch(Predicate): 只要流中有一个元素满足该断言则返回true,否则返回false

寻找元素


  1. findFirst(): 返回流中第一个元素。
  2. findAny(): 返回流中的任意元素。

计数和极值


  1. count(): 返回流中元素的总个数。
  2. max(): 返回流中元素最大值。
  3. min():返回流中元素最小值。

2.3.2 归约操作

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

2.3.3 收集操作

collect: 接收一个Collector实例,将流中元素收集成另外一个数据结构。

Collector<T, A, R>是一个接口,有以下5个抽象方法:

  1. Supplier supplier(): 创建一个结果容器A
  2. BiConsumer<A, T> accumulator(): 消费型接口,第一个参数为容器A,第二个参数为流中元素T
  3. BinaryOperator combiner(): 函数接口,该参数的作用跟上一个方法(reduce)中的combiner参数一样,将并行流中各个子进程的运行结果(accumulator函数操作后的容器A)进行合并。
  4. Function<A, R> finisher(): 函数式接口,参数为:容器A,返回类型为:collect方法最终想要的结果R
  5. Set characteristics(): 返回一个不可变的Set集合,用来表明该Collector的特征。有以下三个特征:
    • CONCURRENT: 表示此收集器支持并发。(官方文档还有其他描述,暂时没去探索,故不作过多翻译)
    • UNORDERED: 表示该收集操作不会保留流中元素原有的顺序。
    • IDENTITY_FINISH: 表示finisher参数只是标识而已,可忽略。

collect主要依赖java.util.stream.Collectors类内置的静态方法。Collectors具体方法如下:

归集

  1. toList(): 将元素收集到一个新的List
  2. toMap(): 将元素收集到Map中,Map其键和值是将提供的映射函数应用于元素的结果。
  3. toSet(): 将元素收集到一个新的Set
  4. toCollection(): 将元素Collection按遇到顺序收集到一个new中。
  5. toConcurrentMap(): 将元素收集到ConcurrentMap的并发对象,其键和值是将提供的映射函数应用于元素的结果。
  6. toUnmodifiableList(): 将元素收集到一个不可修改的List集合中。任何修改List集合的操作都将导致UnsupportedOperationException
  7. toUnmodifiableSet(): 将元素收集到一个不可修改的Set集合中。任何修改Set集合的操作都将导致UnsupportedOperationException

统计

  1. counting(): 返回计算元素数量,如果没有元素,则结果为0
  2. averagingDouble(): 应用于元素的double值函数的算术平均值。如果没有元素,则结果为0
  3. averagingInt(): 应用于元素的int值函数的算术平均值。
  4. averagingLong(): 应用于元素的long值函数的算术平均值。
  5. summarizingDouble(): 将生成double映射函数应用于每个元素,并返回结果值的汇总统计信息。
  6. summarizingInt(): 将生成int映射函数应用于每个元素,并返回结果值的汇总统计信息。
  7. summarizingLong(): 将生成long映射函数应用于每个元素,并返回结果值的汇总统计信息。
  8. summingDouble(): 应用于元素的double值函数的总和。
  9. summingInt(): 应用于元素的int值函数的总和。
  10. summingLong(): 应用于元素的long值函数的总和。
  11. maxBy(): 根据给定的比较器产生最大元素。
  12. minBy(): 根据给定的比较器产生最小元素。

分组

  1. groupingBy(): 根据分类函数对元素进行分组,并返回结果Map
  2. groupingByConcurrent(): 并发执行,根据分类函数对元素进行分组。
  3. partitioningBy(): 对元素进行分区Predicate,并将它们组织成Map<Boolean, List<T>>

接合

  1. joining(): 按遇到顺序将元素连接成String

归约

  1. reducing(): 在指定的BinaryOperator下执行其元素的缩减。

映射

  1. mapping(): 通过在累加之前对每个元素应用映射函数。

归纳

  1. collectingAndThen(): 调整Collector执行其它的结束转换。

并集

  1. teeing(): Jdk 12添加了一个新的teeing方法。用于复杂的聚合操作。

三、使用案例

在使用Stream之前,先理解一个概念:OptionalOptional类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

案例使用的员工类,这是后面案例中使用的员工类:

private static List<Employee> init() {
    List<Employee> employeeList = new ArrayList<Employee>();
    employeeList.add(new Employee("Tom", 8900, 23, "male", "New York"));
    employeeList.add(new Employee("Jack", 7000, 25, "male", "Washington"));
    employeeList.add(new Employee("Lily", 7800, 21, "female", "Washington"));
    employeeList.add(new Employee("Anni", 8200, 24, "female", "New York"));
    employeeList.add(new Employee("Owen", 9500, 25, "male", "New York"));
    employeeList.add(new Employee("Alisa", 7900, 26, "female", "New York"));
    return employeeList;
}

@Data
@AllArgsConstructor
class Employee {
    private String name;  // 姓名
    private int salary;   // 薪资
    private int age;      // 年龄
    private String sex;   // 性别
    private String city;  // 城市
}

3.1 遍历/匹配(foreach/find/match)

Stream也是支持类似集合的遍历和匹配元素的,只是Stream中的元素是以Optional类型存在的。Stream的遍历、匹配非常简单。

public class StreamTest {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1);
        // 匹配第一个
        Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst();
        // 匹配任意(适用于并行流)
        Optional<Integer> findAny = list.parallelStream().filter(x -> x > 6).findAny();
        // 是否包含符合特定条件的元素
        boolean anyMatch = list.stream().anyMatch(x -> x > 6);
        System.out.println("匹配第一个值:" + findFirst.get());
        System.out.println("匹配任意一个值:" + findAny.get());
        System.out.println("是否存在大于6的值:" + anyMatch);
    }
}

运行结果:

匹配第一个值:7
匹配任意一个值:8
是否存在大于6的值:true

一、list.forEach()与list.stream().forEach()区别:
1). list.forEach()使用增强for循环。list.stream().forEach()首先将集合转换为流,然后对集合的流进行迭代。最后调用ReferencePipeline类的forEach方法。

public void forEach(Consumer<? super E_OUT> action) {
    if (!isParallel()) {
        sourceStageSpliterator().forEachRemaining(action);
    } else {
        super.forEach(action);
    }
}

forEachRemaining方法对集合中剩余的元素进行操作,也就是说只遍历一次集合元素。
2). 当一边遍历一边删除的时候,forEach能够快速失败,而stream().forEach()只有等到数组遍历完之后才会抛异常。

二、增强for循环与stream().forEach区别:

1). 普通for循环和增强for循环: break跳出整个for循环,使用continue跳出本次循环。
2). stream.forEach()循环:

  1. 处理集合时不能使用breakcontinue中止循环;
  2. 可以使用关键字return跳出本次循环,并执行下一次遍历。
  3. 不能跳出整个forEach的循环。

3.2 筛选(filter)

筛选,是按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。

案例一:筛选出Integer集合中大于7的元素,并打印出来。

public class StreamTest {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2, 9);
        Stream<Integer> stream = list.stream();
        stream.filter(x -> x > 7).forEach(System.out::println);
    }
}

预期结果:

8 9

案例二:筛选员工中工资高于8000的人,并形成新的集合。形成新集合依赖collect(收集)。

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();
        // 高于8000的员工姓名
        List<String> filterList = employeeList.stream()
                .filter(x -> x.getSalary() > 8000)
                .map(Employee::getName).collect(Collectors.toList());
        System.out.println("高于8000的员工姓名:" + filterList);
    }
}

运行结果:

高于8000的员工姓名:[Tom, Anni, Owen]

3.3 聚合(max/min/count)

maxmincount这些字眼你一定不陌生,没错,在mysql中我们常用它们进行数据统计。Java stream中也引入了这些概念和用法,极大地方便了我们对集合、数组的数据统计工作。

案例一:获取String集合中最长的元素。

public class StreamTest {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd");
        // 比较
        Optional<String> max = list.stream().max(Comparator.comparing(String::length));
        System.out.println("最长的字符串:" + max.get());
    }
}

输出结果:

最长的字符串:weoujgsd

案例二:获取Integer集合中的最大值。

public class StreamTest {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(7, 6, 9, 4, 11, 6);
        // 自然排序
        Optional<Integer> max = list.stream().max(Integer::compareTo);
        // 自定义排序
        Optional<Integer> max2 = list.stream().max(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1.compareTo(o2);
            }
        });
        System.out.println("自然排序的最大值:" + max.get());
        System.out.println("自定义排序的最大值:" + max2.get());
    }
}

输出结果:

自然排序的最大值:11
自定义排序的最大值:11

案例三:获取员工工资最高的人。

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();
        // 员工工资最大值
        Optional<Employee> max = employeeList.stream()
                 .max(Comparator.comparingInt(Employee::getSalary));
        System.out.println("员工工资最大值:" + max.get().getSalary());
    }
}

输出结果:

员工工资最大值:9500

案例四:计算Integer集合中大于6的元素的个数。

public class StreamTest {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9);
        // list中大于6的元素个数
        long count = list.stream().filter(x -> x > 6).count();
        System.out.println("list中大于6的元素个数:" + count);
    }
}

输出结果:

list中大于6的元素个数:4

3.4 映射(map/flatMap)

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

  • map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
  • flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

案例一:英文字符串数组的元素全部改为大写。整数数组每个元素加3。

public class StreamTest {
    public static void main(String[] args) {
        String[] strArr = {"abcd", "bcdd", "defde", "fTr"};
        // 数组元素转大写
        List<String> strList = Arrays.stream(strArr).map(String::toUpperCase)
                .collect(Collectors.toList());

        List<Integer> intList = Arrays.asList(1, 3, 5, 7, 9, 11);
        // 元素加法
        List<Integer> intListNew = intList.stream().map(x -> x + 3).collect(Collectors.toList());

        System.out.println("每个元素大写:" + strList);
        System.out.println("每个元素 + 3:" + intListNew);
    }
}

输出结果:

每个元素大写:[ABCD, BCDD, DEFDE, FTR]
每个元素 + 3:[4, 6, 8, 10, 12, 14]

案例二:将员工的薪资全部增加1000。

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();

        // 不改变原来员工集合的方式
        List<Employee> employeeListNew = employeeList.stream().map(employee -> {
            Employee employeeNew = new Employee(employee.getName(), 0, 0, null, null);
            employeeNew.setSalary(employee.getSalary() + 10000);
            return employeeNew;
        }).collect(Collectors.toList());
        System.out.println("一次改动前:" + employeeList.get(0).getName() 
                                        + "-->" + employeeList.get(0).getSalary());
        System.out.println("一次改动后:" + employeeListNew.get(0).getName()
                                        + "-->" + employeeListNew.get(0).getSalary());

        // 改变原来员工集合的方式
        List<Employee> employeeListNew2 = employeeList.stream().map(employee -> {
            employee.setSalary(employee.getSalary() + 10000);
            return employee;
        }).collect(Collectors.toList());
        System.out.println("二次改动前:" + employeeList.get(0).getName()
                                        + "-->" + employeeListNew.get(0).getSalary());
        System.out.println("二次改动后:" + employeeListNew2.get(0).getName()
                                        + "-->" + employeeListNew.get(0).getSalary());
    }
}

输出结果:

一次改动前:Tom–>8900
一次改动后:Tom–>18900
二次改动前:Tom–>18900
二次改动后:Tom–>18900

案例三:将两个字符数组合并成一个新的字符数组。

public class StreamTest {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("m, k, l, a", "1, 3, 5, 7");
        List<String> listNew = list.stream().flatMap(s -> {
            // 将每个元素转换成一个stream
            String[] split = s.split(",");
            Stream<String> s2 = Arrays.stream(split);
            return s2;
        }).collect(Collectors.toList());
        
        System.out.println("处理前的集合:" + list);
        System.out.println("处理后的集合:" + listNew);
    }
}

输出结果:

处理前的集合:[m-k-l-a, 1-3-5-7]
处理后的集合:[m, k, l, a, 1, 3, 5, 7]

3.5 归约(reduce)

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

案例一:求Integer集合的元素之和、乘积和最大值。

public class StreamTest {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4);
        // 求和方式1
        Optional<Integer> sum = list.stream().reduce((x, y) -> x + y);
        // 求和方式2
        Optional<Integer> sum2 = list.stream().reduce(Integer::sum);
        // 求和方式3
        Integer sum3 = list.stream().reduce(0, Integer::sum);
        
        // 求乘积
        Optional<Integer> product = list.stream().reduce((x, y) -> x * y);

        // 求最大值方式1
        Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y);
        // 求最大值写法2
        Integer max2 = list.stream().reduce(1, Integer::max);
	    
        System.out.println("list求和:" + sum.get() + "," + sum2.get() + "," + sum3);
        System.out.println("list求积:" + product.get());
        System.out.println("list求和:" + max.get() + "," + max2);
    }
}

输出结果:

list求和:29,29,29
list求积:2112
list求和:11,11

案例二:求所有员工的工资之和和最高工资。

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();
    
        // 求工资之和方式1:
    	Optional<Integer> sumSalary = employeeList.stream()
                .map(Employee::getSalary).reduce(Integer::sum);
        // 求工资之和方式4:
    	Integer sumSalary4 = employeeList.stream().map(Employee::getSalary).reduce(0, Integer::sum);
    	// 求工资之和方式2:
    	Integer sumSalary2 = employeeList.stream()
                .reduce(0, (sum, p) -> sum += p.getSalary(), (sum1, sum2) -> sum1 + sum2);
    	// 求工资之和方式3:
    	Integer sumSalary3 = employeeList.stream()
                .reduce(0, (sum, p) -> sum += p.getSalary(), Integer::sum);
    
    	// 求最高工资方式1:
    	Integer maxSalary = employeeList.stream()
                .reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(), Integer::max);
    	// 求最高工资方式2:
    	Integer maxSalary2 = employeeList.stream()
                .reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(),
                        (max1, max2) -> max1 > max2 ? max1 : max2);
    
    	System.out.println("工资之和:" + sumSalary.get() + "," + sumSalary2
                + "," + sumSalary3 + "," + sumSalary4);
    	System.out.println("最高工资:" + maxSalary + "," + maxSalary2);
    }
}

输出结果:

工资之和:49300,49300,49300,49300
最高工资:9500,9500

3.6 收集(collect)

collect(收集),可以说是内容最繁多、功能最丰富的部分了。从字面上去理解,就是把一个流收集起来,最终可以是收集成一个值也可以收集成一个新的集合。collect主要依赖java.util.stream.Collectors类内置的静态方法。

3.6.1 归集(toList/toMap/toSet)

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

案例一 toListtoSet的简单用法:

public class StreamTest {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20);
        // 2的倍数 归集list
        List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList());
        // 2的倍数 归集set
        Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet());

        System.out.println("toList:" + listNew);
        System.out.println("toSet:" + set);
    }
}

运行结果:

toList:[6, 4, 6, 6, 20]
toSet:[4, 20, 6]

案例二:toMap演示

public class StreamTest {
    public static void main(String[] args) {
       List<Employee> employeeList = init();

       Map<String, Employee> map = employeeList.stream()
               .filter(p -> p.getSalary() > 8000)
               .collect(Collectors.toMap(Employee::getName, p -> p));
       System.out.println("toMap1:" + JSONObject.toJSONString(map));

       System.out.println("--------------------------------------");

       //当employeeList中有key重复时:会报错:Duplicate key Employee(name=Alisa, salary=7900, ...)
       employeeList.add(new Employee("Alisa", 2900, 26, "female", "New York"));
       employeeList.add(new Employee("Alisa", 3900, 26, "female", "New York"));

       Map<String, Employee> map2 = employeeList.stream().collect(
               Collectors.toMap(Employee::getName, Function.identity(), (p1, p2) -> p2));
       // 注1:Function.identity() 等于 p->p
       // 注2:(p1, p2) -> p1 取list中重复键中 前面的值
       //     (p1, p2) -> p2 取list中重复键中 后面的值
       System.out.println("toMap2:" + JSONObject.toJSONString(map2));

       System.out.println("--------------------------------------");

       employeeList = init();
       Map<String, String> map3 = employeeList.stream().collect(
               Collectors.toMap(Employee::getName, Employee::getCity));
       System.out.println("toMap3:" + JSONObject.toJSONString(map3));

       // city可以为空字符串但不能为null,否则会报空指针错误,解决方案:
       Map<String, String> map4 = employeeList.stream().collect(
               Collectors.toMap(Employee::getName, p -> p.getCity() == null ? "" : p.getCity()));
       System.out.println("toMap4:" + JSONObject.toJSONString(map4));

       employeeList.add(new Employee("Alisa", 3900, 26, "female", "New York"));
       //重复key,2个value映射到同一key里
       Map<String, String> map5 = employeeList.stream().collect(
               Collectors.toMap(Employee::getName, Employee::getCity,
                       (p1, p2) -> p1 +"," + p2));
       System.out.println("toMap5:" + JSONObject.toJSONString(map5));
    }
}

运行结果:

toMap1:{
    "Tom":{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
    "Owen":{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"},
    "Anni":{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"}
    }
--------------------------------------
toMap2:{
    "Tom":{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
    "Owen":{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"},
    "Anni":{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
    "Alisa":{"age":26,"city":"New York","name":"Alisa","salary":3900,"sex":"female"},
    "Jack":{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
    "Lily":{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"}}
--------------------------------------
toMap3:{
    "Tom":"New York",
    "Owen":"New York",
    "Anni":"New York",
    "Alisa":"New York",
    "Jack":"Washington",
    "Lily":"Washington"}
toMap4:{
    "Tom":"New York",
    "Owen":"New York",
    "Anni":"New York",
    "Alisa":"New York",
    "Jack":"Washington",
    "Lily":"Washington"}
toMap5:{
    "Tom":"New York",
    "Owen":"New York",
    "Anni":"New York",
    "Alisa":"New York,New York",
    "Jack":"Washington",
    "Lily":"Washington"}

案例三:toMap演示

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();
		
        //按照员工姓名去重,求出总工资
        List<Employee> collect = employeeList.stream().collect(
                Collectors.toMap(Employee::getName, a -> a, (o1, o2) -> {
                    o1.setSalary(o1.getSalary() + o2.getSalary());
                    return o1;
                })).values().stream().collect(Collectors.toList());
        System.out.println("去重求和:" + JSONObject.toJSONString(collect));
}

运行结果:

去重求和:[{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"},
{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
{"age":26,"city":"New York","name":"Alisa","salary":11800,"sex":"female"},
{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"}]

3.6.2 统计(count/averaging)

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

  1. 计数:count
  2. 平均值:averagingIntaveragingLongaveragingDouble
  3. 最值:maxByminBy
  4. 求和:summingIntsummingLongsummingDouble
  5. 统计以上所有:summarizingIntsummarizingLongsummarizingDouble

案例:统计员工人数、平均工资、工资总额、最高工资。

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();
        // 求总数 类似list.size()
        Long count = employeeList.stream().collect(Collectors.counting());
       // 将员工先按性别分组,再统计数量
       Map<Integer, Long> count2 = employeeList.stream()
               .collect(Collectors.groupingBy(Employee::getSex, Collectors.counting()));
        // 求平均工资
        Double average = employeeList.stream()
                .collect(Collectors.averagingDouble(Employee::getSalary));
        // 求最高工资
        Optional<Integer> max = employeeList.stream()
                .map(Employee::getSalary)
                .collect(Collectors.maxBy(Integer::compare));
        // 求工资之和
        Integer sum = employeeList.stream().collect(Collectors.summingInt(Employee::getSalary));
        // 一次性统计所有信息
        DoubleSummaryStatistics collect = employeeList.stream()
                .collect(Collectors.summarizingDouble(Employee::getSalary));

        System.out.println("员工总数:" + count);
        System.out.println("员工性别、总数:" + JSONObject.toJSONString(count2));
        System.out.println("员工平均工资:" + average);
        System.out.println("员工最高工资:" + max.get());
        System.out.println("员工工资总和:" + sum);
        System.out.println("员工工资所有统计:" + JSONObject.toJSONString(collect));
    }
}

运行结果:

员工总数:6
员工性别、总数:{"female":3,"male":3}
员工平均工资:8216.666666666666
员工最高工资:9500
员工工资总和:49300
员工工资所有统计:{"average":8216.666666666666,"count":6,"max":9500.0,"min":7000.0,"sum":49300.0}

3.6.3 分组(partitioningBy/groupingBy)

  1. 分组:将集合分为多个Map,比如员工按性别分组。有单级分组和多级分组。类似于数据库中的group by
  2. 分区:将stream按条件分为两个Map,比如员工按薪资是否高于8000分为两部分。分区可以看做是分组的一种特殊情况,在分区中key只有两种情况:truefalse,目的是将待分区集合按照条件一分为二,java8的流式处理利用Collectors.partitioningBy()方法实现分区。

案例:将员工按薪资是否高于8000分为两部分;将员工按性别和城市分组

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();
        List<Employee> employeeList2 = init();

        // 将员工按薪资是否高于8000分组
        Map<Boolean, List<Employee>> part = employeeList.stream()
                .collect(Collectors.partitioningBy(x -> x.getSalary() > 8000));
        // 将员工按性别分组
        Map<String, List<Employee>> group = employeeList.stream()
                .collect(Collectors.groupingBy(Employee::getSex));
        // 将员工先按性别分组,再按城市分组
        Map<String, Map<String, List<Employee>>> group2 = employeeList.stream()
                .collect(Collectors.groupingBy(Employee::getSex, 
                        Collectors.groupingBy(Employee::getCity)));
        	
        // 多个数组中符合年龄要求的按照城市分组
        Map<String, List<Employee>> group3 = Stream.of(employeeList, employeeList2)
                .flatMap(e -> e.stream().filter(s -> s.getAge() < 17))
                .collect(Collectors.groupingBy(Employee::getCity));
		
        // 多个数组按照是否年龄大于23进行分组,key为true和false
        ConcurrentMap<Boolean, List<Employee>> group4 = Stream.of(employeeList, employeeList2)
                .flatMap(Collection::stream)
                .collect(Collectors.groupingByConcurrent(p -> p.getAge() > 23));
       
        System.out.println("员工按薪资是否大于8000分组情况:" + JSONObject.toJSONString(part));
        System.out.println("员工按性别分组情况:" + JSONObject.toJSONString(group));
        System.out.println("员工按性别、城市:" + JSONObject.toJSONString(group2));
        System.out.println("多数组中年龄小于23的按照城市分组:" + JSONObject.toJSONString(group3));
        System.out.println("多数组中年龄大于23进行分组:" + JSONObject.toJSONString(group4));
    }
}

输出结果:

员工按薪资是否大于8000分组情况:{
    false:[{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
           {"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"},
           {"age":26,"city":"New York","name":"Alisa","salary":7900,"sex":"female"}],
    true:[{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
          {"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
          {"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}]}
员工按性别分组情况:{
    "female":[{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"},
              {"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
              {"age":26,"city":"New York","name":"Alisa","salary":7900,"sex":"female"}],
    "male":[{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
            {"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
            {"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}]}
员工按性别、城市:{
    "female":{"New York":[{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
                          {"age":26,"city":"New York","name":"Alisa","salary":7900,"sex":"female"}],
            "Washington":[{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"}]},
    "male":{"New York":[{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
                        {"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}],
            "Washington":[{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"}]}}
多数组中年龄小于23的按照城市分组:{
    "Washington":[{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"},
                  {"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"}]}
多数组中年龄大于23进行分组:{
    false:[{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
           {"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
           {"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"},
           {"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
           {"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"},
           {"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
           {"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
           {"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"},
           {"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
           {"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}],
    true:[{"age":26,"city":"New York","name":"Alisa","salary":7900,"sex":"female"},
          {"age":26,"city":"New York","name":"Alisa","salary":7900,"sex":"female"}]}

3.6.4 接合(joining)

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

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();

        String names = employeeList.stream().map(Employee::getName).collect(Collectors.joining(","));
        String names2 = employeeList.stream().map(Employee::getName)
            	    	    	    .collect(Collectors.joining(",", "[", "]"));
    	System.out.println("所有员工的姓名:" + names);
        System.out.println("所有员工的姓名2:" + names2);

    	List<String> list = Arrays.asList("A", "B", "C");
    	String string = list.stream().collect(Collectors.joining("-"));
    	System.out.println("拼接后的字符串:" + string);
    }
}

运行结果:

所有员工的姓名:Tom,Jack,Lily,Anni,Owen,Alisa
所有员工的姓名2:[Tom,Jack,Lily,Anni,Owen,Alisa]
拼接后的字符串:A-B-C

3.6.5 归约(reducing)

Collectors类提供的reducing方法,相比于stream本身的reduce方法,增加了对自定义归约的支持。

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();

        // 每个员工减去起征点后的薪资之和(这个例子并不严谨,但一时没想到好的例子)
        Integer sum = employeeList.stream()
                .collect(Collectors.reducing(0, Employee::getSalary, (i, j) -> (i + j - 5000)));
        Integer sum2 = employeeList.stream()
                .map(Employee::getSalary)
                .reduce(0, (i, j) -> (i + j - 5000));
        System.out.println("员工扣税薪资总和:" + sum);
        System.out.println("员工扣税薪资总和:" + sum2);
		
        // stream的reduce
        Optional<Integer> sum3 = employeeList.stream().map(Employee::getSalary).reduce(Integer::sum);
        System.out.println("员工薪资总和:" + sum3.get());
    }
}

运行结果:

员工扣税薪资总和:19300
员工扣税薪资总和:19300
员工薪资总和:49300

3.6.6 映射(mapping)

import java.util.stream.Collectors;

public class StreamTest {
   public static void main(String[] args) {
      List<Employee> employeeList = init();
      // 获得所有员工姓名
      List<String> collect = employeeList.stream().map(CustomerVO::getName)
              .collect(Collectors.toList());
      String collect2 = employeeList.stream()
              .collect(Collectors.mapping(CustomerVO::getName, Collectors.joining(",", "[", "]")));
      System.out.println("获得所有员工姓名:" + JSONObject.toJSONString(collect));

      Map<Integer, List<String>> mapping = employeeList.stream()
              .collect(Collectors.groupingBy(CustomerVO::getAge, 
                      Collectors.mapping(CustomerVO::getName, Collectors.toList())));
      System.out.println("年龄分组后,获得所有员工姓名:" + JSONObject.toJSONString(mapping));
   }
}

3.6.7 归纳(collectingAndThen)

先进行结果集的收集,然后将收集到的结果集进行下一步的处理。

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();
        // 根据城市去重,并且排序
        List<Employee> unique = employeeList.stream()
                .collect(Collectors.collectingAndThen(Collectors.toCollection(() -> 
                        new TreeSet<>(Comparator.comparing(Employee::getCity))), ArrayList::new));
        System.out.println("按照城市去重,排序:" + JSONObject.toJSONString(unique));
    }
}

运行结果:

按照城市去重,排序:[{"age":21,"city":"Washington","name":"Lily","salary":8900,"sex":"female"},
                {"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"}]

如果多列去重,修改Comparator条件

Comparator.comparing(o -> o.getName() + ";" + o.getCity()))

3.6.8 并集(teeing)

这是一种新的静态方法,用于加入java.util.stream.Collectors接口,该接口允许使用两个独立的收集器进行收集,然后使用提供的BiFunction合并其结果。传递给结果收集器的每个元素均由两个下游收集器处理,然后使用指定的合并函数将其结果合并为最终结果。

请注意,此函数有助于一步完成特定任务。如果不使用teeing()函数,可以分两步执行给定的任务。它只是一个帮助函数,可以帮助减少冗长程度。

  1. 查找最高和最低工资员工
public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();
        // 查找最高和最低工资员工
        HashMap<String, Employee> result = employeeList.stream()
                .collect(Collectors.teeing(Collectors.maxBy(Comparator.comparing(Employee::getSalary)), 
                        Collectors.minBy(Comparator.comparing(Employee::getSalary)), (e1, e2) -> { 
                    HashMap<String, Employee> map = new HashMap();
                    map.put("max", e1.get());
                    map.put("min", e2.get());
                    return map;
                }
        ));
        System.out.println("最高和最低工资员工:" + JSONObject.toJSONString(result));
    }
}

运行结果:

最高和最低工资员工:
{
    "min": {"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"}, 
    "max": {"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}
}
  1. 过滤项目并计数
public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();
        // 所有薪水高于8500的员工和数量
        HashMap<String, Employee> result = employeeList.stream()
                .collect(Collectors.teeing(
                        Collectors.filtering(e -> e.getSalary() > 8500, Collectors.toList()), 
                        Collectors.filtering(e -> e.getSalary() > 8500, Collectors.counting()), 
                        (list, count) -> { 
                    HashMap<String, Object> map = new HashMap();
                    map.put("list", list);
                    map.put("count", count);
                    return map;
                }
        ));
        System.out.println("所有薪水高于8500的员工和数量:" + JSONObject.toJSONString(result));
    }
}

运行结果:

所有薪水高于8500的员工和数量:
{
    "count": 2, 
    "list": [{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
            {"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}]
}

3.7 排序(sorted)

sorted,中间操作。有两种排序:

  • sorted():自然排序,流中元素需实现Comparable接口。
  • sorted(Comparator com)Comparator排序器自定义排序。

案例:将员工按工资由高到低(工资一样则按年龄由大到小)排序。

import java.util.Comparator;

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();

        // 按工资升序排序(自然排序)
        // nullsLast: 表示将排序字段中的null值放到集合最后面
        // nullsFirst: 表示将排序字段中的null值放到集合最前面
        List<String> newList = employeeList.stream()
                .sorted(Comparator.comparing(Employee::getSalary, 
                        Comparator.nullsLast(Comparator.naturalOrder())))
                .map(Employee::getName).collect(Collectors.toList());
        
        // 按工资倒序排序
        List<String> newList2 = employeeList.stream()
                .sorted(Comparator.comparing(Employee::getSalary).reversed())
                .map(Employee::getName).collect(Collectors.toList());
        
        // 先按工资再按年龄升序排序
        List<String> newList3 = employeeList.stream().sorted(Comparator.comparing(Employee::getSalary)
                .thenComparing(Employee::getAge))
                .map(Employee::getName).collect(Collectors.toList());
        
        // 先按工资(降序)再按年龄(降序)自定义排序
        List<String> newList4 = employeeList.stream().sorted((p1, p2) -> {
            if (p1.getSalary() == p2.getSalary()) {
                return p2.getAge() - p1.getAge();
            } else {
                return p2.getSalary() - p1.getSalary();
            }
        }).map(Employee::getName).collect(Collectors.toList());

        System.out.println("按工资升序排序:" + newList);
        System.out.println("按工资降序排序:" + newList2);
        System.out.println("先按工资再按年龄升序排序:" + newList3);
        System.out.println("先按工资(降序)再按年龄(降序)自定义排序:" + newList4);
    }
}

运行结果:

按工资升序排序:[Jack, Lily, Alisa, Anni, Tom, Owen]
按工资降序排序:[Owen, Tom, Anni, Alisa, Lily, Jack]
先按工资再按年龄升序排序:[Jack, Lily, Alisa, Anni, Tom, Owen]
先按工资(降序)再按年龄(降序)自定义排序:[Owen, Tom, Anni, Alisa, Lily, Jack]

更多排序参考Java集合排序Comparable和Comparator

3.8 提取/组合

流也可以进行合并、去重、限制、跳过等操作。

public class StreamTest {
    public static void main(String[] args) {
        String[] arr1 = { "a", "b", "c", "d" };
        String[] arr2 = { "d", "e", "f", "g" };
		
        Stream<String> stream1 = Stream.of(arr1);
        Stream<String> stream2 = Stream.of(arr2);
        // concat: 合并两个流 distinct: 去重
        List<String> newList = Stream.concat(stream1, stream2).distinct().collect(Collectors.toList());
		
        // limit: 限制从流中获得前n个数据(从1开始每个元素加2)
        List<Integer> collect = Stream.iterate(1, x -> x + 2).limit(10).collect(Collectors.toList());
        // skip: 跳过前n个数据
        List<Integer> collect2 = Stream.iterate(1, x -> x + 2).skip(1)
            	    	    	     .limit(5).collect(Collectors.toList());
        
        System.out.println("流合并:" + newList);
        System.out.println("limit:" + collect);
        System.out.println("skip:" + collect2);
    }
}

运行结果:

流合并:[a, b, c, d, e, f, g]
limit:[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
skip:[3, 5, 7, 9, 11]

3.9 消费(peek)

peek的操作是返回一个新的Stream的,且设计的初衷是用来debug调试的,因此使用steam.peek()必须对流进行一次处理再产生一个新的Stream

public class StreamTest {
    public static void main(String[] args) {
        List<Employee> employeeList = init();

        System.out.println(JSONObject.toJSONString(employeeList));

        System.out.println(JSONObject.toJSONString(getEmployeeTreeList(init())));

        System.out.println(JSONObject.toJSONString(getEmployeeTreeListPeek(employeeList)));
    }

    private static List<Employee> init() {
        List<Employee> employeeList = Lists.newArrayList();
        employeeList.add(new Employee(1001, "节点一", 0));
        employeeList.add(new Employee(1002, "节点二", 0));
        employeeList.add(new Employee(2001, "节点一的子节点1", 1001));
        employeeList.add(new Employee(2002, "节点一的子节点2", 1001));
        employeeList.add(new Employee(2003, "节点一的子节点3", 1001));
        employeeList.add(new Employee(2004, "节点二的子节点1", 1002));
        employeeList.add(new Employee(2005, "节点二的子节点2", 1002));
        employeeList.add(new Employee(3001, "节点一的子节点1的子节点1", 2001));
        employeeList.add(new Employee(3002, "节点一的子节点1的子节点2", 2001));
        employeeList.add(new Employee(3003, "节点一的子节点2的子节点", 2002));
        employeeList.add(new Employee(4001, "节点一的子节点1的子节点1的子节点", 3001));
        return employeeList;
    }

    // 递归
    private static List<Employee> getEmployeeTreeList(List<Employee> employeeList) {
        List<Employee> rootMenus = employeeList.stream()
                .filter(e -> e.getParentId().equals(0))
                .collect(Collectors.toList());
        rootMenus.forEach(employee -> {
            getChildrenList(employee, employeeList);
        });
        return rootMenus;
    }

    private static void getChildrenList(Employee employee, List<Employee> employeeList) {
        List<Employee> children = employeeList.stream()
                .filter(e -> e.getParentId().equals(employee.getId()))
                .collect(Collectors.toList());

        children.stream().forEach(child -> {
            List<Employee> cd = employeeList.stream()
                    .filter(e -> e.getParentId().equals(child.getId()))
                    .collect(Collectors.toList());
            getChildrenList(child, employeeList);
            child.setChildren(cd);
        });

        employee.setChildren(children);
    }

    // 以下是peek递归
    private static List<Employee> getEmployeeTreeListPeek(List<Employee> employeeList) {
        if (CollectionUtils.isNotEmpty(employeeList)) {
            // 递归获取菜单树形结构
            // 获取父节点,说明:父节点的parentId都是0
            return employeeList.stream().filter((Employee m) -> m.getParentId() == 0)
                    .peek((Employee m) -> m.setChildren(getChildrenListPeek(m, employeeList)))
                    .collect(Collectors.toList());
        }
        return Collections.emptyList();
    }

    private static List<Employee> getChildrenListPeek(Employee employee, List<Employee> employeeList) {
        // 子节点parentId = 父节点ID
        return employeeList.stream().filter((Employee e) -> employee.getId() == (e.getParentId()))
                .peek((Employee e1) -> e1.setChildren(getChildrenListPeek(e1, employeeList)))
                .collect(Collectors.toList());
    }

    @Data
    @AllArgsConstructor
    public static class Employee {
        private int id;
        private String value;
        private Integer parentId;
        private List<Employee> children;

        public Employee(int id, String value, int parentId) {
            this.id = id;
            this.value = value;
            this.parentId = parentId;
        }
    }
}

peek构建的tree,工具类可以参考通用TreeUtil工具

四、拓展

4.1 peek与map的区别

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

peek接收一个Consumer,而map接收一个Function

  • map:用于对流中的每个元素进行映射处理,然后再形成新的流;
  • peek:用于debug调试流中间结果,不能形成新的流,但能修改引用类型字段的值;

Consumer是没有返回值的,它只是对Stream中的元素进行某些操作,但是操作之后的数据并不返回到Stream中,所以Stream中的元素还是原来的元素。

Function是有返回值的,这意味着对于Stream的元素的所有操作都会作为新的结果返回到Stream中。

这就是为什么peek String不会发生变化而peek Object会发送变化的原因。

4.2 peek和foreach区别

  • peek:会继续返回Stream对象。
  • forEach:返回void,结束Stream操作。

参考文章

posted @ 2022-04-25 15:41  夏尔_717  阅读(751)  评论(0编辑  收藏  举报