Stream流操作

128 阅读6分钟

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();
}

通过数组创建

@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);
    }
}

image-20230411163557059

可以看到, 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()));

image-20230411163727098

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());
}

image-20230411165059665

这里 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 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法如下表所示:

返回类型方法作用举例
ListtoList把流中元素收集到ListList emps = list.stream().collect(Collectors.toList());
SettoSet把流中元素收集到SetSet emps= list.stream().collect(Collectors.toSet());
CollectiontoCollection把流中元素收集到创建的集合Collection emps = list.stream().collect(Collectors.toCollection(ArrayList::new));
Longcounting计算流中元素的个数long count = list.stream().collect(Collectors.counting());
IntegersummingInt对流中元素的整数属性求和int total=list.stream().collect(Collectors.summingInt(Employee::getSalary));
DoubleaveragingInt计算流中元素Integer属性的平均值double avg = list.stream().collect(Collectors.averagingInt(Employee::getSalary));
Stringjoining连接流中每个字符串String str= list.stream().map(Employee::getName).collect(Collectors.joining());
Optional< T >maxBy根据比较器选择最大值Optional< Employee >max= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary)))
Optional< T >minBy根据比较器选择最小值Optional min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary)));
转换函数返回的类型collectingAndThen包裹另一个收集器,对其结果转换函数int how= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
IntSummaryStatisticssummarizingInt收集流中Integer属性的统计值。int SummaryStatisticsiss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary));
Map<Boolean, List< T >>partitioningBy根据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);
}

文章出处:blog.csdn.net/weixin_4406…