Java---流式编程

集合优化了对象的存储,而流则是对数据的处理

流是一系列与特定存储机制无关的元素,利用流,我们无需迭代集合中的元素,就可以提取和操作它们。这些管道通常被组合在一起,在流上形成一条操作管道。

使用流的一个核心好处是,它使得程序更加短小并且更易理解。同时流还可以同 Lambda 表达式和方法引用(method references)一起使用。

流一个重要的特性是:流是懒加载的,意味着它在绝对必要的情况下才计算。这个特性是我们可以表示非常大(甚至无限)的序列,而不用担心内存的问题。

流的支持

Java 设计者在设计流时面临着这样一个难题:现存的大量类库不仅为 Java 所用,同时也被应用在整个 Java 生态圈数百万行的代码中。如何将一个全新的流的概念融入到现有类库中呢?

Java 8 采用的解决方案是:在接口中添加被 default默认)修饰的方法。通过这种方案,设计者们可以将流式(stream)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。

流的操作有三种:创建流、修改流元素(中间操作)、消费流(终端操作)

流的创建:

流的创建方式有以下这几种方式:

  • Stream.of():需要传入一组元素参数。
public class StreamOf {
    public static void main(String[] args) {
        Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!");
            .forEach(System.out::println);
    }
}
// 中间需要传入一组元素参数
// 代码中传入的是一组字符串元素,forEach() 传入一个方法引用。
  • stream(): 对于集合类,可以直接调用集合类中的 stream()方法:
List list = Arrays.asList("It's a wonderful day for pie!".split(" "))
list.stream()
    .forEach(System.out::print);
  • Stream.generate(): 需要传入一个 Supplier ,Suppier 是一个通用的函数式接口,所以可以配合Lambda 使用。
// Stream.generate() 中间传入的参数 Generator 类实现Supplier 接口,
//所以实现的 get(),Stream.generate(new Generator()) 会自动调用 get() 产生所需要的流元素。
public class Generator implements Supplier {
    Random rand = new Random(47);
    char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();

    public String get() {
        return "" + letters[rand.nextInt(letters.length)];
    }

    public static void main(String[] args) {
        String word = Stream.generate(new Generator())
                            .limit(30)
                            .collect(Collectors.joining());
        System.out.println(word);
    }
}

搭配 lambda 表达式使用:传入与 supplier 接口中 get() 方法签名相同的方法引用

Stream.generate(() -> "duplicate")
              .limit(3)
              .forEach(System.out::println);
// 输出
// “duplicate”
// “duplicate”
// “duplicate”
  • Stream.iterate(): 该函数有两个参数,以种子(第一个参数)开头,并将其传给方法(第二个参数,需要传入一个方法引用)。方法的结果将添加到流,并存储作为第一个参数用于下次调用 iterate()。
基本类型流的生成
  • InStream.range(): 生成整型序列的流,range() 需要传入两个参数,指定一个序列范围。
  • LongStream:生成 Long 序列的流
  • DoubleStream:生成 double 序列的流
public class Ranges {
    public static void main(String[] args) {
      System.out.println(range(10, 20).sum());
    }
}
// sum() :用于将流中的元素相加求和
数组流生成
  • Arrays.stream(数组) :利用 Arrays 类中含有一个名为 stream() 的静态方法将数组转换成为流
Arrays.stream(new double[] { 3.14159, 2.718, 1.618 })
            .forEach(n -> System.out.format("%f ", n));

同时也可以

 Arrays.stream(new int[] { 1, 3, 5, 7, 15, 28, 37 }, 3, 6)

此时Arrays.stream() 的调用有两个额外的参数。第一个参数告诉 stream() 从数组的哪个位置开始选择元素,第二个参数用于告知在哪里停止。

  • splitAsStream() :Java 8 在 java.util.regex.Pattern 中增加的一个新的方法。这个方法可以根据传入的公式将字符序列转化为流。但是有一个限制,输入只能是 CharSequence,因此不能将流作为 splitAsStream() 的参数。
中间操作

中间操作,用于从一个流中获取对象,并进行处理,再将对象作为另一个流从后端输出,以连接到其他操作。

查看流中元素

peek(): 该操作的目的是帮助调试。它允许你无修改地查看流中的元素。因为 peek() 符合无返回值的 Consumer 函数式接口,所以我们只能观察,无法使用不同的元素来替换流中的对象

