五分钟学习 Java 8 的流编程

一、概述

  1. Java8中在Collection中增长了一个stream()方法,该方法返回一个Stream类型。咱们就是用该Stream来进行流编程的;
  2. 流与集合不一样,流是只有在按需计算的,而集合是已经建立完毕并存在缓存中的;
  3. 流与迭代器同样都只能被遍历一次,若是想要再遍历一遍,则必须从新从数据源获取数据;
  4. 外部迭代就是指须要用户去作迭代,内部迭代在库内完成的,无需用户实现;
  5. 能够链接起来的流操做称为中间操做,关闭流的操做称为终端操做(从形式上看,就是用.连起来的操做中,中间的那些叫中间操做,最终的那个操做叫终端操做)。

二、筛选

2.1 过滤

Stream<T> filter(Predicate<? super T> predicate);
复制代码

filter经过指定一个Predicate类型的行为参数对流中的元素进行过滤,最终仍是会返回一个流,由于它是中间操做。中间操做返回的结果都是一个流,因此,若是咱们想要获得一个集合或者其余的非流类型,就须要使用终端操做来获取。git

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).collect(Collectors.toList());
// [4, 5, 5, 6, 7, 8, 9]
复制代码

2.2 去重

Stream<T> distinct();
复制代码

上面就是去重的方法的定义,它会按照流中的元素的equal()和hashCode()方法进行去重。去重以后将继续返回一个流,因此它也是中间操做。github

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).distinct().collect(Collectors.toList());
// [4, 5, 6, 7, 8, 9]
复制代码

2.3 限制

Stream<T> limit(long maxSize);
复制代码

就像是SQL里面的limit语句,在流中也有相似的limit()方法。它用于限制返回的结果的数量,将会从流的头开始取固定数量的元素,也是中间操做,使用完以后仍然会返回一个流。编程

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).limit(3).collect(Collectors.toList());
// [4, 5, 5]
复制代码

2.4 跳过

Stream<T> skip(long n);
复制代码

这个方法的定义和limit()几分类似。它也是中间操做,用于跳过从流的头开始指定数量的元素,使用完以后仍然会返回一个流。缓存

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).skip(3).collect(Collectors.toList());
// [6, 7, 8, 9]
复制代码

三、映射

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
复制代码

还记得Function函数接口的方法吗?它容许你把输入的类型转换成另外一种类型。上面就是它在map()方法中的应用。在流操做中使用了该方法以后,流就会尝试将当前流中全部的元素转换成另外一种类型。当你调用终端操做collect()的时候,天然也就获得了另外一种类型的集合。markdown

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<String> filter = list.stream().map((integer -> String.valueOf(integer) + "-")).collect(Collectors.toList());
// 结果:[1-, 1-, 2-, 3-, 4-, 5-, 5-, 6-, 7-, 8-, 9-]
复制代码

四、查找

Optional<T> findFirst();
Optional<T> findAny();
复制代码

在指定的流中查找元素的时候能够用这两个方法,它们是Stream接口中的方法,返回的已经再也不是Stream类型了,这能够说明它们是终端操做。因此,一般也是用来放在终端,继续操做的话就要使用Optional接口的方法了。app

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
Optional<Integer> optionalInteger = list.stream().filter(integer -> integer > 10).findAny();
Optional<Integer> optionalInteger = list.stream().filter(integer -> integer > 10).findFirst();
复制代码

上面是使用的两个示例,这里返回的结果是Optional类型的。Optional的设计借鉴了Guava中的Optional。使用它的好处是你不须要像之前同样将返回的结果与null进行判断,并在结果为null的时候经过=赋值一个默认值了。使用Optional中的方法,你能够更优雅地完成相同的操做。下面咱们列出Optional中的一些经常使用的方法:dom

编号 方法 说明
1 isPresent() 判断值是否存在,存在的话就返回true,不然返回false
2 isPresent(Consumer block) 在值存在的时候执行给定的代码
3 T get() 若是值存在,那么返回该值;不然,抛出NoSuchElement异常
4 T orElse(T other) 若是值存在,那么返回该值;不然,则返回other

五、匹配

boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
boolean anyMatch(Predicate<? super T> predicate);
复制代码

从定义上面来看,上面的三个方法也是终端操做。它们分别用来判断:流中的数据是否所有匹配指定的条件,流中的数据是否所有不匹配指定的条件,流中的数据是否存在一些匹配指定的条件。下面是一些示例:ide

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
boolean allMatch = list.stream().allMatch(integer -> integer < 10);
boolean anyMatch = list.stream().anyMatch(integer -> integer > 3);
boolean noneMatch = list.stream().noneMatch(integer -> integer > 100);
复制代码

