Java8 Lambda表示式與Stream API (二): Stream API的使用

weixin_33936401發表於2017-05-09

你要知道的Java8 匿名內部類、函式式介面、lambda表示式與Stream API都在這裡

轉載請註明出處 http://www.jianshu.com/p/3bdd8853016d

本文主要講解Java8 Stream API,但是要講解這一部分需要匿名內部類、lambda表示式以及函式式介面的相關知識,本文將分為兩篇文章來講解上述內容,讀者可以按需查閱。

本文是本系列文章的第二篇,主要講解Stream API,在學習Stream API之前要求讀者有一定的lambda表示式基礎,如果相關知識不瞭解可以參考本系列文章的第一篇Java 匿名內部類、lambda表示式與函式式介面

Stream API

Java8新增的stream功能非常強大,這裡的streamJava IO中的stream是完全不同概念的兩個東西。本文要講解的stream是能夠對集合物件進行各種序列或併發聚集操作,Stream API依賴於前一篇文講解的lambda表示式,只有當兩者結合時才能極大的提高程式設計效率並且程式碼更易理解和維護。Stream API支援序列和併發的集合操作,這也是響應了現在多核處理器的需求,Stream API的併發採用的是我們熟悉的fork/join模式,手動編寫並行程式碼很複雜也很容易出錯,但是採用Stream API來進行集合物件上的併發操作你不需要編寫任何多執行緒程式碼就能夠輕而易舉的實現併發操作,從而提高程式碼的執行效率,也極大的簡化了程式設計難度。

聚集操作

在實際開發中,我們經常對一個集合內的物件進行一系列的操作,比如排序、查詢、過濾、重組、資料統計等操作,通常情況下我們可能會採用for迴圈遍歷的方式來逐一進行操作,這樣的程式碼即複雜又難以維護,如果對效能有要求再進行多執行緒程式碼的編寫就更加的複雜了,同時也更容易出錯。

下面舉一個栗子:

class User
{
    private String userID;
    private boolean isVip;
    private int balance;
    
    public User(String userID, boolean isVip, int balance)
    {
        this.userID = userID;
        this.isVip = isVip;
        this.balance = balance;
    }
    
    public boolean isVip()
    {
        return this.isVip;
    }
    
    public String getUserID()
    {
        return this.userID;
    }
    
    public int getBalance()
    {
        return this.balance;
    }
}

public class HelloWorld
{   
    public static void main(String[] args)
    {
        ArrayList<User> users = new ArrayList<>();
        users.add(new User("2017001", false, 0));
        users.add(new User("2017002", true, 36));
        users.add(new User("2017003", false, 98));
        users.add(new User("2017004", false, 233));
        users.add(new User("2017005", true, 68));
        users.add(new User("2017006", true, 599));
        users.add(new User("2017007", true, 1023));
        users.add(new User("2017008", false, 9));
        users.add(new User("2017009", false, 66));
        users.add(new User("2017010", false, 88));
        
        //普通實現方式
        ArrayList<User> tempArray = new ArrayList<>();
        ArrayList<String> idArray = new ArrayList<>(3);
        for (User user: users)
        {
            if (user.isVip())
            {
                tempArray.add(user);
            }
        }
        tempArray.sort(new Comparator<User>(){
            public int compare(User o1, User o2) {
                return o2.getBalance() - o1.getBalance();
            }
        });
        for (int i = 0; i < 3; i++)
        {
            idArray.add(tempArray.get(i).getUserID());
        }
        for (int i = 0; i < idArray.size(); i++)
        {
            System.out.println(idArray.get(i));
        }

        //Stream API實現方式
        //也可以使用parallelStream方法獲取一個併發的stream,提高計算效率
        Stream<User> stream = users.stream();
        List<String> array = stream.filter(User::isVip).sorted((t1, t2) -> t2.getBalance() - t1.getBalance()).limit(3).map(User::getUserID).collect(Collectors.toList());
        array.forEach(System.out::println);
    }
}

