流式编程

扫码查看

集合优化了对象的存储,流和对象的处理有关

利用流,我们无需迭代集合中的元素,就可以提取和操作它们。这些管道通常被组合在一起,在流上形成一条操作管道。

举个例子,假如你要随机展示 5 至 20 之间不重复的整数并进行排序。实际上,你的关注点首先是创建一个有序集合。围绕这个集合进行后续的操作。但是使用流式编程,你就可以简单陈述你想做什么:

// streams/Randoms.java
import java.util.*;
public class Randoms {
    public static void main(String[] args) {
        new Random(47)
            .ints(5, 20)
            .distinct()
            .limit(7)
            .sorted()
            .forEach(System.out::println);
    }
}

输出

6
10
13
16
17
18
19

分析一下这段代码

1.首先给Random一个seed,保证每次运行产生相同的结果。

2.ints()会返回一个流对象,这样我们就对流进行进一步处理了。

    public IntStream ints(int randomNumberOrigin, int randomNumberBound) {
        if (randomNumberOrigin >= randomNumberBound)
            throw new IllegalArgumentException(BadRange);
        return StreamSupport.intStream
                (new RandomIntsSpliterator
                         (this, 0L, Long.MAX_VALUE, randomNumberOrigin, randomNumberBound),
                 false);
    }

3.实际上后面的distinct(),sorted(),limit()都是对流对象处理返回一个流对象,这点是必要的,我们不能处理流对象后返回一个其他的对象。

4.foreach()这边是InputStream接口下的一个方法,利用了前面的方法引用

    void forEach(IntConsumer action);

在这一段代码中没有声明一个变量,这里全程表达的是怎么做,如果是传统的声明式编程就像下面这段代码一样。

// streams/ImperativeRandoms.java
import java.util.*;
public class ImperativeRandoms {
    public static void main(String[] args) {
        Random rand = new Random(47);
        SortedSet<Integer> rints = new TreeSet<>();
        while(rints.size() < 7) {
            int r = rand.nextInt(20);
            if(r < 5) continue;
            rints.add(r);
        }
        System.out.println(rints);
    }
}

实际上,它看起来反而更难理解了。

在 ImperativeRandoms.java 中显式地编写迭代机制称为外部迭代。而在 Randoms.java 中,流式编程采用内部迭代,这是流式编程的核心特性之一。这种机制使得编写的代码可读性更强,也更能利用多核处理器的优势。通过放弃对迭代过程的控制,我们把控制权交给并行化机制。我们将在并发编程一章中学习这部分内容。

另一个重要方面,流是懒加载的。这代表着它只在绝对必要时才计算。你可以将流看作“延迟列表”。由于计算延迟,流使我们能够表示非常大(甚至无限)的序列,而不需要考虑内存问题。

流创建

// streams/StreamOf.java
import java.util.stream.*;
public class StreamOf {
    public static void main(String[] args) {
        Stream.of(new Bubble(1), new Bubble(2), new Bubble(3))
            .forEach(System.out::println);
        Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!")
            .forEach(System.out::print);
        System.out.println();
        Stream.of(3.14159, 2.718, 1.618)
            .forEach(System.out::println);
    }
}

输出

Bubble(1)
Bubble(2)
Bubble(3)
It's a wonderful day for pie!
3.14159
2.718
1.618
// streams/CollectionToStream.java
import java.util.*;
import java.util.stream.*;
public class CollectionToStream {
    public static void main(String[] args) {
        List<Bubble> bubbles = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3));
        System.out.println(bubbles.stream()
            .mapToInt(b -> b.i)
            .sum());

        Set<String> w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" ")));
        w.stream()
         .map(x -> x + " ")
         .forEach(System.out::print);
        System.out.println();

        Map<String, Double> m = new HashMap<>();
        m.put("pi", 3.14159);
        m.put("e", 2.718);
        m.put("phi", 1.618);
        m.entrySet().stream()
                    .map(e -> e.getKey() + ": " + e.getValue())
                    .forEach(System.out::println);
    }
}

输出

6
a pie! It's for wonderful day
phi: 1.618
e: 2.718
pi: 3.14159

mapToInt() 方法将一个对象流(object stream)转换成为包含整型数字的 IntStream

