Java8: Stream

寫在前面: Java8的Stream用起來真的不是通常的爽。當你看到Stream的操做後,相信你不再會去寫各類for循環、嵌套for循環,特別是作報表,體會更深.java

What is Stream?

流(Stream)是Java API的新成員,它容許以聲明性的方式處理數據集合(相似於數據庫查詢語句).暫且理解爲遍歷數據集的高級迭代器.git

先舉個例子嚐嚐鮮:github

/*
需求: 獲取菜單中熱量小於400卡路里的菜餚名稱,並按照卡路里排序.
*/
@Data
@Accessor(chain = true)
public class Dish {
    // 該類將會在本文中屢次用到
    // omit getter,setter and constructor
    private String name;
    private boolean vegetarian;
    private int calories;
    private Type type;

    public enum Type {MEAT, FISH, OTHER}
}

// 普通寫法
public static List<String> getLowCaloricDishesNamesInJava7(List<Dish> dishes){
    List<Dish> lowCaloricDishes = new ArrayList<>();
    for(Dish d: dishes){
        if(d.getCalories() < 400){
            lowCaloricDishes.add(d);
        }
    }
    List<String> lowCaloricDishesName = new ArrayList<>();
    Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
        public int compare(Dish d1, Dish d2){
            return Integer.compare(d1.getCalories(), d2.getCalories());
        }
    });
    for(Dish d: lowCaloricDishes){
        lowCaloricDishesName.add(d.getName());
    }
    return lowCaloricDishesName;
}
// Stream 寫法
public static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes){
    return dishes.stream()
        .filter(d -> d.getCalories() < 400)
        .sorted(comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(toList());
}

從上面的代碼能夠明顯看出區別,Stream寫法更加簡短、優美,而且可讀性很強,我看到這段代碼我就知道是幹什麼的.這就引伸出Stream的優勢:數據庫

  • 聲明性 -- 更簡潔,更易讀(表達能力很強)
  • 可複合 -- 更靈活(可過濾,可映射,可排序...)
  • 可並行 -- 性能更好(使用parallelStream)

Introduce of Stream

流就是從支持數據處理操做的源生成的元素序列數組

  • 元素序列: 我理解的存儲流數據的數據結構(?)
  • 源: 提供數據的源,如集合,數組,輸入/輸出資源等.從有序集合生成流時會保留原有的順序.
  • 數據處理操做: filter,sort,map,find...流操做能夠順序執行,也能夠並行執行.

Stream and Collection

集合是數據結構,因此它的主要目的是存儲和訪問集合元素,但流的目的是在於計算.數據結構

集合講的是數據,集合能夠遍歷無數次,而流只能遍歷一次,遍歷完以後咱們就說這個流被消費掉了.準確的說,流只能被消費一次,那些終端操做都是消費流.app

Stream<Integer> s = Arrays.asList(1,2,3,4).stream();
s.forEach(System.out.print);  // 打印:1 2 3 4
s.forEach(System.out.print);  // 無打印

Inner Iteration and Outer Iteration

使用集合須要咱們本身去作迭代(好比for-each),這就叫外部迭代.相反,Stream庫使用內部迭代,也就是不須要咱們去作迭代.好比:ide

// 仍是上面那個Dish類,假設有個對象List<Dish> menu, 要打印menu中全部菜餚的名稱
// 外部迭代
for(Dish d : menu) {
    System.out.println(d.getName());
}
// 內部迭代
menu.stream().map(Dish::getName).forEach(System.out::println);

Operations of Stream

先看一個例子:函數

menu.stream().filter(d -> d.getCalories() > 300).map(Dish::getName).forEach(System.out::println);
  • 生成流: menu.steam(),生成流的方式有好多,如Arrays.stream(),可本身去查看Java API
  • 中間操做(流的延遲性質): 處理流並返回流,好比filter,map,distinct等,相似fluent API.
  • 終端操做: 消費流數據,好比forEach,collect等.

流的延遲性質

若是流水線上沒有觸發一個終端操做,那麼中間操做是不會對流數據進行處理的.這是由於中間操做通常能夠合併起來,在終端操做時一次性處理.好比:性能

List<String> names = menu.stream().filter(d -> {
        System.out.println("filtering");
        return d.getCalories() > 300;
    }).map(d -> {
        System.out.println("mapping");
        return d.getName();
    }).limit(3).collect(toList());
/*
上述的代碼輸出:
    filtering
    mapping
    filtering
    mapping
    filtering
    mapping
*/

從上述的打印結果明顯能夠看出來,流會對中間操做進行合併,儘管filter和map是兩個獨立的操做,但它們合併到同一次遍歷中了(循環合併).

Common Operations

不少流操做的方法參數類型都是函數式接口,這些函數式接口都是JDK自帶的,本文將不會解釋這些函數式接口,能夠本身看接口的定義.

操做 類型 參數類型 函數描述符 描述
filter 中間 Predicate<T> T -> Boolean 過濾
distinct 中間 去重
skip 中間 long 跳過前幾項
limit 中間 long 只取前幾項
map 中間 Function<T,R> T -> R 映射
flatMap 中間 Function<T,Stream<R>> T -> Stream<R> 扁平化流
sorted 中間 Comparator<T> (T,T) -> int 排序
anyMatch 終端 Predicate<T> T -> Boolean 任意項匹配
noneMatch 終端 Predicate<T> T -> Boolean 無匹配
allMatch 終端 Predicate<T> T -> Boolean 全部匹配
findAny 終端 返回任意項
findFirst 終端 返回第一項
forEach 終端 Consumer<T> T -> void 遍歷流
collect 終端 Collector<T,A,R> 收集流數據
reduce 終端 BinaryOperator<T> (T,T) -> T 歸約
count 終端 long 數量

上面這些都是經常使用的流操做,順便提一下,使用skip和limit還能夠作分頁操做.下面講解一下map,flatMap,reduce

映射: map

映射,也就是我從一個數據通過某些操做變成了另外一個數據,也就是x --> y

x --f(x)--> y

舉個栗子:

// 獲取List<Dish> menu中全部菜餚的名稱
Stream<Dish> ds = menu.stream();  //菜單流
Stream<String> ns = menu.stream().map(e -> e.getName());  //菜餚名稱流

扁平化: flatMap

扁平化流,這是<<Java8實戰>>中這麼翻譯的,按我我的理解的話,我以爲flatMap就是合併流:

Stream<T> + Stream<T> + Stream<T> ==> Stream<R>

舉個栗子:

// 有一個String集合,須要將每一個String切分紅字符,並去重
List<String> ss = Arrays.asList("Hello", "World")
                        .stream()                   // Stream<String>
                        .map(e -> e.split(""))      // Stream<String[]>
                        .flatMap(Arrays::stream)    // Stream<String>
                        .distinct()
                        .collect(toList());
ss.forEach(System.out::print);
/*
輸出:
Helowrd
*/

對於上面這個栗子,執行map操做後返回Stream<String[]>(String[]指的是流元素的類型),接下來執行flatMap,將String[] -> Stream<String>, 而後將多個Stream<String>合併成一個Stream<String>.
下面這張圖能夠很形象地解釋flatMap.
stream-flatMap

歸約: reduce

歸約這個說法不太好理解,查看詞典reduce還有個解釋是"概括爲".wiki上對於歸約的解釋:

所謂的歸約是將某個計算問題轉換爲另外一個問題的過程。

也就是說reduce是描述如何從一個計算問題轉換爲另外一個問題.大概是這麼理解的吧.嗯,應該就是這樣理解的(有木有大佬幫忙解釋一下...〒︿〒).仍是舉幾個栗子吧.

/*
計算一個數值集合的總和
*/
Integer sum = Arrays.asList(4, 5, 3, 9).stream().reduce(0, (a, b) -> a + b);
// 計算問題: 計算List<Integer>的總和
// 過程: reduce, 轉換爲另外一個問題"能夠設置一個初值爲0, 而後每次累加, 也就是(a, b) -> a + b"
// (可能理解有誤)

/*
求一個數值集合的最大值
*/
Integer max = Arrays.asList(1, 2, 3, 4, 5).stream().reduce(Integer::max).orElse(0);

至於reduce方法具體是如何實現的,一步一步累加的過程,能夠看下圖:stream-reduce.png

reduce有三個重載方法,可根據須要使用對應的方法.

T reduce(T identity, BinaryOperator<T> accumulator);
    Optional<T> reduce(BinaryOperator<T> accumulator);
    <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> cobiner);