上述程式碼首先定義了一個使用者類,這個類儲存使用者是否是VIP、使用者ID以及使用者的餘額,假如現在有一個需求,將VIP中餘額最高的三個使用者的ID找出來,傳統的思路一般就是建立一個臨時的list,然後逐一判斷,將所有的VIP使用者加入到這個臨時的list中,然後呼叫集合類的sort方法根據餘額排序,最後再遍歷三次獲取餘額最高的三個使用者的ID等資訊。這樣的方法看似簡單,但程式碼寫出來即混亂也不好看,如果使用者量非常大,有幾千萬甚至幾個億,這樣遍歷的方式效率就會特別低,如果手工加上多執行緒的併發操作,程式碼就更加複雜了。

上述程式碼的第二部分使用Stream API的方式來計算,首先通過集合類獲取了一個普通的stream,如果資料量大可以使用parallelStream方法獲取一個併發的stream,這樣接下來的計算程式設計師不需要編寫任何多執行緒程式碼系統會自動進行多執行緒計算。獲取了stream以後首先呼叫filter方法找到是否為VIP使用者然後對VIP使用者進行排序操作,接下來限制只獲取三個使用者的資訊,然後將使用者對映為使用者ID,最後將該stream轉換為集合類,兩種實現方式的結果完全一樣,但是明顯的採用Stream API的程式碼更加簡潔易懂。

Stream API的編寫大量依賴lambda表示式以及lambda表示式引用方法引用構造器,如果您對這一塊不理解可以查閱文章Java 匿名內部類、lambda表示式與函式式介面

如何使用Stream

A sequence of elements supporting sequential and parallel aggregate operations

上面是Java文件中定義的Stream,可以看出,Stream就是元素的集合,並且可以採用序列或並行的方式進行聚集操作。在使用時我們可以將Stream理解為一個迭代器,只不過這個迭代器更加高階,能夠對其中的每一個元素進行我們規定的計算。

當我們要使用Stream API時,首先需要建立一個Stream物件,可以通過集合類的例項方法streamparallelStream來獲取一個普通的序列stream或是並行stream。也可以使用StreamIntStreamLongStreamDoubleStream建立一個Stream物件,Stream是一個比較通用的流,可以代表任何引用資料型別,其他的則是指特定型別的流。最常用的就是通過一個集合型別來獲取相應型別的Stream

流的操作分為中間操作 Intermediate結束操作 Terminal

  • 中間操作(Intermediate):一個流可以採用鏈式呼叫的方式進行數箇中間操作,主要目的就是開啟流然後對這個流進行各種過濾、對映、聚集、統計操作等,如上述程式碼中的filtermap操作等。每一個操作結束後都會返回一個新的流,並且這些操作都是lazy的,也就是在進行結束操作時才會真正的進行計算,一次遍歷就計算出所有結果。
  • 結束操作(Terminal):一個流只能執行一個結束操作,當執行了結束操作以後這個流就不能再被執行,也就是說不能再次進行中間操作或結束操作,所以結束操作一定是流的最後一個操作,如上述程式碼中的collect方法。當開始執行結束操作的時候才會對流進行遍歷並且只一次遍歷就計算出所有結果。

Stream的建立

  • 通過集合類建立

通過集合建立Stream的方法是我們最常用的,集合類的例項方法streamparallelStream可以獲取相應的流。

ArrayList<User> users = new ArrayList<>();
users.add(new User("2017001", false, 0));
users.add(new User("2017002", true, 36));
users.add(new User("2017003", false, 98));
Stream<User> stream = users.stream();
  • 通過陣列構造
String[] str = {"Hello World", "Jiaming Chen", "Zhouhang Cheng"};
Stream<String> stream = Stream.of(str);
  • 通過單個元素構造
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
  • Stream與Array和Collection的轉換

一般我們都會對Stream進行結束操作,用於獲取一個陣列或是集合類,通過陣列和集合類建立Stream前文已經介紹了,這裡介紹通過Stream獲取陣列或集合類。

String[] str = {"Hello World", "Jiaming Chen", "Zhouhang Cheng"};
Stream<String> stream = Stream.of(str);
    
String[] strArray = stream.toArray(String[]::new);
List<String> strList = stream.collect(Collectors.toList());
ArrayList<String> strArrayList = stream.collect(Collectors.toCollection(ArrayList::new));
Set<String> strSet = stream.collect(Collectors.toSet());

上面的程式碼分別將流轉換為陣列、List、ArrayList和Set型別,具體的引數可以檢視官方API文件。

Stream 常用方法

  • filter