排序

sorted() 操作排序,使用默认比较器实现,另一种形式是传入一个比较器。另一种形式的实现:传入一个 Comparator 参数。

移除元素

distinct(): 可用于消除流中的重复元素。相比创建一个 Set 集合,该方法的工作量要少得多。

filter(Predicate):过滤操作会保留与传递进去的过滤器函数计算结果为 true 元素。

isPrime() 作为过滤器函数,用于检测质数。

应用函数到元素
  • map() 会获取流中的所有元素,并且对流中元素应用函数操作从而产生新的元素,并将其传递到后续的流中。

  • mapToInt(ToIntFunction):操作同上,但结果是 IntStream

  • mapToLong(ToLongFunction):操作同上,但结果是 LongStream

  • mapToDouble(ToDoubleFunction):操作同上,但结果是 DoubleStream

使用 map 也可以产生和接收类型不同的类型
map(类::new) 传入一个类的构造器。可以把接收类型转换为其他类。例如 int 转成一个自定义的类型。

在 flatMap 中组合流

map() 函数的功能是产生流,而如果我们传入的元素是流,则会产生元素流的流。
此时可以利用 flatMap() :该函数功能与 map 一样。但不同是该函数会把流扁平化为元素,最终产生的是元素。

public class FlatMap {
    public static void main(String[] args) {
        Stream.of(1, 2, 3)
        .flatMap(i -> Stream.of("Gonzo", "Fozzie", "Beaker"))
        .forEach(System.out::println);
    }
}
其他操作:
  • ints(): 方法产生一个流并且 ints() 方法有多种方式的重载 — 两个参数限定了数值产生的边界.
    相应其他数据类型的操作:longs()

  • limit() : 操作获取指定数量的元素
    limit(8):或者前 8 个元素。

  • forEach() 方法遍历输出,它根据传递给它的函数对每个流对象执行操作。
    通常使用该方法传入 System.out::println 来遍历输出流中的每个元素。

  • boxed(): 流操作将会自动地把基本类型包装成为对应的装箱类型, 对应解箱 unboxed()

  • skip() 操作 : 它根据参数丢弃指定数量的流元素

Optional 对象

一些标准流返回 Optional 对象,此对象可以作为流的持有者,即使查看的元素不存在也能友好的提示我们。

流中的一些操作方法

  • findFirst() 返回一个包含第一个元素的 Optional 对象,如果流为空则返回 Optional.empty

  • findAny() 返回包含任意元素的 Optional 对象,如果流为空则返回 Optional.empty

  • max() 和 min() 返回一个包含最大值或者最小值的 Optional 对象,如果流为空则返回 Optional.empty

class OptionalsFromEmptyStreams {
    public static void main(String[] args) {
        System.out.println(Stream.empty()
             .findFirst());
    }
}
// 输出:Optional.empty

当流为空时,返回 Optional.empty 对象。
空流可以通过 Stream.empty() 创建,也可以运用 Stream.empty(),使用此方法时,需要赋值给一个指定的类型的变量,不然 java 编辑器无法进行类型推导,即

Stream s = Stream.empty()

当接收到 Optional 对象时,应首先调用 isPresent()检查其中是否包含元素。如果存在,可使用 get() 获取。

class OptionalBasics {
    static void test(Optional optString) {
        if(optString.isPresent())
            System.out.println(optString.get()); 
        else
            System.out.println("Nothing inside!");
    }
    public static void main(String[] args) {
        test(Stream.of("Epithets").findFirst());
        test(Stream.empty().findFirst());
    }
}
便利函数

通常对于流返回的 Optional对象,我们需要进行解包,才能获取到里面的元素。
流中提供了一些便利函数,用于解包 Optional 对象,简化了 “对所包含的对象的检查和执行操作”:

  • ifPresent(Consumer):当值存在时调用Consumer,否则什么也不做。
  • orElse(otherObject):如果值存在则直接返回,否则生成 otherObject
  • orElseGet(Supplier):如果值存在则直接返回,否则使用 Supplier 函数生成一个可替代对象。
  • orElseThrow(Supplier):如果值存在直接返回,否则使用 Supplier 函数生成一个异常。
创建 Optional

当需要在代码中添加 Optional对象时,可以利用 Optional对象的静态方法来创建:

  • empty():生成一个空 Optional。
  • of(value):将一个非空值包装到 Optional 里。
  • ofNullable(value):针对一个可能为空的值,为空时自动生成 Optional.empty,否则将值包装在 Optional中。
