【Java】《OnJava8》笔记——第14章流式编程

《On Java 8》中文版,又名《Java编程思想》 第5版
接下来的都是个人学习过程的笔记,不是总结,没有参考价值,但是这本书很棒

流创建

package chapter14;

import java.util.*;
import java.util.stream.Stream;

public class ConllectionToStream {
    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 pie!".split(" ")));
        w.stream().map(x->x+" ").forEach(System.out::print);
        System.out.println();

        Map<String,Double> m=new HashMap<>();
        m.put("p1",3.14);
        Stream<Map.Entry<String, Double>> stream = m.entrySet().stream();
        stream.map((e->e.getKey()+": "+e.getValue()))
                .forEach(System.out::println);

        m.put("e", 2.718);
        stream.map((e->e.getKey()+": "+e.getValue()))
                .forEach(System.out::println);//会报错
    }
}

上面会报错stream has already been operated upon or closed,也就是说,流在获取后,如果源被修改,这个流就要重新获取

随机数流

// 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());
    }
}

对于boxed方法,书上的解释是

但是 Random 类只能生成基本类型 int, long, double 的流。幸运的是, boxed() 流操作将会自动地把基本类型包装成为对应的装箱类型,从而使得 show() 能够接受流。

rand.ints()的返回是IntStream类型,其完整声明为public interface IntStream extends BaseStream<Integer, IntStream>,而Stream类型的完整声明为public interface Stream<T> extends BaseStream<T, Stream<T>>,可以看出两者没有继承关系(我原本以为有呢)。而boxed()就是进行转换用的

// 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(" ")));
    }
}

首先,上面运行过程中,toString没有被调用;然后Stream.generate会根据Supplier生成一个Streamlimit没啥好说的;最后,collectStream类的一个方法,顾名思义,将流内元素收集起来,该方法接受一个Collector对象,这个对象指导流如何进行收集操作

generate()

package chapter14;

import java.util.function.Supplier;
import java.util.stream.Stream;

public class Bubbles {
    public static void main(String[] args) {
        Supplier<Object> s=Bubble::bubbler;
        Supplier<Bubble> b=Bubble::bubbler;

        Stream.generate(s).limit(5).forEach(System.out::println);
    }
}

上面代码里,sb都是通过lambda表达式构建的,但s和b的泛型类型是不一样的。实际上Bubble::bubbler返回的类是什么,是根据声明的时候的变量类型来决定的。就比如sSupplier<Object>类的,那么表达式返回的就是这个类的。(当然,泛型只在编译期起作用)

iterate()

package chapter14;

import java.util.stream.Stream;

public class Fibonacci {
    int x=1;
    Stream<Integer> numbers(){
        return Stream.iterate(0,i->{
            System.out.println("damn"+i);
            int result=x+i;
            x=i;
            return result;
        });
    }

    public static void main(String[] args) {
        new Fibonacci().numbers()
                .skip(20)
                .limit(10)
                .forEach(System.out::println);
    }
}

Stream.iterate() 以种子(第一个参数)开头,并将其传给方法(第二个参数)。方法的结果将添加到流,并存储作为第一个参数用于下次调用 iterate(),依次类推。上面可以看出,result每次都会存一下,用作下一次地调用的输入

流的建造者模式

package chapter14;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;

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

    public FileToWordsBuilder(String filePath) throws IOException {
        Files.lines(Paths.get(filePath))
                .forEach(line->{
                    for (String w:line.split("[ .?,]+"))
                        builder.add(w);
                });
    }

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

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

没什么,只是觉得知道了如何构建一个流,挺好的

中间操作

跟踪和调试

// streams/Peeking.java
class Peeking {
    public static void main(String[] args) throws Exception {
        FileToWords.stream("Cheese.dat")
        .skip(21)
        .limit(4)
        .map(w -> w + " ")
        .peek(System.out::print)
        .map(String::toUpperCase)
        .peek(System.out::print)
        .map(String::toLowerCase)
        .forEach(System.out::print);
    }
}

peek方法接受一个Consumer对象,其功能如下所述,就是对输入流中的每个元素进行一个操作,并且返回这个流

Returns a stream consisting of the elements of this stream, additionally performing the provided action on each element as elements are consumed from the resulting stream.