六、归约

Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
复制代码

Stream接口中的reduce方法共有三个重载版本,上面咱们给出经常使用的两个的定义。它们基本是相似的,只是第二个方法参数列表中多了个初始值,而没有初始值的那个,返回了Optinoal类型;因此,区别不大,咱们只要搞明白它的行为就能够了。下面是归约的例子:函数

List<String> list = Arrays.asList("a", "b", "c", "d", "e", "f");
String ret = list.stream().reduce("-", (a, b) -> a + b);
复制代码

它的输出结果是-abcdef,显然它的效果就是:假如,$是某种操做,List是某个"数列",那么归约的意义就是初始值$n[0]$n[1]$n[2]$...$n[n-1]oop

七、数值流

一样是由于装箱的性能缘由,Java8中为数值类型专门提供了数值流:IntStream DoubleStream和LongStream。Stream接口提供了三个中间方法来完成从任意流映射到数值流的操做:

IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
复制代码

因此你能够用上面三个方法从任意流中获取数值流。而后,再利用数值流的方法来完成其余的操做。上面三个数值流和Stream接口都继承子BaseStream,因此它们包含的方法仍是有区别的,但整体上来讲大同小异。Stream比较具备通常性,上面三个数值流更有针对性,后者也提供了许多便利的方法。若是想要从数值流中获取对象流,你能够调用它们的boxed()方法,来获取装箱以后的流。

这里稍说起一下,对于Optional,Java8也为咱们提供了对应的数值类型:OptionalInt OptionalDouble OptionalLong。

在上面的三种数值流中还有几个静态方法用于获取指定数值范围的流:

public static LongStream range(long startInclusive, final long endExclusive)
public static LongStream rangeClosed(long startInclusive, final long endInclusive)
复制代码

上面是用于获取指定范围的LongStream的方法,一个对应于数学中的开区间,一个对应于数学中的闭区间的概念。

八、构建流

上面咱们在获取流的时候,实际上都是从Collection的默认方法stream()中获取的流,这有些笨拙。实际上,Java8为咱们提供了一些建立流的方法。这里,咱们列举一下这些方法:

public static<T> Builder<T> builder() // 1
public static<T> Stream<T> empty() // 2
public static<T> Stream<T> of(T t) // 3
public static<T> Stream<T> of(T... values) // 4
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) // 5
public static<T> Stream<T> generate(Supplier<T> s) // 6 
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) // 7
复制代码

上面的方法都是Stream接口中的静态方法,咱们能够用这些方法来获取到流。下面咱们对每一个方法作一些简要的说明:

  1. 从名称上就能够看出这里使用了构建者模式,你能够每次调用Builder的add()方法插入一个元素来建立流;
  2. 用来建立一个空的流
  3. 建立一个只包含一个元素的流
  4. 使用不定参数建立一个包含指定元素的流
  5. 弄清楚它的原理关键是要搞明白后面的UnaryOperator的含义,这是一个函数式接口,而且继承自Function,不一样之处在于它的入参和回参类型相同。这个方法的原理是从某个种子值开始,按照后面的函数的规则进行计算,每次是在以前的值的基础上执行某个函数的。因此Stream.iterate(2, n -> n * n).limit(3)将返回由2 4 16构成的流。
  6. 这里的Supplier也是一个函数接口,它只有一个get()方法,无参,只接受指定类型的返回值。因此,这个方法须要你提供一个用于生成数值的函数(或者说规则),好比Math.random()等等。
  7. 这个比较容易理解,就是经过将两个流合并来获得一个新的流。

九、收集器

上面咱们已经见识过了流的规约操做,可是那些操做还比较幼稚。Java8的收集器为咱们提供了更增强大的规约功能。

提及收集器,确定绕不过两个类Collector和Collectors,它俩有啥关系呢?其实Collector只是一个接口;Collectors是一个类, 其中的静态内部类CollectorImpl实现了该接口,而且被Collectors用来提供一些功能。Collectors中有许多的静态方法用于获取Collector的实例,使用这些实例咱们能够完成复杂的功能。固然,咱们也能够经过实现Collector接口来定义本身的收集器。

Stream的collect()方法有3个重载的版本。咱们就是经过其中的一个来使用收集器的,这是它的定义:

<R, A> R collect(Collector<? super T, A, R> collector);
复制代码

咱们注意一下这个方法的参数和返回类型. 从上面咱们能够看出传入的Collector有3个泛型,其中的最后一个泛类型R与返回的类型是一致的. 这很重要——能够预防你调用了某个方法殊不知道最终返回的是什么类型。

