你应该知道的Stream流操作

267 阅读5分钟

“我正在参加「掘金·启航计划」”

Stream

Java8中有两大最为重要的改变。第一个是 Lambda 表达式,在上篇文章已经讲述了一些;另外一个则是 StreamAPI(java.util.stream.*)。 Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

1. 什么是Stream?

Stream是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

“集合讲的是数据,流讲的是计算!”流需要注意以下几个特点:

①Stream 自己不会存储元素。

②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。

③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

1. Stream操作的三个步骤

⚫ 创建Stream: 通过一个数据源(如:集合、数组),获取一个流。

⚫ 中间操作:一个中间操作链,对数据源的数据进行处理。

⚫ 终止操作(终端操作):一个终止操作,执行中间操作链,并产生结果。

3. Stream的创建

3.1 由集合创建流

Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:

⚫ default Stream stream() : 返回一个顺序流

⚫ default Stream parallelStream() : 返回一个并行流
顺序流使用的较多,使用单线程按顺序对数据源进行操作,并行流的线程有多个,处理效率高但是顺序不确定。

3.2由数组创建流

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

⚫ static Stream stream(T[] array): 返回一个流

重载形式,能够处理对应基本类型的数组:

⚫ public static IntStream stream(int[] array)

⚫ public static LongStream stream(long[] array)

⚫ public static DoubleStream stream(double[] array)

3.2由值创建流

可以使用静态方法 Stream.of(), 通过显示值创建一个流。(它可以接收任意数量的参数。接收的参数会被保存到一个容器里面)

⚫ public static Stream of(T... values) : 返回一个流

4. Stream 的中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理! 而在终止操作时一次性全部处理,称为“惰性求值”,这些操作也是我们最关心的。

4.1 筛选与切片

流方法含义
filter(Predicate p)接收 Lambda,从流中排除某些元素。
distinct()筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
limit(long maxSize)截断流,使其元素不超过给定数量。
skip(long n)跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
@Test
    public void test1(){
        List<String> stringList = Arrays.asList("aa","bb","cc","cc","ddd","sdsdsds","aa");
        //筛选字符串集合里面字符串长度不大于2的字符串并用新的集合收集起来
        System.out.println(stringList.stream().filter(str -> str.length() <= 2).collect(Collectors.toList()));
        //原集合不变
        System.out.println(stringList);
        //去掉重复的字符串,如果集合里是自定义的对象,需要重写hashCode() 和 equals()
        System.out.println(stringList.stream().distinct().collect(Collectors.toList()));
        //取前三个元素
        System.out.println(stringList.stream().limit(3).collect(Collectors.toList()));
        //跳过前三个,取后三个
        System.out.println(stringList.stream().skip(3).collect(Collectors.toList()));
        //也可以进行多次操作,但是进行终止操作后就不能再进行中间操作
        System.out.println(
                stringList.stream()
                        .filter(s -> s.length() <= 3)
                        .distinct()
                        .limit(4)
                        .skip(1)
                        .collect(Collectors.toList())
        );
    }

这样看就有些SQL的味道了,而且很容易理解。

4.2 映射