流元素排序

public class SortedComparator {
    public static void main(String[] args) throws IOException {
        FileToWords.stream("D:\\code\\OnJava8\\src\\chapter14\\Cheese.dat")
                .skip(10)
                .limit(10)
//                .sorted(Comparator.reverseOrder())
                .sorted((String o1, String o2) ->1)
                .map(w->w+" ")
                .forEach(System.out::print);
    }
}

也可以用lambda表达式,String加和不加都行,不加的话,会根据这个流的泛型类进行推断。

移除元素

下面的代码的作用是找质数,isPrime的功能是判断一个数n是不是质数,其中rangeClosed方法提供的是一个包含右端点的数字流,noneMatch方法是对流中每个元素进行判别,如果所有元素的判别都是false,那么返回true(顾名思义嘛,noneMatch没有元素能match上)。而iterate方法之前说过,是一个迭代方法,生成从2开始的整数流

// streams/Prime.java
import java.util.stream.*;
import static java.util.stream.LongStream.*;
public class Prime {
    public static Boolean isPrime(long n) {
        return rangeClosed(2, (long)Math.sqrt(n))
        .noneMatch(i -> n % i == 0);
    }
    public LongStream numbers() {
        return iterate(2, i -> i + 1)
        .filter(Prime::isPrime);
    }
    public static void main(String[] args) {
        new Prime().numbers()
        .limit(10)
        .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        new Prime().numbers()
        .skip(90)
        .limit(10)
        .forEach(n -> System.out.format("%d ", n));
    }
}

在map中组合流

Files.lines方法会产生一个字符串流,每个元素其实都是一行,也就是“行流”,如果简单地使用map方法,对每行使用splitAsStream,那生成的就是一个流的流,随后flat就得了

// streams/FileToWords.java
import java.nio.file.*;
import java.util.stream.*;
import java.util.regex.Pattern;
public class FileToWords {
    public static Stream<String> stream(String filePath) throws Exception {
        return Files.lines(Paths.get(filePath))
          .skip(1) // First (comment) line
          .flatMap(line ->
            Pattern.compile("\\W+").splitAsStream(line));
    }
}

Optional类

每次我都会为Stream.<String>empty()这种方法感到震惊

注意,空流是通过 Stream.empty() 创建的。如果你在没有任何上下文环境的情况下调用 Stream.empty(),Java 并不知道它的数据类型;这个语法解决了这个问题。如果编译器拥有了足够的上下文信息,比如:Stream<String> s = Stream.empty();,就可以在调用 empty() 时推断类型。

java的Optional类和Scala里的Option类一样啊= =

// streams/OptionalBasics.java
import java.util.*;
import java.util.stream.*;
class OptionalBasics {
    static void test(Optional<String> 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.<String>empty().findFirst());
    }
}

Optional对象的操作

下面的filter方法是归属于Optional类的。不是基本的Stream类的filter方法

// streams/OptionalFilter.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
class OptionalFilter {
    static String[] elements = {
            "Foo", "", "Bar", "Baz", "Bingo"
    };
    static Stream<String> testStream() {
        return Arrays.stream(elements);
    }
    static void test(String descr, Predicate<String> pred) {
        System.out.println(" ---( " + descr + " )---");
        for(int i = 0; i <= elements.length; i++) {
            System.out.println(
                    testStream()
                            .skip(i)
                            .findFirst()
                            .filter(pred));
        }
    }
    public static void main(String[] args) {
        test("true", str -> true);
        test("false", str -> false);
        test("str != \"\"", str -> str != "");
        test("str.length() == 3", str -> str.length() == 3);
        test("startsWith(\"B\")",
                str -> str.startsWith("B"));
    }
}

flatMap是应用于已生成 Optional 的映射函数,所以 flatMap()不会像 map()那样将结果封装在 Optional 中,因此,flatMap 将提取非空 Optional 的内容并将其应用在映射函数。唯一的区别就是 flatMap()不会把结果包装在 Optional 中。
看方法定义可以看出,public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper),该方法的映射函数必须得是生成Optional

Optional流