// streams/RandomGenerators.java
import java.util.*;
import java.util.stream.*;
public class RandomGenerators {
    public static <T> void show(Stream<T> stream) {
        stream
        .limit(4)
        .forEach(System.out::println);
        System.out.println("++++++++");
    }

    public static void main(String[] args) {
        Random rand = new Random(47);
        show(rand.ints().boxed());
        show(rand.longs().boxed());
        show(rand.doubles().boxed());
        // 控制上限和下限:
        show(rand.ints(10, 20).boxed());
        show(rand.longs(50, 100).boxed());
        show(rand.doubles(20, 30).boxed());
        // 控制流大小:
        show(rand.ints(2).boxed());
        show(rand.longs(2).boxed());
        show(rand.doubles(2).boxed());
        // 控制流的大小和界限
        show(rand.ints(3, 3, 9).boxed());
        show(rand.longs(3, 12, 22).boxed());
        show(rand.doubles(3, 11.5, 12.3).boxed());
    }
}

输出

-1172028779
1717241110
-2014573909
229403722
++++++++
2955289354441303771
3476817843704654257
-8917117694134521474
4941259272818818752
++++++++
0.2613610344283964
0.0508673570556899
0.8037155449603999
0.7620665811558285
++++++++
16
10
11
12
++++++++
65
99
54
58
++++++++
29.86777681078574
24.83968447804611
20.09247112332014
24.046793846338723
++++++++
1169976606
1947946283
++++++++
2970202997824602425
-2325326920272830366
++++++++
0.7024254510631527
0.6648552384607359
++++++++
6
7
7
++++++++
17
12
20
++++++++
12.27872414236691
11.732085449736195
12.196509449817267
++++++++

为了消除冗余代码,我创建了一个泛型方法 show(Stream<T> stream) (在讲解泛型之前就使用这个特性,确实有点作弊,但是回报是值得的)。类型参数 T 可以是任何类型,所以这个方法对 Integer、Long 和 Double 类型都生效。但是 Random 类只能生成基本类型 int, long, double 的流。幸运的是, boxed() 流操作将会自动地把基本类型包装成为对应的装箱类型,从而使得 show() 能够接受流。

一段读文件内容的代码

// streams/RandomWords.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
import java.io.*;
import java.nio.file.*;
public class RandomWords implements Supplier<String> {
    List<String> words = new ArrayList<>();
    Random rand = new Random(47);
    RandomWords(String fname) throws IOException {
        List<String> lines = Files.readAllLines(Paths.get(fname));
        // 略过第一行
        for (String line : lines.subList(1, lines.size())) {
            for (String word : line.split("[ .?,]+"))
                words.add(word.toLowerCase());
        }
    }
    public String get() {
        return words.get(rand.nextInt(words.size()));
    }
    @Override
    public String toString() {
        return words.stream()
            .collect(Collectors.joining(" "));
    }
    public static void main(String[] args) throws Exception {
        System.out.println(
            Stream.generate(new RandomWords("Cheese.dat"))
                .limit(10)
                .collect(Collectors.joining(" ")));
    }
}

当你使用 Collectors.joining(),你将会得到一个 String 类型的结果,每个元素都根据 joining() 的参数来进行分割。还有许多不同的 Collectors 用于产生不同的结果。

在主方法中,我们提前看到了 Stream.generate() 的用法,它可以把任意 Supplier<T> 用于生成 T 类型的流。

IntStream的一些小tip

// streams/Ranges.java
import static java.util.stream.IntStream.*;
public class Ranges {
    public static void main(String[] args) {
        // 传统方法:
        int result = 0;
        for (int i = 10; i < 20; i++)
            result += i;
        System.out.println(result);
        // for-in 循环:
        result = 0;
        for (int i : range(10, 20).toArray())
            result += i;
        System.out.println(result);
        // 使用流:
        System.out.println(range(10, 20).sum());
    }
}

输出

145
145
145

repeat代替for循环