使用上面三個方法的時候要注意函數式接口的類型,以及泛型,下面我舉個例子,計算菜單中全部的菜餚的熱量總和,聲明一點,下面的寫法是很是很差的寫法(shit code),只是單純用來比較reduce三個重載方法的用法,以及寫的時候要注意函數式接口的類型

Dish sum1 = dishes.stream().reduce(new Dish(), (a, b) -> new Dish().setCalories(a.getCalories() + b.getCalories()));
System.out.println(sum1.getCalories());

Dish sum2 = dishes.stream().reduce((a, b) -> new Dish().setCalories(a.getCalories() + b.getCalories())).get();
System.out.println(sum2.getCalories());

// 第三個參數暫時還不知道什麼用處
Integer sum3 = dishes.stream().reduce(0, (c, d) -> c + d.getCalories(), (a, b) -> a - b);
System.out.println(sum3);

固然上面那個求和也能夠這麼寫: Arrays.asList(4, 5, 3, 9).stream().mapToInt(e -> e).sum(),其實sum的實現也是調的reduce方法.這裏的mapToInt是轉化成一個IntStream(數值流).

Numerical Stream

舉個例子:

int calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);

這段代碼的的問題是,它有一個暗含裝箱的成本(爲何是裝箱,而不是拆箱的成本?).

Java8引入了三個原始類型特化流接口來解決這個問題: IntStream,LongStream,DoubleStream,分別將流中的元素特化爲int,long,double,從而避免了暗含的裝箱成本.映射到數值流,可使用mapToInt,mapToLong,mapToDouble,而轉換爲對象流直接調用boxed()方法便可.

Java8爲數值流提供了不少的方法,好比sum,min,max,count,average等等.如今我們就可使用數值流來計算菜單中全部菜餚的熱量總和:

int calories = menu.stream().mapToInt(Dish::getCalories).sum();

Summary

  • 在Stream中使用了大量的函數式接口,結合上一篇博客來講的話,流是函數式接口的最佳實踐,那麼使用流是Lambda表達式的最佳實踐
  • Stream的專一點是處理數據,只能被消費一次(終端操做),而集合的重點是在於存取
  • Stream的寫法簡短易讀,一目瞭然
  • Stream的經常使用操做,除了flatMap和reduce不太好理解,其餘的都是顧名思義
  • 數值流,減小了裝箱的成本
相關文章
相關標籤/搜索