Bootstrap

Java8 Stream 流

简介

Java8两大新特征,一是支持 lambda 表达式,二是 Stream API。在使用 Stream API 之前,最好有 lambda表达式的基础,如果不太清楚,可以看我的另一篇博客lambda表达式

在编写代码的过程中,我们经常要对集合或数组数据进行操作,而有了 Stream API,我们能够非常轻松的对集合、数据进行映射、过滤、查找等操作,使得我们能够像操作数据库一样的操作集合。

Stream 本身并不存储元素,它并不改变源数据,每次操作都会形成一个新的流,并且只有执行了Stream的终止操作,中间的过滤、查找等操作才会执行。

集合注重存储,Stream注重计算。集合是基于内存层面的,而Stream是基于CPU的。

Stream 操作的步骤

  • 创建
  • 中间操作
  • 终止操作

只有执行了终止操作,中间操作才会执行。这也是 Stream 的延迟执行的体现。

在这里插入图片描述

Stream 的创建

通过集合创建

default Stream stream() {} 返回一个顺序流

	@Test
    public void test() {
        List<String> list = new ArrayList<>();
        list.add("H");
        list.add("E");
        list.add("l");
        list.add("l");
        list.add("O");
        Stream<String> stream = list.stream();
    }

通过数组创建

通过 Arrays 的静态方法进行创建

public static Stream stream(T[] array) {}

	@Test
    public void test() {
        Integer[] arr = {1,2,3,4,5,6,7,8,9};
        Stream<Integer> stream = Arrays.stream(arr);
    }

顺便一提,在 Arrays 中,帮我们重载了此方法,根据数组类型不同,返回不同的类型的流。

  • public static IntStream stream(int[] array)
  • public static LongStream stream(long[] array)
  • public static DoubleStream stream(double[] array)
    @Test
    public void test() {
        int[] ints = {1,2,3};
        IntStream intStream = Arrays.stream(ints);
        double[] doubles = {1.1,1.2,1.3};
        DoubleStream doubleStream = Arrays.stream(doubles);
        long[] longs = {1L,2L,3L};
        LongStream stream = Arrays.stream(longs);
    }

使用 of 方法创建

使用 Stream 的静态方法进行创建

public static < T > Stream< T > of(T… values) {}

	@Test
    public void test() {
        Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6);
    }

创建无限流

可以使用 Stream 的静态方法 Stream.iterate() 和 Stream.generate(),创建无限流。

  • public static Stream iterate(final T seed, final UnaryOperator< T > f) {} 此方法用于迭代
  • public static Stream generate(Supplier< T > s) {} 此方法用于生成

为什么说是无限流呢? 因为在创建无限流时,如果没有使用终止操作,那么这个流的中间操作,会一直执行。

generate(Supplier< T > s) 方法用于生成元素,方法传入一个提供者接口,是一个函数式接口,我们可以用 lambda 和方法引用来简化代码。

举例:无限生成随机数

    @Test
    public void test() {
        // generate
        Stream<Double> generate = Stream.generate(Math::random);
        generate.forEach(System.out::println);
    }

iterate(final T seed, final UnaryOperator< T > f) 方法用于迭代元素,第一个参数是一个种子数,也就是元素的起始值。第二个参数是一个功能型函数式接口,我们也可以使用 lambda 和方法引用来简化代码。如不太清楚功能型函数式接口,请移步到 另一篇博客

举例:无限迭代生成的元素

    @Test
    public void test() {
    	// iterator
        Stream<Integer> iterate = Stream.iterate(0, ele -> ele + 2);
        iterate.forEach(System.out::println);

    }

想要让无限流停止,我们可以对流进行一些中间操作,使其停止即可。

Stream 的中间操作

筛选与切片

filter(Predicate p) ,接收断言型函数式接口,对流中的元素进行过滤。

	@Test
    public void test() {
        List<String> list = new ArrayList<>();
        list.add("春天");
        list.add("春风");
        list.add("春色");
        list.add("春意");
        list.add("秋天");

        list.stream().filter(e-> e.contains("天")).forEach(System.out::println);
    }

distinct(),对流进行去重。

	@Test
    public void test() {
        List<String> list = new ArrayList<>();
        list.add("春天");
        list.add("春天");
        list.add("春色");
        list.add("春意");
        list.add("秋天");
        list.stream().distinct().forEach(System.out::println);
    }

