强大的StreamAPI

前言

其实之前已经说了不少的有关Java8的新的特性的话题了,比如说最重要的lambda表达式,还有之前说的那个函数式接口与方法引用都是Java8中的一大更新。不过Java8中两大最重要的改变,第一个是lambda表达式,第二个就是我们接下来要说的强大的StreamAPI了。Stream API ( java.util.stream) 把真正的函数式编程风格引入到Java中。这 是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程 序员的生产力,让程序员写出高效率、干净、简洁的代码。

StreamAPI的基础知识

那么StreamAPI到底是一个什么玩意呢?所谓的Stream就是流的意思,大抵就是处理数据用的玩意。Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进 行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。 也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种 高效且易于使用的处理数据的方式。这么说可能是有点儿懵逼的感觉,Stream是一种处理集合数据的算法或者说结构吗?不全是。那么?Stream到底是什么呢?

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

关于Stream有三大需要注意的地方。

  • Stream自己不会储存元素,他只是用来处理元素用的。
  • Stream不会改变源对象,他之后将源对象经过处理之后返回一个新的Stream
  • Stream的操作是延迟执行的,他只会在需要结果的时候才去执行。

关于Stream的操作,主要分为一下的三个步骤:

  • 创建一个Stream;从一个数据源中获取一个需要处理的流。这个数据源多半是集合。

  • 中间操作;对这个流进行处理。

  • 终止操作;结束对这个流的操作。

创建Stream

StreamAPI这个玩意不像之前的东西,这个东西就是要按照规矩来一步一步的来学,毕竟他就是一个API而已。那么我们先从如何创建一个Stream开始说起。

前面说了,最常用与stream的就是Java中的集合。那么第一种方式就是。

从集合中创建一个Stream

集合的最终父类是Collection,在Java8种这个接口被扩展了。增加了两个新的方法。

default Stream<E> stream() // 返回一个顺序流 
default Stream<E> parallelStream() // 返回一个并行流

也就是说有了一个集合,我们就可以通过stream或者parallelStream方法返回一个stream对象。比如说。

public class Demo {
    public static List<Student> getList() {
        List<Student> list = new ArrayList<>();
        list.add(new Student("sher1", 18, 88.3, 110));
        list.add(new Student("sher2", 28, 188.3, 2110));
        list.add(new Student("sher3", 38, 288.3, 3110));
        list.add(new Student("sher4", 48, 388.3, 4110));
        list.add(new Student("sher5", 58, 488.3, 5110));
        list.add(new Student("sher6", 68, 588.3, 6110));
        list.add(new Student("sher7", 78, 688.3, 7110));
        list.add(new Student("sher8", 88, 788.3, 8110));

        return list;
    }
    public static void main(String[] args) {
        List<Student> list = getList();
        Stream<Student> studentStream = list.stream();
        Stream<Student> parallelStream = list.parallelStream();

    }
}

通过上面的方式就可以创建出一个Stream。至于取得这个Stream之后的操作如何,这里不说。(没用使用官方文档的套路说,是因为官方文档中对Stream的说明都太高大上了,我都不知道Stream是啥,你给我整那么多代码干啥)

上面创建了两个流。第一个是顺序流,也是最常用的。第二个是并行流。

这里我们可以使用forEach方法来对流中的元素进行一次遍历。

studentStream.forEach(System.out::println);
System.out.println("======================");
parallelStream.forEach(s -> System.out.println(s));

forEach函数需要的参数是一个Consumer。上面的第一种方式是使用方法引用,第二种方式是使用lambda表达式。这个算是对之前学习的内容的一个简单的复习。这里来看一下他们的输出。

