【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類以及衍生的對象

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