// streams/Signal.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class Signal {
    private final String msg;
    public Signal(String msg) { this.msg = msg; }
    public String getMsg() { return msg; }
    @Override
    public String toString() {
        return "Signal(" + msg + ")";
    }
    static Random rand = new Random(47);
    public static Signal morse() {
        switch(rand.nextInt(4)) {
            case 1: return new Signal("dot");
            case 2: return new Signal("dash");
            default: return null;
        }
    }
    public static Stream<Optional<Signal>> stream() {
        return Stream.generate(Signal::morse)
                .map(signal -> Optional.ofNullable(signal));
    }
}

// streams/StreamOfOptionals.java
import java.util.*;
import java.util.stream.*;
public class StreamOfOptionals {
    public static void main(String[] args) {
        Signal.stream()
                .limit(10)
                .forEach(System.out::println);
        System.out.println(" ---");
        Signal.stream()
                .limit(10)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .forEach(System.out::println);
    }
}

上面两段代码主要看的是Signal.stream().limit(10).filter(Optional::isPresent) .map(Optional::get).forEach(System.out::println);这一段,filter方法接受的是Predicate对象,该类是一个接口,只有一个boolean test(T t);方法需要实现,而Optional::get正是上一章节中讲过的非绑定方法引用,这个get方法是个非静态方法,他没有参数。这也就是说,Optional::isPresent会生成一个Predicate对象,这个对象的test方法长下面这样

boolean test(Optional t){
	return t.isPresent();
}

终端操作

收集

package chapter14;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

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

toCollection的功能如下

Returns a Collector that accumulates the input elements into a new {@code Collection}, in encounter order. The {@code Collection} is created by the provided factory.

// streams/MapCollector.java
import java.util.*;
import java.util.stream.*;
class Pair {
    public final Character c;
    public final Integer i;
    Pair(Character c, Integer i) {
        this.c = c;
        this.i = i;
    }
    public Character getC() { return c; }
    public Integer getI() { return i; }
    @Override
    public String toString() {
        return "Pair(" + c + ", " + i + ")";
    }
}
class RandomPair {
    Random rand = new Random(47);
    // An infinite iterator of random capital letters:
    Iterator<Character> capChars = rand.ints(65,91)
            .mapToObj(i -> (char)i)
            .iterator();
    public Stream<Pair> stream() {
        return rand.ints(100, 1000).distinct()
                .mapToObj(i -> new Pair(capChars.next(), i));
    }
}
public class MapCollector {
    public static void main(String[] args) {
        Map<Integer, Character> map =
                new RandomPair().stream()
                        .limit(8)
                        .collect(
                                Collectors.toMap(Pair::getI, Pair::getC));
        System.out.println(map);
    }
}

可以看到RandomPair中的stream方法返回了一个对象流,这个流是组合多个流以生成的新的对象流。书上说使用迭代器是唯一可以完成的方法。
这个代码里的toMap方法需要两个参数,是两个Function对象,前者是生成key,后者生成value

// streams/SpecialCollector.java
import java.util.*;
import java.util.stream.*;
public class SpecialCollector {
    public static void main(String[] args) throws Exception {
        ArrayList<String> words =
                FileToWords.stream("Cheese.dat")
                        .collect(ArrayList::new,
                                ArrayList::add,
                                ArrayList::addAll);
        words.stream()
                .filter(s -> s.equals("cheese"))
                .forEach(System.out::println);
    }
}

官方文档对这个方法介绍的听明白的

Performs a mutable reduction operation on the elements of this stream. A mutable reduction is one in which the reduced value is a mutable result container, such as an ArrayList, and elements are incorporated by updating the state of the result rather than by replacing the result. This produces a result equivalent to:

R result = supplier.get();
for (T element : this stream)
     accumulator.accept(result, element);
return result;

至于第三个参数BiConsumer<R,R> combiner,指的是这个方法可以将两个R类型的容器结合起来。至于有啥用,我也没看明白

组合所有流元素

// streams/Reduce.java
import java.util.*;
import java.util.stream.*;
class Frobnitz {
    int size;
    Frobnitz(int sz) { size = sz; }
    @Override
    public String toString() {
        return "Frobnitz(" + size + ")";
    }
    // Generator:
    static Random rand = new Random(47);
    static final int BOUND = 100;
    static Frobnitz supply() {
        return new Frobnitz(rand.nextInt(BOUND));
    }
}
public class Reduce {
    public static void main(String[] args) {
        Stream.generate(Frobnitz::supply)
                .limit(10)
                .peek(System.out::println)
                .reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1)
                .ifPresent(System.out::println);
    }
}