Student{mName='sher1', mAge=18, mScore=88.3, phone=110}
Student{mName='sher2', mAge=28, mScore=188.3, phone=2110}
Student{mName='sher3', mAge=38, mScore=288.3, phone=3110}
Student{mName='sher4', mAge=48, mScore=388.3, phone=4110}
Student{mName='sher5', mAge=58, mScore=488.3, phone=5110}
Student{mName='sher6', mAge=68, mScore=588.3, phone=6110}
Student{mName='sher7', mAge=78, mScore=688.3, phone=7110}
Student{mName='sher8', mAge=88, mScore=788.3, phone=8110}
=========================================================
Student{mName='sher6', mAge=68, mScore=588.3, phone=6110}
Student{mName='sher5', mAge=58, mScore=488.3, phone=5110}
Student{mName='sher8', mAge=88, mScore=788.3, phone=8110}
Student{mName='sher7', mAge=78, mScore=688.3, phone=7110}
Student{mName='sher3', mAge=38, mScore=288.3, phone=3110}
Student{mName='sher4', mAge=48, mScore=388.3, phone=4110}
Student{mName='sher2', mAge=28, mScore=188.3, phone=2110}
Student{mName='sher1', mAge=18, mScore=88.3, phone=110}

顺序流中的数据和原来集合中的元素的顺序是一致的,但是并行流我们运行了几次之后发现顺序都是会发生变化的。可见并行流中对于数据的处理是没有顺序的。更严谨的说,他对数据的处理是并行的。这里多线程还没有说,如果讲了多线程之后就是特别的容易理解的了。

从数组中创建一个流

数组中似乎没有stream这个方法。不过Java8种更新了一个和数组的使用密切相关的类——Arrays。我们可以使用如下的方式从数组中获取一个流。

static <T> Stream<T> stream(T[] array) //返回一个流

代码测试如下

@Test
public void test2() {
    int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    String[] strArr = {"sher", "hony", "sherhony"};
    IntStream stream = Arrays.stream(arr);
    Stream<String> stringStream = Arrays.stream(strArr);
    stream.forEach(System.out::println);
    stringStream.forEach(System.out::println);
}

其实上面的代码很容易发现一个奇怪的地方。为啥int数组返回的是IntStream而不是Stream<Integer>。我们上面的自定义的类型和String类型都是Stream<T>的形式。当我使用Stream<Integer>来代替IntStream的时候,发现报错了。我们发现,这个函数处理int的时候并没有涉及装箱拆箱的操作,而是这个方法对int[] short[] double[]这三个基本数据类型参数做了重载。如果是short[] float[] char[]这种数组的话,是没有办法直接使用Arrays.stream方法的。可以转为Short[]等才能够使用这个方法。

public static IntStream stream(int[] array)
public static LongStream stream(long[] array)
public static DoubleStream stream(double[] array)

通过Stream的of()方法

上面是两种创建流的方式最常用的方式。下面再次来介绍一下通过Stream的of()方法来创建一个流。方法如下:

public static<T> Stream<T> of(T... values) // 返回一个流
@Test
public void test3() {
    Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6, 7);
    integerStream.forEach(System.out::println);
}

创建一个无限流

上面的三个方法都是创建了一个有限流。我们也可以通过一下的两种方式创建一个无限流。

//迭代 
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
// 生成 
public static<T> Stream<T> generate(Supplier<T> s) 

先来说第一个方式迭代。所谓迭代就是通过一个递推的方式来产生一个无限流。比如说产生所有的奇数流就可以这样写。

@Test
public void test4() {
    Stream<Integer> iterate = Stream.iterate(1, x -> x + 2);
    iterate.limit(10).forEach(System.out::println);
}

上面使用的limit(10)就是把流限制在10个,不然无限流就会无限运行下去。

也可以使用generate方法,比如说产生无限个随机数的流。

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

Stream的中间操作

上面我们已经基本掌握了创建一个Stream的方式,现在要学的是Stream中最重要的一块——中间操作。

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

那么上面我们只是使用了创建流的方式为什么我们可以看到流中的数据呢?那是因为我们确实使用了终止操作,forEach就是一个终止操作,安装流中的顺序来遍历流中的元素。其实上面无限流中说到的限制流中元素的个数的操作就是一种中间操作。中间操作也分为多种,下面来一种一种介绍。

筛选与切片

方法 描述
filter(Predicate p) 接收 Lambda ,从流中排除某些元素
distinct() 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
limit(long maxSize) 截断流,使其元素不超过给定数量
skip(long n) 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一 个空流。与 limit(n) 互补

下面来对上面的代码就以上面的那个List<Student>做代码演示

输出所有年龄大于40岁的