Optional 对象的操作

当流管道生成了 Optional 对象,可以使用下列一些方法对对象进行操作。

  • filter(Predicate):将 Predicate 应用于 Optional 中的内容并返回结果。当 Optional 不满足 Predicate 时返回空。如果 Optional 为空,则直接返回。

  • map(Function):如果 Optional不为空,应用 FunctionOptional 中的内容,并返回结果。否则直接返回 Optional.empty

  • flatMap(Function):同 map(),但是提供的映射函数将结果包装在 Optional 对象中,因此 flatMap() 不会在最后进行任何包装。

一般来说,流的 filter() 会在 Predicate 返回 false 时移除流元素。而Optional.filter()在失败时不会删除 Optional,而是将其保留下来,并转化为空。

终端操作
数组
  • toArray():将流转换成适当类型的数组。
  • toArray(generator):在特殊情况下,生成自定义类型的数组。
循环
  • forEach(Consumer)常见如 System.out::println 作为 Consumer 函数。
  • forEachOrdered(Consumer): 保证 forEach 按照原始流顺序操作
集合
  • collect(Collector):使用 Collector 收集流元素到结果集合中。
  • collect(Supplier, BiConsumer, BiConsumer):同上,第一个参数 Supplier 创建了一个新结果集合,第二个参数 BiConsumer 将下一个元素包含到结果中,第三个参数 BiConsumer 用于将两个值组合起来。

Collectors 还有其他更加复杂的实现,可以通过查看文档了解。
例如:实现将流元素收集到 TreeSet 中的集合。可以利用将集合的的构造函数引用传递给 Collectors.toCollection(),从而构建任何类型的集合

public class TreeSetOfWords {
    public static void
    main(String[] args) throws Exception {
        Set words2 =
                Files.lines(Paths.get("TreeSetOfWords.java"))
                        .flatMap(s -> Arrays.stream(s.split("\\W+")))
                        .filter(s -> !s.matches("\\d+")) // No numbers
                        .map(String::trim)
                        .filter(s -> s.length() > 2)
                        .limit(100)
                        .collect(Collectors.toCollection(TreeSet::new));
        System.out.println(words2);
    }
}

Files.lines() 打开 Path 并将其转换成为行流。

组合
  • reduce(BinaryOperator):使用 BinaryOperator 来组合所有流中的元素。因为流可能为空,其返回值为 Optional
  • reduce(identity, BinaryOperator):功能同上,但是使用 identity 作为其组合的初始值。因此如果流为空,identity 就是结果。
  • reduce(identity, BiFunction, BinaryOperator):更复杂的使用形式(暂不介绍),这里把它包含在内,因为它可以提高效率。通常,我们可以显式地组合 map() 和 reduce() 来更简单的表达它。
匹配
  • allMatch(Predicate) :如果流的每个元素根据提供的 Predicate 都返回 true 时,结果返回为 true。在第一个 false 时,则停止执行计算。
  • anyMatch(Predicate):如果流中的任意一个元素根据提供的 Predicate 返回 true 时,结果返回为 true。在第一个 false 是停止执行计算。
  • noneMatch(Predicate):如果流的每个元素根据提供的 Predicate 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算。
查找
  • findFirst():返回第一个流元素的 Optional,如果流为空返回 Optional.empty。
  • findAny():返回含有任意流元素的 Optional,如果流为空返回 Optional.empty。
信息
  • count():流中的元素个数。
    max(Comparator):根据所传入的 Comparator 所决定的“最大”元素。
  • min(Comparator):根据所传入的 Comparator 所决定的“最小”元素。
数字流信息
  • average():求取流元素平均值。
  • max() 和 min():数值流操作无需 Comparator。
  • sum():对所有流元素进行求和。
  • summaryStatistics():生成可能有用的数据。目前并不太清楚这个方法存在的必要性,因为我们其实可以用更直接的方法获得需要的数据。

collect() 收集操作,它根据参数来组合所有流中的元素。
当传入的参数是Collectors.joining(),你将会得到一个 String 类型的结果,每个元素都根据 joining() 的参数来进行分割。其他不同的 Collectors 的参数则会产生不同的结果

你可能感兴趣的:(Java---流式编程)