打印结果如下,其中前十个个第一个peek方法打印出来的,最后一个才是最后一个ifPresent的打印结果。reduce方法接受BinaryOperator<T>对象,至于其为什么返回Optional对象,这是为了应付流为空的状况

Frobnitz(58)
Frobnitz(55)
Frobnitz(93)
Frobnitz(61)
Frobnitz(61)
Frobnitz(29)
Frobnitz(68)
Frobnitz(0)
Frobnitz(22)
Frobnitz(7)
Frobnitz(29)

这个Optional<T> reduce(BinaryOperator<T> accumulator)做的事在文档中描述的也很清楚

Performs a reduction on the elements of this stream, using an associative accumulation function, and returns an Optional describing the reduced value, if any. This is equivalent to:

boolean foundAny = false;
T result = null;
for (T element : this stream) {
     if (!foundAny) {
         foundAny = true;
         result = element;
     }
     else
         result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();

实际上Stream.generate(Frobnitz::supply)产生的流的类型是ReferencePipeline的,其reduce方法如下

@Override
public final Optional<P_OUT> reduce(BinaryOperator<P_OUT> accumulator) {
    return evaluate(ReduceOps.makeRef(accumulator));
}

evaluate方法实际上是一个返回泛型类的方法final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp),这引起了我的疑问,返回的既然是泛型类,为什么能和Optional<P_OUT>的签名一致呢,我做了个小实验,如下所示,发现编译器好像可以进行类型推断,testS()返回类型为String,那么其调用test的时候,就认为泛型R就是String

public class TestParamType {
    <R> R test(R a){
        return a;
    }

    String testS(){
        return test("a");
    }
}

匹配

// streams/Matching.java
// Demonstrates short-circuiting of *Match() operations
import java.util.stream.*;
import java.util.function.*;
import static streams.RandInts.*;

interface Matcher extends BiPredicate<Stream<Integer>, Predicate<Integer>> {}

public class Matching {
    static void show(Matcher match, int val) {
        System.out.println(
                match.test(
                        IntStream.rangeClosed(1, 9)
                                .boxed()
                                .peek(n -> System.out.format("%d ", n)),
                        n -> n < val));
    }
    public static void main(String[] args) {
        show(Stream::allMatch, 10);
        show(Stream::allMatch, 4);
        show(Stream::anyMatch, 2);
        show(Stream::anyMatch, 0);
        show(Stream::noneMatch, 5);
        show(Stream::noneMatch, 0);
    }
}

这段代码真的很好很好,以show(Stream::allMatch, 4);为例,Stream::allMatch是通过非绑定方法生成的一个Matcher对象,这个对象里的test方法长成如下这样

boolean test(Stream<Integer> t, Predicate<Integer> u){
	return t.allMatch(u);
}

所以说test方法返回的是allMatch的结果,而show(Stream::allMatch, 4)的的功能等同于下面代码

System.out.println(IntStream.rangeClosed(1, 9)
                .boxed()
                .peek(n -> System.out.format("%d ", n))
                .allMatch(n->n<4))

其结果如下所示

1 2 3 4 false

诶我们发现,IntStream.rangeClosed(1, 9)生成的明明是1到9的整数流,并且通过peek进行了打印,而为什么只打印了1 2 3 4呢。个人揣测这是因为流的特性,因为流并不存储数据,而只是一个结构,上面的例子中,IntStream.rangeClosed(1, 9).boxed()先生成一个Stream<Integer>流,这个流就是我们要操作的流,在应用的时候,他先生成1,然后调用peek方法进行打印,再调用allMatchPredicate对象的test方法,发现返回的true,相安无事,继续;然后再生成一个2,重复上面的过程;直到这个流生成了4,执行peek方法没啥事儿,但当执行到了allMatch的时候,发现test的返回是false了,那么由于短路特性,这个流不会再进行后续的计算,直接执行结束,并返回allMatch的执行结果。

信息

数字流信息

信息和数字流信息的统计值返回的都是Optional类以及衍生的对象

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章