咱们先来看一些简单的例子,这里的stream是由Student对象构成的流:

Optional<Student> student = stream.collect(Collectors.maxBy(comparator))  // 须要传入一个比较器到maxBy()方法中
long count = stream.collect(Collectors.counting())
复制代码

上面的两种方式比较鸡肋,由于你可使用count()和max()方法来替代它们。下面咱们再看一些收集器的其余例子,注意在这些例子中,我并无使用lambda简化函数式接口,是由于想要你更清楚地看到它的泛类型和方法定义。这可能有助于你理解这些方法的做用机理。

9.1 计算平均值和总数

下面的语句用于计算平均值,相似的还有summingInt()用于计算总数。它们的用法是类似的。

Double d = stream.collect(Collectors.averagingInt(new ToIntFunction<Student>() {
    @Override
    public int applyAsInt(Student value) {
        return value.getGrade();
    }
}));
复制代码

从上面咱们看出,调用averagingInt()方法的时候须要传入一个ToIntFunction函数式接口,用于根据指定的类型返回一个整数值。

9.2 链接字符串

joining()工厂方法是专门用来链接字符串的,它要求流是字符串流,因此在对Student流进行拼接以前,须要先将其映射成字符串流:

String members = stream.map(new Function<Student, String>() {
    @Override
    public String apply(Student student) {
       return student.getName();
    }
}).collect(Collectors.joining(", ")); // 使用','将字符串拼接起来
复制代码

9.3 广义的规约汇总

Optional<Student> optional = stream.collect(Collectors.reducing(new BinaryOperator<Student>() {
    @Override
    public Student apply(Student student, Student student2) {
        return student.getGrade() > student2.getGrade() ? student : student2;
    }
}));
复制代码

上面的就是用来规约的函数。咱们用了reducing工厂方法,并向其中传入一个BinaryOperator类型。这里咱们指定最终的返回类型是Student。因此,上面的代码的效果是获取成绩最大的学生。

9.4 分组

Collectors中的分组仍是比较有意思的。咱们先看groupingBy方法的定义:

Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier)
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream)
复制代码

groupingBy方法有3个重载的版本,这里咱们给出其中经常使用的两个。第一个方法是经过指定规则对流进行分组的,而第二个方法先经过classifier指定的规则对流进行分组,而后用downstream的规则对分组后的流进行后续的操做。注意第二个参数仍然是Collector类型,这说明咱们仍然能够对分组后的流再次收集,好比再分组、求最大值等等。

Map<Integer, List<Student>> map = stream.collect(Collectors.groupingBy(new Function<Student, Integer>() {
    @Override
    public Integer apply(Student student) {
       return student.getClazz();
    }
}));
复制代码

以上是groupingBy()方法的第一个例子。注意这里咱们是经过将Student经过'班级字段'映射成一个整数来进行分组的。下面是一个二次分组的例子。这里的用了上面的第二个groupingBy()方法,并在downstream中指定了另外一个分组操做。

Map<Integer, Map<Integer, List<Student>>> map = stream.collect(Collectors.groupingBy(new Function<Student, Integer>() {
    @Override
    public Integer apply(Student student) {
       return student.getClazz();
    }
}, Collectors.groupingBy(new Function<Student, Integer>() {
    @Override
    public Integer apply(Student student) {
        return student.getGrade() == 100 ? 1 : student.getGrade() > 90 ? 2 : student.getGrade() > 80 ? 3 : 4;
    }
})));
复制代码

9.5 分区

与分组相似的还有一个分区的操做,分区只是分组的一种特例。它们的使用方式也基本一致,它的方法签名与上面的groupingBy方法相似。咱们直接看它的一个使用的方式好了:

Map<Boolean, List<Student>> map = stream.collect(Collectors.partitioningBy(new Predicate<Student>() {
    @Override
    public boolean test(Student student) {
        return student.getGrade() > 90;
    }
}));
复制代码

这就是分区的使用方式。它经过一个指定的函数式接口,将指定的类型映射到一个布尔类型。因此,它相似与分组,只不过它分组的结果只有两种,要么true,要么false。固然,相似于分组,你也能够在partitioningBy()方法的第二个参数中再指定一个收集器,这样就能够对分区后的流进行后续的操做了。

总结:

以上就是Java8中的流的常见的用法,这里只是列举了一些常见的、Java8 API中提供的一些类和方法。重点仍然是搞清楚其中的设计的原理,不要盲目记忆。学习的时候结合JDK源码进行,看到方法的定义就大体了解了它的设计原理。最后,不得不说的是,使用流编程确实很简洁和优雅。

相关代码:Github

相关文章
相关标签/搜索