@Test
public void test6() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    stream.filter(x -> x.getAge() > 40).forEach(System.out::println);
}

去掉重复的学生

@Test
public void test6() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    stream.distinct().forEach(System.out::println);
}

注意:一个流在经过终止操作了就不能够再使用。

输出前五个学生

@Test
public void test6() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    stream.limit(5).forEach(System.out::println);
}

跳过前五个学生

@Test
public void test6() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    stream.skip.forEach(System.out::println);
}

上面的方法都比较简单,下面来看看下一个操作。

映射

方法 描述
map(Function f) 接收一个函数作为参数,该函数会被应用到每个元 素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元 素上,产生一个新的 DoubleStream。
mapToInt(ToIntFunction f) 接收一个函数作为参数,该函数会被应用到每个元 素上,产生一个新的 IntStream。
mapToLong(ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元 素上,产生一个新的 LongStream。
flatMap(Function f) 接收一个函数作为参数,将流中的每个值都换成另 一个流,然后把所有流连接成一个流

所谓的map就是映射的意思。高中是都学过映射的概念,就是将一个值以某种法则变成另一个值。

@Test
public void test7() {
    Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6, 7);
    integerStream.map(x -> {
        if (x % 2 == 0) {
            return 0;
        } else {
            return x;
        }
    }).forEach(System.out::println);
}

上面的方法可以将流中的所有的偶数变成0。对于这个例子来说也可以写成mapToIntmapToDoublemapToLong这三种形式,只不过是返回值的类型不同罢了。

对于flatMap的使用就是比较复杂了的。他是将每个值都换成另一个流,然后将所有的流连接成为一个流。

比如如下的代码

@Test
public void test8() {
    List<List<Integer>> list = new ArrayList<>();
    List<Integer> list1 = new ArrayList<>();
    list1.add(1);
    list1.add(1);
    list1.add(2);
    List<Integer> list2= new ArrayList<>();
    list2.add(2);
    list2.add(3);
    List<Integer> list3 = new ArrayList<>();
    list3.add(6);
    list.add(list1);
    list.add(list2);
    list.add(list3);

    Stream<List<Integer>> stream = list.stream();
    stream.forEach(x ->{
        x.forEach(System.out::println);
    });
}

这个流中的元素都是List,如果我们想要取得所有的数据的话就需要对流中的元素再次使用forEach。这两个forEach是不一样的。一个forEach是流对象的,而另一个是集合对象的方法。

如果我们使用的是flatMap方法就可以将流中的数据转成一个流,然后将这些流合并成一个流,这样我们就可以只使用一个forEach就可以对这个流进行遍历了。

list.stream().flatMap(List::stream).forEach(System.out::println);

只需要使用这一行的代码,其中使用两个方法引用。List::stream是将一个List转为Stream用的。这也是对方法引用的一个复习了。

上面的映射的操作应该是StreamAPI的核心操作,特别是map方法就是最重要的一个操作。前面的filter方法也是一个非常重要的方法。

排序

方法 描述
sorted() 产生一个新流,其中按自然顺序排序
sorted(Comparator com) 产生一个新流,其中按比较器顺序排

如果要使用第一种方式,流中的数据的类型必要要实现Comparable接口(里面有个compareTo方法),不然是不可以使用第一种方式的。(这都应该是Java中的基础中的基础了吧)

如果是使用第二种方式的话,只要实现这个函数式接口就行了。

@Test
public void test9() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    stream.sorted(Comparator.comparing(Student::getAge)).forEach(System.out::println);
}

如果需要得是降序的话,就只能写成这种形式了

stream.sorted((x, y) -> -Integer.compare(x.getAge(), y.getAge()))
        .forEach(System.out::println);

上面基本上就是最常用的中间操作了,下面要来介绍的是终止操作。

终止操作

上面的操作中我们使用的所有的终止操作都是forEach方法来遍历,其实Stream的终止操作也有很多,而且也分为不同的类型。下面就来一一介绍吧。

匹配和查找