方法描述
map(Function f)接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。
mapToInt(ToIntFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。
mapToLong(ToLongFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。
flatMap(Function f)接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
@Test
public void test2(){
    List<String> list = Arrays.asList("1","12","3","123","4");
    System.out.println(list.stream().map(str -> Double.valueOf(str)).collect(Collectors.toList()));
    List<User> users = new ArrayList<>();
    users.add(new User(12,"Jack",null));
    users.add(new User(13,"Tom",null));
    users.add(new User(14,"Mack",null));
    System.out.println(users);
    //这里User类的set方法会返回User对象本身,否则没有返回值的话会报错
    System.out.println(users.stream().map(user -> user.setAge(user.getAge()+1)).collect(Collectors.toList()));
}

map可以看成是对集合里每一个元素的处理,处理后可以映射为其它类型的对象,也可以映射为原来的对象。
flatMap和map的区别在于,map只能一对一的转换,而flatMap不仅可以一对一转换,它会返回一个流,也就是一对多进行转换,即可以把一个集合或者数组进行展开返回里面的多个值组成的流

@Test
public void test4(){
    List<List<Integer>> list =  new ArrayList<>();
    List<Integer> sub1 = Arrays.asList(1,3);
    List<Integer> sub2 = Arrays.asList(2,4);
    List<Integer> sub3 = Arrays.asList(6,5);
    list.add(sub1);
    list.add(sub2);
    list.add(sub3);
    System.out.println(list);
    System.out.println(list.stream().map(nums->nums.stream()).collect(Collectors.toList()));
    System.out.println(list.stream().flatMap(nums->nums.stream()).sorted().collect(Collectors.toList()));
}

image.png

4.3 排序

方法描述
sorted()产生一个新流,其中按自然顺序排序
sorted(Comparator comp)产生一个新流,其中按比较器顺序排序
@Test
public void test3(){
    List<Integer> list = Arrays.asList(12, 123, 23, 3, 12, 33, 190, 9);
    System.out.println(list.stream().sorted().collect(Collectors.toList()));
    //compare方法是比较i和j,i < j返回-1,i = j返回0,i > j返回1
    System.out.println(list.stream()
            .sorted((i, j) -> -Integer.compare(i, j))
            .collect(Collectors.toList()));
    List<String> stringList = Arrays.asList("aa", "11", "cc", "dd", "ee", "bb");
    System.out.println(stringList.stream().sorted().collect(Collectors.toList()));

    List<User> users = new ArrayList<>();
    users.add(new User(12, "Jack", null));
    users.add(new User(13, "Tom", null));
    users.add(new User(14, "Mack", null));
    //从大到小排序
    users.stream()
            .sorted((u1, u2) -> -u1.getAge() < u2.getAge() ? -1 : 1 )
            .forEach(System.out::println);
}

sorted()这个方法会对元素进行自然顺序排序,反应到数字就是从小到大,需要元素所对应的对象需要实现Comparable接口,重写compareTo接口,Integer和String都对此类进行了实现,否则直接使用sorted()会报错;sorted(Comparator comp)这个重载方法需要接收一个比较器,需要自定义排序规则。

5. 终止操作

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

5.1 查找与匹配

方法描述
allMatch(Predicate p)检查是否匹配所有元素
anyMatch(Predicate p)检查是否至少匹配一个元素
noneMatch(Predicate p)检查是否没有匹配所有元素
findFirst()返回第一个元素
findAny()返回当前流中的任意元素
count()返回流中元素总数
max(Comparator c)返回流中最大值
min(Comparator c)返回流中最小值
forEach(Consumer c)内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部 迭代——它帮你把迭代做了)
@Test
public void test5(){
    City beijing = new City(1.68,"北京",1128);
    City tianjin = new City(1.1,"天津",924);
    City chongqing = new City(1.6,"重庆",1500);
    City shanghai = new City(0.63,"上海",2489);
    List<City> cities = Arrays.asList(beijing,tianjin,chongqing,shanghai);
    //匹配所有:所有城市面积大于0.5万平方公里,返回true
    System.out.println(cities.stream().allMatch(city->city.getArea()>0.5));
    //至少匹配一个,至少有一个尝试的人口超过2000万,返回true
    System.out.println(cities.stream().anyMatch(city->city.getPeoples()>2000));
    //是否没有匹配,没有城市的名字以“湖”开头,返回true
    System.out.println(cities.stream().noneMatch(city->city.getName().startsWith("湖")));

    System.out.println(cities.stream().max((c1,c2)->c1.getArea()<c2.getArea()?-1:1));
    System.out.println(cities.stream().min((c1,c2)->c1.getArea()<c2.getArea()?-1:1));
    cities.stream().forEach(System.out::println);
}

min和max接受的仍然是比较器,返回的不是对象本身,而是用Optional包装的对象

image.png

5.2 规约

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

先看第二个方法reduce(BinaryOperator b),其中,BinaryOperator继承了BiFunction,源码如下,也就是BiFunction的apply方法的两个参数和返回值都是同一类型

public interface BinaryOperator<T> extends BiFunction<T,T,T>

再看第一个reduce(T iden, BinaryOperator b),区别在于返回值的不同,这个方法返回的类型是T,第二个返回的类型是Optiona。

@Test
public void test6(){
    List<Integer> list = Arrays.asList(12,123,23,3,12,33,190,9);
    System.out.println(list.stream().reduce((i, j) -> i + j));
    System.out.println(list.stream().reduce(0, (i, j) -> i + j));
}

第一个结果是Optional[405],后面是405,0是初始值。

5.3 收集

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

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