JAVA8 之 Stream 流(四)
如果說前面幾章是函數語言程式設計的方法論,那麼 Stream 流就應該是 JAVA8 為我們提供的最佳實踐。
Stream 流的定義
Stream 是支援序列和並行操作的一系列元素。流操作會被組合到流管道中(Pipeline)中,一個流管道必須包含一個源(Source),這個源可以是一個數組(Array),集合(Collection)或者 I/O Channel,會有一個或者多箇中間操作,中間操作的意思就是流與流的操作,流還會包含一箇中止操作,這個中止操作會生成一個結果。
Stream 流的作用
以函數語言程式設計的方式更好的操作集合。完全依賴於函式式介面。在 java.util.stream 包中。
流的建立方式
使用陣列的方式
//第一種方式,使用 Stream.of 方法 Stream stream1 = Stream.of("hello","world","hello world"); String[] myArray = new String[]{"hello","world","hello world"}; Stream stream2 = Stream.of(myArray); //第二種方式,使用 Arrays.stream() Stream stream3 = Arrays.stream(myArray);
使用集合的方式
Stream 的作用是以函數語言程式設計的方式操作集合,所以對於集合類,一定有更好更方便的方法去建立 Stream 流。
List<String> list = Arrays.asList(myArray); Stream stream4 = list.stream();
對於集合類,直接呼叫 stream 方法就可以獲得這個集合對應的 Stream 流。通過檢視原始碼我們發現這個方法直接定義在 Collection 介面中,並且是一個預設方法。所以所有 Collection 的子類都可以直接呼叫這個方法。這也是最為常用的方法。
default Stream<E> stream() { return StreamSupport.stream(spliterator(), false); }
使用檔案流(基本不會使用,簡單瞭解即可)
下面是一個直接讀取檔案中的內容並且轉化為 Stream 流,最後輸出的過程。
//檔案流 private static Stream<String> fileStream(){ Path path = Paths.get("C:\\Users\\abs\\a.txt"); try(Stream<String> lines = Files.lines(path)){ lines.forEach(System.out::println); return lines; }catch(IOException e){ throw new RuntimeException(e); } }
其他方式
最後的方式是用於建立無限流,無限流的意思是如果你不加任何限制,流中的資料是無限。用於建立無限流的方法有 iterator 和 generate。
generate 方法需要傳入一個 Supplier 型別的函式式介面,這個函式式介面用於產生無限流中所需要的資料。
//全是數字 1 的無限流 Stream.generate(()->1); //隨機數字的無限流 Stream.generate(Math::random);
iterator 方法需要傳入兩個引數,第一個給定一個初始值,第二個引數是一個函式式介面 UnaryOperator,這個函式式介面就是輸入和輸出相同的 Function 介面。
Stream.iterate(0,n->n+1).limit(10).forEach(System.out::println);
輸出結果:
0 1 2 3 4 5 6 7 8 9
上面的 limit 是避免無限流一直產生,到達指定個數就停止。
Steam 流的優勢
下面我們通過一個簡單的例子來了解一下使用 Stream 流到底有哪些好處。等我們學完 Stream API 後會給大家提供更多的例子,讓大家真正瞭解它。
比如我們給定一個 List 集合,裡面放了很多數字,我們想要得到數字的平方然後求和。
以前的寫法:
List<Integer> l = Arrays.asList(1,2,3,4,5,6,7);
int res = 0;
for(int i=0;i<l.size();i++){
res += i*i;
}
使用 Stream 後的寫法只需要一行程式碼:
int r = l.stream().map(i->i+i).reduce(0,Integer::sum);
大家現在可能不明白 map 或者 reduce 的作用,我們稍後會詳細講解這一部分,這裡只是想讓大家看看區別,以及認識到 Stream 對於函數語言程式設計的使用和好處。
Stream 流的特性和原理
流不儲存值,通過管道的方式獲取值。對流的操作會生成一個結果,不過並不會修改底層的資料來源。集合可以作為流的底層資料來源,也可以通過 generate/iterator 方法來生成資料來源。
得到流之後,我們可以對流中的資料進行很多操作,比如過濾,對映,排序等等,處理完之後的資料可以再次被收集起來轉化為我們需要的資料型別。
從上面的圖我們可以看出,一個完成的流操作過程是包含兩種型別的,一個是中間操作,一個是終止操作。中間操作指的是過濾,排序和對映等中間處理過程的方法,終止操作指的是我們將流處理完畢後返回結果的操作,比如 collect,reduce 和 count 等等。
中間操作:一個流後面可以跟隨零個或者多箇中間操作。其目的只要是開啟流,做出某種程度的資料對映/過濾,然後返回一個新的流,交給下一個操作使用,這些操作都是延遲的,就是說僅僅呼叫到這些類的方法,並沒有真正開始流的遍歷。
終止操作:一個流只能有一個終止操作,當這個操作執行後,流就被使用光了,無法再被操作。所以這必定是流的最後一個操作。終止操作的執行,才會是真正開始流的遍歷,並且會生成一個結果。
還有一個重要的概念流是惰性的
,在資料來源上的計算只有資料在被終止的時候才會被執行。也就是說所有的中間操作都是惰性求值,不遇到終止操作,中間操作的程式碼是不會執行的。
舉個例子:
我們對於一個數字結合進行一個 map 中間操作,將元素乘以 2,同時我們有一個 System.out 語句用於檢視程式碼是否執行了。
List<Integer> l = Arrays.asList(1,2,3,4,5,6,7);
l.stream().map(i->{
i = i*2;
System.out.println(i);
return i;
});
最終的執行結果是什麼也沒列印。
那如果我們給他加一個終止操作那,結果如下:
中間操作:2
終止操作:2
中間操作:4
終止操作:4
中間操作:6
終止操作:6
中間操作:8
終止操作:8
中間操作:10
終止操作:10
中間操作:12
終止操作:12
中間操作:14
終止操作:14
這時中間操作和終止操作都執行了,這證明中間操作是惰性的。
還有一個需要注意的點就是,Stream 其實與 IO Stream 的概念是一致的,是不能重複使用的,關閉(執行終止操作後就關閉了)後也是不能使用的。
//用集合生成一個流並進行過濾,過濾後返回一個 stream s1。
List<Integer> l = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
Stream s1 = l.stream().filter(item -> item > 2);
//因為 filter 是中間操作,流並沒有被關閉,所以還可以執行其他操作,distnict 是一個終止操作,執行完畢後流就關閉了
s1.distinct();
//流已經關閉了,再執行操作就會丟擲異常
s1.forEach(System.out::println);
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
at com.paul.framework.chapter7.StreamCreate.main(StreamCreate.java:48)
Stream 流的 API 採用了建造者設計模式,這就意味著我們可以在一句程式碼中連續呼叫 Stream 的 API。
中間操作 API
filter
顧名思義,filter 就是過濾的意思。引數需要我們傳入一個 Predicate 型別的函式式介面。不符合 Predicate 函式式介面的條件的流將被過濾出去。
Stream<T> filter(Predicate<? super T> predicate);
我們需要篩選出分數大於 60 分的學生:
public static void main(String[] args) { List<Student> lists = new ArrayList<>(); lists.add(new Student("wang",80,"Female")); lists.add(new Student("li",95,"Male")); lists.add(new Student("zhao",100,"FeMale")); lists.add(new Student("qian",54,"Male")); // filter 是一箇中間操作,返回過濾後的 Stream 流。forEach 是一個終止操作,對 filter 過濾之後的流進行處理。 lists.stream().filter(s->s.getMark()>60).forEach((s)-> System.out.println(s.getName())); } //上一個例子我們對過濾後的流進行了列印操作,我們其實也可以把過濾後的流整理成一個集合 List<Student> l = lists.stream().filter(s->s.getMark()>60).collect(Collectors.toList()); l.forEach(s-> System.out.println(s.getName()));
兩次列印的結果是相同的:
//第一次列印的結果 wang li zhao //第二次列印的結果 wang li zhao
filter 函式為我們提供了最為簡單的方法去過濾集合,避免了重複程式碼,邏輯也更易懂。
map
通過流的方式對集合中的元素進行匹配操作。引數需要我們傳入一個 Function 型別的函式式介面。Function 型別的函式式介面需要一個輸入和一個輸出,對應對映之前需要的元素和對映之後得到的元素。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
比如集合中的元素是學生類,我們最終想要得到的結果是學生的分數,就可以使用 map 方法。
List<Student> lists = new ArrayList<>(); lists.add(new Student("wang",80,"Female")); lists.add(new Student("li",95,"Male")); lists.add(new Student("zhao",100,"FeMale")); lists.add(new Student("qian",54,"Male")); lists.stream().map(s->s.getMark()).collect(Collectors.toList()).forEach(System.out::println);
將 list 轉換為 stream 後,通過 map 方法將學生類轉換成學生成績的 int 型別,然後通過 collect 方法將流轉換為集合,最後通過 forEach 方法將學生成績打印出來。
列印的結果:
80 95 100 54
比如我們想將集合中的字串轉換成大寫字母。
List<String> list = Arrays.asList("hello","world","helloworld","test"); list.stream().map(String::toUpperCase).collect(Collectors.toList()).forEach(System.out::println);
map 方法裡我們通過方法引用(將字串轉為大寫的方法 String 類已經定義好了,所以我們直接使用方法引用,而不是寫一個匿名函式的 Lambda 表示式)將集合中的字串轉換成大寫,然後通過 collect 將流轉換為集合,最終使用 forEach 方法列印轉換後的字串。
列印結果:
HELLO WORLD HELLOWORLD TEST
mapTo*
如果我們的 map 方法返回值是 int,long 或者 double 的話,我們可以直接使用 Stream API 為我們提供了 mapToInt,mapToLong,mapToDouble 方法。這幾個方法返回的是 IntStream,LongStream 和 DoubleStream。
mapToInt, mapToLong 和 mapToDouble 是為了避免自動拆裝箱帶來的效能損耗。大家應該知道,像 int,long,double 這種基本資料型別是不能使用面向物件相關操作的,為此 Java 引入了自動拆裝箱的功能,能夠在需要使用面向物件的特性時幫我們將基本資料型別 int,long 和 double 轉換為 Integer,Long 和 Double 等包裝型別。在需要使用基本資料型別時(比如計算),又可以將包裝型別 Integer,Long 和 Double 轉換為基本資料型別 int,long 和 double。
如果我們使用的不對,就會有一些自動拆裝箱的效能損耗。
如果我們需要得到基本資料型別的結果,就可以使用 mapToInt, mapToLong 和 mapToDouble,這樣的到的是基本資料型別的流,可以方便我們進行計算等等操作。
int sum = lists.stream().mapToInt(s->s.getMark()).sum(); System.out.println(sum);
flatMap
flat 的意思是扁平化,這個函式式的作用是將我們 map 之後的集合或者陣列等等元素打散成我們想要的元素。
flatMap 方法需要返回一個 Stream 資料型別。T 是輸入的集合型別元素,R 是打散之後的元素型別,是 Stream 型別。
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
來看一個例子:
比如我們的流中以前有三個 ArrayList,map 之後依然後會有三個 ArrayList,flatMap 會將三個 ArrayList 合併到一個 ArrayList 中。
Stream<List<Integer>> stream = Stream.of(Arrays.asList(1),ArrayList.asList(2,3), ArrayList.asList(4,5,6)); // 將 stream 裡面的每一個 list 再次轉化為 stream<Integer>,然後在進行 map 操作。 stream.flatMap(theList->theList.stream()).map(item->item*item).forEach(System.out::println);
這個例子中,List
代表打散之前的元素,Integer 代表我們打散之後的元素型別。 在看另外一個例子,字串去重複。
List<String> list = Arrays.asList("hello welcome","world hello","hello world hello","hello welcome"); //錯誤的寫法, 這是對 String[] 的 distinct List<String[]> result = list.stream().map(item->item.split(" ")).distinct().collect(Collectors.toList());
split 方法輸入的是字串,返回的是一個字串陣列,所以最後返回的是 String 陣列流 Stream<String[]>。
我們使用 flatMap 將 String 陣列打散成 String。
//正確的寫法,要用 flatmap 將 String[] 打散成 String List<String> result = list.stream().map(item->item.split(" ")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());
flatMapTo*
flatMapTo* 也有許多具體的實現實現,和 mapTo* 用法類似,這裡就不再贅述了。
limit
limit 方法可以對流中需要返回的元素加以限制,因為流中元素的方法執行是嚴格按照順序進行的,limit 方法就相當於取前幾個元素。
我們通過下面這個例子來了解 limit 和無限流。
IntStream.iterate(0, i->(i+1)%2).distinct().limit(6).forEach(System.out::println);
IntStream.iterate(0, i->(i+1)%2) 不斷產生 0,1,0,1,0,1..... 這樣的無限流,distinct 方法去除重複,limit 方法雖然限制流中只有 6 個元素,但是 distinct 方法先執行它會對無限流一致執行去復操作,所以方法永遠不會結束。這個 limit 在這裡也失去了作用。
執行結果雖然只顯示了 0,1。但是方法一直不會結束。
正確的寫法:
IntStream.iterate(0, i->(i+1)%2).limit(6).distinct().forEach(System.out::println);
先呼叫 limit 方法,限制流中只有 6 個元素,然後去重,結果列印 0,1。程式結束。
skip
skip 方法和 limit 方法的用法類似,可以跳過流中的前幾個元素。
IntStream.iterate(0, i->i+1).limit(10).skip(3).forEach(System.out::println);
首先通過 iterate 和 limit 產生 10 Integer 個元素的流,通過 skip 跳過前三個。最終的結果如下:
3 4 5 6 7 8 9
sort
sort 方法有兩個實現,一個是不需要傳入引數的,另一個是需要我們傳入 Comparator。
//根據自然順序排序 Stream<T> sorted(); //根據 Comparator 的規則進行排序 Stream<T> sorted(Comparator<? super T> comparator);
我們以前對集合排序時通常會使用 JDK 中 Collection 介面的 sort 方法:
List<String> names = Arrays.asList("java8","lambda","method","class"); //以前的寫法 Collections.sort(names, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o2.compareTo(o1); } });
使用 Lambda 表示式對上面的寫法進行一下改進:
Collections.sort(names,(o1,o2)-> o2.compareTo(o1));
現在我們還可以使用 Stream API 中的 sorted 方法:
names.stream().sorted().forEach(System.out::println); names.stream().sorted((o1,o2)-> o2.compareTo(o1)).forEach(System.out::println);
結果:
class java8 lambda method