方法 描述
allMatch(Predicate p) 检查是否匹配所有元素
anyMatch(Predicate p) 检查是否至少匹配一个元素
noneMatch(Predicate p) 检查是否没有匹配所有元素
findFirst() 返回第一个元素
findAny() 返回当前流中的任意元素
count() 返回流中元素总数
max(Comparator c) 返回流中最大值
min(Comparator c) 返回流中最小值
forEach(Consumer c) 内部迭代(使用 Collection 接口需要用户去做迭代, 称为外部迭代。相反,Stream API 使用内部迭 代——它帮你把迭代做了)

上面的值也都是很容易理解,不需要其他过多的说明。

归约

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

这个操作一般情况下是和map操作结合起来用的,比如要求所有的学生的年龄之和。

@Test
public void test11() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    Optional<Integer> reduce = stream.map(Student::getAge).reduce(Integer::sum);
    reduce.ifPresent(System.out::println);
}

至于第一个函数的第一个参数就是初始值。用法如下。

@Test
public void test11() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
//        Optional<Integer> reduce = stream.map(Student::getAge).reduce(Integer::sum);
    Integer reduce = stream.map(Student::getAge).reduce(a, (x, y) -> x * y);
    System.out.println(reduce);
}

这里返回的类型是T,不是Optional<T>

收集

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

toList List<T> 
把流中元素收集到List 
List<Employee> emps= list.stream().collect(Collectors.toList()); 

toSet Set<T> 
把流中元素收集到Set
Set<Employee> emps= list.stream().collect(Collectors.toSet()); 

toCollection Collection<T>
把流中元素收集到创建的集合 
Collection<Employee> emps =list.stream().collect(Collectors.toCollection(ArrayList::new)); 

counting Long 
计算流中元素的个数
long count = list.stream().collect(Collectors.counting());

summingInt Integer 
对流中元素的整数属性求和 
int total=list.stream().collect(Collectors.summingInt(Employee::getSalary));

averagingInt Double
计算流中元素Integer属性的平均值
double avg = list.stream().collect(Collectors.averagingInt(Employee::getSalary));

summarizingInt IntSummaryStatistics
收集流中Integer属性的统计值。如:平 均值
int SummaryStatisticsiss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary));

joining String 
连接流中每个字符串 
String str= list.stream().map(Employee::getName).collect(Collectors.joining()); 

maxBy Optional<T> 
根据比较器选择最大值 
Optional<Emp>max= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary))); 

minBy Optional<T> 
根据比较器选择最小值 
Optional<Emp> 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));

groupingBy Map<K, List<T>>
根据某属性值对流分组,属性为K, 结果为V
Map<Emp.Status, List<Emp>> map= list.stream().collect(Collectors.groupingBy(Employee::getStatus)); 

partitioningBy Map<Boolean, List<T>> 
根据true或false进行分区 
Map<Boolean,List<Emp>> vd = list.stream().collect(Collectors.partitioningBy(Employee::getManage));

上面是常用的一些方法。下面都有代码演示。

@Test
public void test12() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    ArrayList<Student> collect = stream.filter(x -> x.getAge() > 40)
            .collect(Collectors.toCollection(ArrayList::new));
    for (Student student : collect) {
        System.out.println(student);
    }
}

@Test
public void test13() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    Integer collect = stream.collect(Collectors.reducing(0, Student::getAge, Integer::sum));
    System.out.println(collect);
}

@Test
public void test14() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    Integer collect = stream.collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
    System.out.println(collect);
}

@Test
public void test15() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    Map<Integer, List<Student>> collect = stream.collect(Collectors.groupingBy(Student::getAge));
    for (Integer i : collect.keySet()) {
        System.out.println("i = " + i);
        System.out.println(collect.get(i).get(0));
    }
}

@Test
public void test16() {
    List<Student> list = getList();
    Stream<Student> stream = list.stream();
    Map<Boolean, List<Student>> collect = stream.collect(Collectors.partitioningBy(x -> x.getAge() > 40));
    for (Boolean b : collect.keySet()) {
        System.out.println(b);
        for (Student s : collect.get(b)) {
            System.out.println(s);
        }
    }
}

总结

StreamAPI是Java8种新增的非常顶的API,对于这种API式的东西就是多用多学习,其他也是没有什么好的捷径了。


一枚小菜鸡