limit(long maxSize) ,限制流中的元素数量。可以使用 limit方法来终止无限流。

    @Test
    public void test() {
        Stream<Double> generate = Stream.generate(Math::random);
        generate.limit(10).forEach(System.out::println);
    }

skip(long n) 跳过流中的 n 个元素。

    @Test
    public void test() {
        List<String> list = new ArrayList<>();
        list.add("春天");
        list.add("春天");
        list.add("春色");
        list.add("春意");
        list.add("秋天");
        list.stream().skip(2L).forEach(System.out::println);
    }

映射

map(Function f) ; 接收一个功能型函数式接口的实现类,该函数式接口中的抽象方法会被用到流中的每一个元素上。通常使用 map(Function f) 方法对流中的数据进行处理、提取、转换成其他对象等操作。

举例:使用 map 将流中的字符串映射成大写的,然后通过 filter 过滤字符串长度小于5的进行遍历输出。

    @Test
    public void test() {
        List<String> list = Arrays.asList("hello", "world", "ni", "hao", "shi", "jie", "!");
        list.stream()
                .map(String::toUpperCase)
                .filter(e -> e.length() < 5)
                .forEach(System.out::println);
    }

flatMap(Function f); flatMap 的功能和 map 类似,都是将方法应用到流中的每个元素上。但是有一点区别,当流中的元素还是一个流时,map 会将流中的流看做一个对象处理。而flatMap会将流中的流拆开,将方法也应用到流中的流的各个元素上。

类比: List 的 add() 和 addAll() 方法

    @Test
    public void test() {
        List list1 = new ArrayList();
        list1.add(1);
        list1.add(2);
        list1.add(3);

        List list2 = new ArrayList();
        list2.add(4);
        list2.add(5);
        list2.add(6);

        list1.add(list2);
        System.out.println(list1.size()); // 4
        list1.addAll(list2);
        System.out.println(list1.size()); // 7
    }

add方法如果添加的还是一个集合时,则会将集合当做一个对象添加进去。相当于 map。
addAll方法如果添加的还是一个集合时,则会将集合拆开把元素一一加到 list 中。相当于 flatMap。

举个例子。在映射时,将流中的字符串全部转换成字符流。如果是用 map 映射,则将映射后的元素看做一个整体,然后执行后序流程。如果是用 flatMap 映射,则流中的元素如果还是流,那么会将流拆开,再执行后序操作。

    @Test
    public void test() {
        List<String> list = new ArrayList<>();
        list.add("hello");
        list.add(" hi");
        System.out.println("*****直接map*****");
        Stream<Stream<Character>> streamStream1 = list.stream().map(this::strToCharacter);
        streamStream1.forEach(System.out::println);
        System.out.println("*****两个forEach等同于 flatMap *****");
        Stream<Stream<Character>> streamStream2 = list.stream().map(this::strToCharacter);
        streamStream2.forEach(strStream -> {
            strStream.forEach(System.out::print);
        });
        System.out.println();
        System.out.println("*****直接 flatMap *****");
        Stream<Character> characterStream = list.stream().flatMap(this::strToCharacter);
        characterStream.forEach(System.out::print);
    }

    /**
     * 将字符串转换成字符流
     * @param str 输入的字符串
     * @return 字符流
     */
    public Stream<Character> strToCharacter(String str) {
        ArrayList<Character> list = new ArrayList<>();
        for (Character c : str.toCharArray()) {
            list.add(c);
        }
        return list.stream();
    }

结果入下:
在这里插入图片描述

排序

流的排序就比较简单了。在 Java 中,涉及到排序,无外乎就两种,一种自然排序,实现 Comparable 接口,一种定制排序,实现 Comparator 接口。

  • Stream< T > sorted(); 使用自然排序
  • Stream< T > sorted(Comparator<? super T> comparator); 使用定制排序

这里就以定制排序举例了。