// onjava/Repeat.java
package onjava;
import static java.util.stream.IntStream.*;
public class Repeat {
    public static void repeat(int n, Runnable action) {
        range(0, n).forEach(i -> action.run());
    }
}
// streams/Looping.java
import static onjava.Repeat.*;
public class Looping {
    static void hi() {
        System.out.println("Hi!");
    }
    public static void main(String[] args) {
        repeat(3, () -> System.out.println("Looping!"));
        repeat(2, Looping::hi);
    }
}

输出

Looping!
Looping!
Looping!
Hi!
Hi!
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

这里我们的传给Runnable的是什么呢?虽然我觉得这边有点像多态的感觉,但是我们传的并不是一个类啊,是一个方法体,这边我暂时的理解是一个实现接口,以后明白了再补充上。

generate()

// streams/Generator.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class Generator implements Supplier<String> {
    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);
    }
}

输出

YNZBRNYGCFOWZNTCQRGSEGZMMJMROE

如果要创建包含相同对象的流,只需要传递一个生成那些对象的 lambda 到 generate() 中:

// streams/Duplicator.java
import java.util.stream.*;
public class Duplicator {
    public static void main(String[] args) {
        Stream.generate(() -> "duplicate")
              .limit(3)
              .forEach(System.out::println);
    }
}

输出

// streams/Duplicator.java
import java.util.stream.*;
public class Duplicator {
    public static void main(String[] args) {
        Stream.generate(() -> "duplicate")
              .limit(3)
              .forEach(System.out::println);
    }
}
// streams/Bubble.java
import java.util.function.*;
public class Bubble {
    public final int i;

    public Bubble(int n) {
        i = n;
    }

    @Override
    public String toString() {
        return "Bubble(" + i + ")";
    }

    private static int count = 0;
    public static Bubble bubbler() {
        return new Bubble(count++);
    }
}

由于 bubbler() 与 Supplier<Bubble> 是接口兼容的,我们可以将其方法引用直接传递给 Stream.generate()

// streams/Bubbles.java
import java.util.stream.*;
public class Bubbles {
    public static void main(String[] args) {
        Stream.generate(Bubble::bubbler)
              .limit(5)
              .forEach(System.out::println);
    }
}

输出

Bubble(0)
Bubble(1)
Bubble(2)
Bubble(3)
Bubble(4)

这个generate有点工厂类的味道,反正看起来很舒服,好理解,之前还对这类的感觉不好理解,现在想一下这样好像易读性更好。

Iterate()

生成一个斐波那契

// streams/Fibonacci.java
import java.util.stream.*;
public class Fibonacci {
    int x = 1;

    Stream<Integer> numbers() {
        return Stream.iterate(0, i -> {
            int result = x + i;
            x = i;
            return result;
        });
    }

    public static void main(String[] args) {
        new Fibonacci().numbers()
                       .skip(20) // 过滤前 20 个
                       .limit(10) // 然后取 10 个
                       .forEach(System.out::println);
    }
}

输出

6765
10946
17711
28657
46368
75025
121393
196418
317811
514229

斐波那契数列将数列中最后两个元素进行求和以产生下一个元素。iterate() 只能记忆结果,因此我们需要利用一个变量 x 追踪另外一个元素。

在主方法中,我们使用了一个之前没有见过的 skip() 操作。它根据参数丢弃指定数量的流元素。在这里,我们丢弃了前 20 个元素。

这里的函数表达式改变的就是外部变量x,因为并不是一个参数传过来的所以没有问题。

流的建造者模式

// streams/FileToWordsBuilder.java
import java.io.*;
import java.nio.file.*;
import java.util.stream.*;

public class FileToWordsBuilder {
    Stream.Builder<String> builder = Stream.builder();

    public FileToWordsBuilder(String filePath) throws Exception {
        Files.lines(Paths.get(filePath))
             .skip(1) // 略过开头的注释行
             .forEach(line -> {
                  for (String w : line.split("[ .?,]+"))
                      builder.add(w);
              });
    }

    Stream<String> stream() {
        return builder.build();
    }

    public static void main(String[] args) throws Exception {
        new FileToWordsBuilder("Cheese.dat")
            .stream()
            .limit(7)
            .map(w -> w + " ")
            .forEach(System.out::print);
    }
}

这边需要对builder有了解,当然现在基本都用autoValue

先吃饭,回来再写

12-26 01:40
查看更多