filter的栗子前面已經舉過了,filter函式需要傳入一個實現Predicate函式式介面的物件,該介面的抽象方法test接收一個引數並返回一個boolean值,為true則保留,false則剔除,前文舉的栗子就是判斷是否為VIP使用者,如果是就保留,不是就剔除。
原理如圖所示:

3132379-146340e03f840676.jpeg
filter
  • map、flatMap

map的栗子前面已經舉過了,map函式需要傳入一個實現Function函式式介面的物件,該介面的抽象方法apply接收一個引數並返回一個值,可以理解為對映關係,前文舉的栗子就是將每一個使用者對映為一個userID
原理如圖所示:

3132379-a9f68a6377bfaaf0.jpeg
map

map方法是一個一對一的對映,每輸入一個資料也只會輸出一個值。
flatMap方法是一對多的對映,對每一個元素對映出來的仍舊是一個Stream,然後會將這個子Stream的元素對映到父集合中,栗子如下:

Stream<List<Integer>> inputStream = Stream.of(Arrays.asList(1), Arrays.asList(2, 3), Arrays.asList(4, 5, 6));
List<Integer> integerList = inputStream.flatMap((childList) -> childList.stream()).collect(Collectors.toList());
//將一個“二維陣列”flat為“一維陣列”
integerList.forEach(System.out::println);
  • limit、skip

limit用於限制獲取多少個結果,與資料庫中的limit作用類似,skip用於排除前多少個結果。

  • sorted

sorted的栗子前面也舉過了,sorted函式需要傳入一個實現Comparator函式式介面的物件,該介面的抽象方法compare接收兩個引數並返回一個整型值,作用就是排序,與其他常見排序方法一致。

  • distinct

distinct用於剔除重複,與資料庫中的distinct用法一致。

  • findFirst

findFirst方法總是返回第一個元素,如果沒有則返回空,它的返回值型別是Optional<T>型別,接觸過swift的同學應該知道,這是一個可選型別,如果有第一個元素則Optional型別中儲存的有值,如果沒有第一個元素則該型別為空。

Stream<User> stream = users.stream();
Optional<String> userID = stream.filter(User::isVip).sorted((t1, t2) -> t2.getBalance() - t1.getBalance()).limit(3).map(User::getUserID).findFirst();
userID.ifPresent(uid -> System.out.println("Exists"));
  • min、max

min可以對整型流求最小值,返回OptionalInt
max可以對整型流求最大值,返回OptionalInt
這兩個方法是結束操作,只能呼叫一次。

  • allMatch、anyMatch、noneMatch

allMatchStream中全部元素符合傳入的predicate返回 true

anyMatchStream中只要有一個元素符合傳入的predicate返回 true

noneMatchStream中沒有一個元素符合傳入的predicate返回 true

  • reduce

reduce方法用於組合Stream元素,它可以提供一個初始值然後按照傳入的計算規則依次和Stream中的元素進行計算,因此上文介紹的minmax都可以看做是reduce的一種實現。

舉個例子:

IntStream is = IntStream.range(0, 10);
System.out.println(is.reduce(0, Integer::sum));
        
IntStream intStream = IntStream.range(0, 10);
System.out.println(intStream.reduce((o1, o2) -> o1 + o2));
        
Stream<String> stream = Stream.of("Hello", "World", "Jiaming", "Chen");
System.out.println(stream.reduce("", String::concat));

第一個IntStream呼叫的reduce方法設定了一個初始值,因此最終reduce計算的結果一定有值,該方法呼叫Integer的類方法sum用於計算Stream的總和。
第二個IntStream呼叫reduce方法時沒有設定初始值,因此最終reduce計算的結果不一定有值,所以返回值型別是Optional型別,沒有提供初始值時會自動將第一個和第二個元素先進行計算,但有可能不存在第一個或第二個元素,因此返回值是Optional型別。

Stream API的效能

這篇文章詳細測試了Stream API的效能Java Stream API效能測試
總的來說,對於複雜計算並且擁有多核CPU來說,使用Stream API進行併發計算速度最快,也推薦使用。對於計算比較簡單,手工外部迭代效能更加。單核CPU儘量不要使用併發的Stream API計算。如果沒有太高的效能要求,想要編寫出簡潔的程式碼還是推薦使用Stream API

備註

由於作者水平有限,難免出現紕漏,如有問題還請不吝賜教。

相關文章