先整写测试数据:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
    private int id;
    private String name;
    private int age;
    private double salary;
}
//---------------------------------
public class EmployeeData {
	public static List<Employee> getEmployees(){
		List<Employee> list = new ArrayList<>();
		list.add(new Employee(1001, "马化腾", 34, 6000.38));
		list.add(new Employee(1001, "朱化腾", 34, 7000.38));
		list.add(new Employee(1002, "马云", 12, 9876.12));
		list.add(new Employee(1002, "郝云", 15, 9999.12));
		list.add(new Employee(1003, "刘强东", 33, 3000.82));
		list.add(new Employee(1004, "雷军", 26, 7657.37));
		list.add(new Employee(1005, "李彦宏", 65, 5555.32));
		list.add(new Employee(1006, "比尔盖茨", 42, 9500.43));
		list.add(new Employee(1007, "任正非", 26, 4333.32));
		list.add(new Employee(1008, "扎克伯格", 35, 2500.32));
		return list;
	}
}

先根据 id 排序,如果 id 相同,再根据工资排序。

    @Test
    public void test() {
        List<Employee> employees = EmployeeData.getEmployees();
        employees.stream().sorted((e1, e2) -> {
            int compare = Integer.compare(e1.getId(), e2.getId());
            if (compare != 0) {
                return compare;
            } else {
                return Double.compare(e1.getSalary(), e2.getSalary());
            }
        }).forEach(System.out::println);
    }

再举一个例子,比如我现在有两个集合,其中两个集合里面的元素相同,但是顺序不同。现在要让一个集合中元素的顺序和另一个集合保持一致,也可以使用排序完成。

public class SortTest {
    static List<Integer> orderIdList = new ArrayList<>();
    static List<Integer> randomIdList = new ArrayList<>();
    static {
        orderIdList.add(7);
        orderIdList.add(8);
        orderIdList.add(9);
        orderIdList.add(4);
        orderIdList.add(5);
        orderIdList.add(6);
        orderIdList.add(1);
        orderIdList.add(2);
        orderIdList.add(3);

        randomIdList.add(1);
        randomIdList.add(4);
        randomIdList.add(7);
        randomIdList.add(2);
        randomIdList.add(5);
        randomIdList.add(8);
        randomIdList.add(3);
        randomIdList.add(6);
        randomIdList.add(9);
    }

    @Test
    public void testSortCollection() {
        List<Integer> collect = randomIdList.stream().sorted((e1, e2) -> {
            Integer orderIndex1 = orderIdList.indexOf(e1);
            Integer orderIndex2 = orderIdList.indexOf(e2);
            return orderIndex1 - orderIndex2;
        }).collect(Collectors.toList());
        System.out.println(collect);
    }
}

在这里插入图片描述
可以看到, randomIdList 集合中元素的顺序和 orderIdList 保持一致了。

如果想要倒序,则在 orderIndex1 - orderIndex2 前一个负号就行,这里就不演示了。

不过上述流的操作有点臃肿,其实一行代码就可以搞定。

		// 这里使用 Comparator.comparingInt 传入的 int 值是 randomIdList 中元素在 orderIdList 中的下标
        // 这样 Comparator 会根据 randomIdList 中元素在 orderIdList 中的下标进行排序,这样就实现了和 orderIdList 顺序一致
        // 再说细一点,我现在 randomIdList 中 1 4 7 2 5 8 3 6 9 元素在 orderIdList 中的下标分别是 6 3 0 7 4 1 8 5 2
        // 这样 Comparator 就会根据这些下标排序,这样元素顺序又会和 orderIdList 一致了
        // 升序下标对应就是 0 1 2 3 4 5 6 7 8 9,元素的值对应就是 7 8 9 4 5 6 1 2 3
        // 降序下标对应就是 9 8 7 6 5 4 3 2 1,元素的值对应就是 3 2 1 6 5 4 9 8 7
        // 打印输出验证下
        // 根据下标升序
        System.out.println(randomIdList.stream().sorted(Comparator.comparingInt(t -> orderIdList.indexOf(t))).collect(Collectors.toList()));
        // 根据下标降序
        System.out.println(randomIdList.stream().sorted(Comparator.comparingInt(t -> -orderIdList.indexOf(t))).collect(Collectors.toList()));

在这里插入图片描述

Stream 的终止操作

匹配与查找

方法描述
allMatch(Predicate p)检查流中所有元素是否匹配自定义的条件
anyMatch(Predicate p)检查是否至少匹配一个元素
noneMatch(Predicate p)返回第一个元素
count()返回流中元素总数
max(Comparator c)返回流中最小值
forEach(Consumer c)内部迭代

