Java8之Stream-函式式介面

IT小生發表於2019-03-03

實習前只是粗略的看了下Java8的一些基本語法,但是沒有系統的學習過.在使用一段時間後決定系統的對其進行一次分析,加深對Java8函數語言程式設計的理解,提高自己的編碼技巧.另外kotlin崛起,感興趣的朋友嘗試下混編也未嘗不可.


函式式介面

函式式介面,對於Java來說就是介面內只有一個公開方法的介面,因為使用lanbda表示式,例如() -> user.getName()對應的呼叫則可能是func.get(),編譯器會根據介面推斷所屬於的方法,如果有兩個則無法推斷.Java8提供了很多函式式介面,一般都使用註解@FunctionalInterface宣告,有必要了解如下一些函式式介面.

函式式介面 引數型別 返回型別 描述
Supplier T 接收一個T型別的值
Consumer T 處理一個T型別的值
BiConsumer T,U 處理T型別和U型別的值
Predicate T boolean 處理T型別的值,並返回true或者false.
ToIntFunction T int 處理T型別的值,並返回int值
ToLongFunction T long 處理T型別的值,並返回long值
ToDoubleFunction T double 處理T型別的值,並返回double值
Function T R 處理T型別的值,並返回R型別值
BiFunction T,U R 處理T型別和U型別的值,並返回R型別值
BiFunction T,U R 處理T型別和U型別的值,並返回R型別值
UnaryOperator T T 處理T型別值,並返回T型別值,
BinaryOperator T,T T 處理T型別值,並返回T型別值

以上的函式每一個代表的都是一種基本的操作,操作之間可以自由組合,所以才有了stream這些靈活的操作.

Stream操作

Stream的操作是建立在函式式介面的組合上的,最好的學習方法是看Stream介面來學習.下面舉一些例子來分析,假設有這樣的一些初始資料.

List<String> testData = new ArrayList<String>();
    testData.add("張三");
    testData.add("李四");
    testData.add("王二");
    testData.add("麻子");複製程式碼

filter

    Stream<T> filter(Predicate<? super T> predicate);複製程式碼

filter接收predicate函式,predicate是接收T值,返回boolean值,那麼對應的引用就可以寫成如下形式,意思是取集合中以`張`開頭的名字.

testData.stream()
        .filter(x -> x.startsWith("張"))複製程式碼

map

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);複製程式碼

map操作接收的是Function介面,對於Function接收T值返回R值,那map的作用就很明顯是轉換用的,比如下面程式碼,轉換名稱為對應的名稱長度,也就是從輸入String資料返回int資料.

testData.stream()
        .map(x -> x.length())複製程式碼

flatMap

    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);複製程式碼

flatMap和map都是使用Function介面,不同的是返回值flatMap限定為Stream型別.所以flatMap可以作為合併流使用,如以下程式碼,提取出所有的字元.

testData.stream()
        .flatMap(x -> Stream.of(x.split("")))
        .collect(Collectors.toList());
        //輸出  [張, 三, 李, 四, 王, 二, 麻, 子]複製程式碼

peek

    Stream<T> peek(Consumer<? super T> action);複製程式碼

peek引數為Consumer,Consumer接收T值,無返回,那麼該方法就可以作為除錯不影響stream中內容的一些操作,不過由於物件都是地址引用,你再此做一些物件內容操作也是可以的.
reduce

<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);複製程式碼

Reduce比較複雜的一個介面,屬於歸納性操作,看引數,第一個是U泛型,也就是輸入型別的引數,最為初始值,第二個BiFunction,接收T,U引數,返回U型別引數,BinaryOperator接收U,U型別,並返回U型別.

    StringBuilder identity = new StringBuilder();
    StringBuilder reduce = testData.stream()
        .flatMap(x -> Stream.of(x.split("")))
        .reduce(identity, (r, x) -> {
          r.append(x);
          return r;
        }, StringBuilder::append);
    System.out.println(identity == reduce);
    System.out.println(reduce.toString());
    //輸出 true
   //  張三李四王二麻子複製程式碼

首先提供一個基本容器identity,然後兩個引數r即是identity,x為每次輸入引數,最後一個StringBuilder::append是併發下多個identity的合併策略.
再舉個例子,既然reduce屬於歸納性操作,那麼也可以當成collect使用,如下:

 ArrayList<String> identity = new ArrayList<>();
    ArrayList<String> result = testData.stream()
        .flatMap(x -> Stream.of(x.split("")))
        .reduce(identity, (r, x) -> {
          r.add(x);
          return r;
        },(r1,r2) -> {
          r1.addAll(r2);
          return r1;
        });
    System.out.println(identity == result);
    System.out.println(result);
    //輸出 true
    //[張, 三, 李, 四, 王, 二, 麻, 子]複製程式碼

強大的collect

collect無疑是stream中最強大的操作,掌握了collect操作才能說掌握了stream.為了便於使用者,Java提供了Collectors類,該類提供了很多便捷的collect操作,如Collector<T, ?, List<T>> toList(),Collector<T, ?, Set<T>> toSet()等操作.這些操作最終都會呼叫如下建構函式構造出collector物件,因此掌握該本質是最佳的學習方式.

CollectorImpl(Supplier<A> supplier,
                      BiConsumer<A, T> accumulator,
                      BinaryOperator<A> combiner,
                      Function<A,R> finisher,
                      Set<Characteristics> characteristics) {
            this.supplier = supplier;
            this.accumulator = accumulator;
            this.combiner = combiner;
            this.finisher = finisher;
            this.characteristics = characteristics;
        }複製程式碼

Supplier類似reduce中的u,接收一個後設資料,BiConsumer則是運算元據,BinaryOperator併發下聚合,finisher完成時的轉換操作,Set應該按照定義是優化一些操作中的轉換.如下面的toList()操作,其finish操作為castingIdentity().

   public static <T>
    Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_ID);
    }複製程式碼

再看toMap的實現

    public static <T, K, U, M extends Map<K, U>>
    Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction,
                                Supplier<M> mapSupplier) {
        BiConsumer<M, T> accumulator
                = (map, element) -> map.merge(keyMapper.apply(element),
                                              valueMapper.apply(element), mergeFunction);
        return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
    }複製程式碼

Function作為轉換函式提供了key和value的轉換,BinaryOperator提供了重複key合併策略,mapSupplier則表示最終收集到的容器.那麼使用就很簡單了

HashMap<Character, String> map = testData.stream()
        .collect(Collectors.toMap(x -> x.charAt(0), Function.identity()
            , (v1, v2) -> v2, HashMap::new));複製程式碼

其他還有很多方法,就不一一敘述,主要是瞭解這些介面,知道他所擁有的功能,以及組合的意義,即可很好的掌握Java中的函數語言程式設計.

個人部落格 mrdear.cn ,歡迎交流

相關文章