匹配与查找比较简单,这里就举第一个例子。

    @Test
    public void test() {
        List<String> stringList = Arrays.asList("hello","world","hi","word");
        boolean b = stringList.stream().allMatch(str -> str.length() > 5);
        System.out.println(b); // false
    }

归约

方法描述
reduce(T iden, BinaryOperator b)可以将流中元素反复结合起来,得到一个值。返回 T,第一个参数是初始值
reduce(BinaryOperator b)可以将流中元素反复结合起来,得到一个值。返回 Optional

reduce 会帮我们遍历流中的元素。

    @Test
    public void test() {
        List<String> stringList = Arrays.asList("hello","world","hi","word");
        String reduce1 = stringList.stream().reduce("", (s1, s2) -> String.valueOf(s1.length() + s2.length()));
        System.out.println(reduce1);
        
        List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        // Optional 是 Java8 新增的防止空指针的类,通过 get() 方法可以获取到包装的值
        Optional<Integer> reduce2 = integerList.stream().reduce((i1, i2) -> {
            return i1 + i2;
        });
        System.out.println(reduce2.get());
        // 上述代码等同于下面代码
        Optional<Integer> reduce3 = integerList.stream().reduce(Integer::sum);
        System.out.println(reduce3.get());
    }

在这里插入图片描述
这里 String 流输出的值是5。 因为字符串默认值为 “”,那么第一次就是0+5,那么 s1 就变成了 “5”; 第二次去就是 “5”.length() + “world”.length(),再String.valueOf()一下,就变成了 “6”,那么永远就是 1 + 下一个字符串的长度了。最终输出 5。

收集

收集在开发中也是非常实用的终止操作。常常用来对流进行中间操作最后收集生成集合。

方法描述
collect(Collector c)将流转换为其他形式。参数一个 Collector接口的实现,用于给Stream中元素做汇总的方法。

Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。

在开发中我们往往不去手写 Collector 的实现类,而用 Java 8 新增的 Collectors 中的静态方法来创建 Collector 的实现类。Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法如下表所示:

方法返回类型作用举例
toListList把流中元素收集到ListList emps = list.stream().collect(Collectors.toList());
toSetSet把流中元素收集到SetSet emps= list.stream().collect(Collectors.toSet());
toCollectionCollection把流中元素收集到创建的集合Collection emps = list.stream().collect(Collectors.toCollection(ArrayList::new));
countingLong计算流中元素的个数long count = list.stream().collect(Collectors.counting());
summingIntInteger对流中元素的整数属性求和int total=list.stream().collect(Collectors.summingInt(Employee::getSalary));
averagingIntDouble计算流中元素Integer属性的平均值double avg = list.stream().collect(Collectors.averagingInt(Employee::getSalary));
summarizingIntIntSummaryStatistics收集流中Integer属性的统计值。int SummaryStatisticsiss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary));
joiningString连接流中每个字符串String str= list.stream().map(Employee::getName).collect(Collectors.joining());
maxByOptional< T >根据比较器选择最大值Optional< Employee >max= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary)));
minByOptional< T >根据比较器选择最小值Optional min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary)));
reducing归约产生的类型从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值int total=list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum));
collectingAndThen转换函数返回的类型包裹另一个收集器,对其结果转换函数int how= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
groupingByMap<K, List< T >>根据某属性值对流分组,属性为K,结果为VMap<Emp.Status, List< Employee >> map= list.stream().collect(Collectors.groupingBy(Employee::getStatus));
partitioningByMap<Boolean, List< T >>根据true或false进行分区Map<Boolean,List> vd = list.stream().collect(Collectors.partitioningBy(Employee::getManage));

这里就拿 groupingBy() 方法举例。

假如我要将员工根据 id 进行分组,保存到 map 中,key 是员工的 id,value 就是这些员工,那么就可以这么操作。

@Test
    public void test() {
        List<Employee> employees = EmployeeData.getEmployees();
        Map<Integer, List<Employee>> collect = employees.stream().collect(Collectors.groupingBy(Employee::getId));
        System.out.println(collect);
    }

总结

Stream API 还是很实用的,最好能够将其掌握,不仅开发效率高,还装x。

;