Java 8系列之Stream的基本語法詳解
Stream系列:
概述
繼Java 8系列之Lambda表示式之後,我們來了解Stream。Stream 是用函數語言程式設計方式在集合類上進行復雜操作的工具,其集成了Java 8中的眾多新特性之一的聚合操作,開發者可以更容易地使用Lambda表示式,並且更方便地實現對集合的查詢、遍歷、過濾以及常見計算等。
聚合操作
為了學習聚合的使用,在這裡,先定義一個數據類:
public class Student { int no; String name; String sex; float height; public Student(int no, String name, String sex, float height) { this.no = no; this.name = name; this.sex = sex; this.height = height; } **** } Student stuA = new Student(1, "A", "M", 184); Student stuB = new Student(2, "B", "G", 163); Student stuC = new Student(3, "C", "M", 175); Student stuD = new Student(4, "D", "G", 158); Student stuE = new Student(5, "E", "M", 170); List<Student> list = new ArrayList<>(); list.add(stuA); list.add(stuB); list.add(stuC); list.add(stuD); list.add(stuE);
現有一個List list裡面有5個Studeng物件,假如我們想獲取Sex=“G”的Student,並打印出來。如果按照我們原來的處理模式,必然會想到一個for迴圈就搞定了,而在for迴圈其實是一個封裝了迭代的語法塊。在這裡,我們採用Iterator進行迭代:
Iterator<Student> iterator = list.iterator(); while(iterator.hasNext()) { Student stu = iterator.next(); if (stu.getSex().equals("G")) { System.out.println(stu.toString()); } }
整個迭代過程是這樣的:首先呼叫iterator方法,產生一個新的Iterator物件,進而控制整
個迭代過程,這就是外部迭代 迭代過程通過顯式呼叫Iterator物件的hasNext和next方法完成迭代
而在Java 8中,我們可以採用聚合操作:
list.stream()
.filter(student -> student.getSex().equals("G"))
.forEach(student -> System.out.println(student.toString()));
首先,通過stream方法建立Stream,然後再通過filter方法對源資料進行過濾,最後通過foeEach方法進行迭代。在聚合操作中,與Labda表示式一起使用,顯得程式碼更加的簡潔。這裡值得注意的是,我們首先是stream方法的呼叫,其與iterator作用一樣的作用一樣,該方法不是返回一個控制迭代的 Iterator 物件,而是返回內部迭代中的相應介面: Stream,其一系列的操作都是在操作Stream,直到feach時才會操作結果,這種迭代方式稱為內部迭代。
外部迭代和內部迭代(聚合操作)都是對集合的迭代,但是在機制上還是有一定的差異:
- 迭代器提供next()、hasNext()等方法,開發者可以自行控制對元素的處理,以及處理方式,但是隻能順序處理;
- stream()方法返回的資料集無next()等方法,開發者無法控制對元素的迭代,迭代方式是系統內部實現的,同時系統內的迭代也不一定是順序的,還可以並行,如parallelStream()方法。並行的方式在一些情況下,可以大幅提升處理的效率。
Stream
如何使用Stream?
聚合操作是Java 8針對集合類,使程式設計更為便利的方式,可以與Lambda表示式一起使用,達到更加簡潔的目的。
前面例子中,對聚合操作的使用可以歸結為3個部分:
- 建立Stream:通過stream()方法,取得集合物件的資料集。
- Intermediate:通過一系列中間(Intermediate)方法,對資料集進行過濾、檢索等資料集的再次處理。如上例中,使用filter()方法來對資料集進行過濾。
- Terminal通過最終(terminal)方法完成對資料集中元素的處理。如上例中,使用forEach()完成對過濾後元素的列印。
在一次聚合操作中,可以有多個Intermediate,但是有且只有一個Terminal。也就是說,在對一個Stream可以進行多次轉換操作,並不是每次都對Stream的每個元素執行轉換。並不像for迴圈中,迴圈N次,其時間複雜度就是N。轉換操作是lazy(惰性求值)的,只有在Terminal操作執行時,才會一次性執行。可以這麼認為,Stream 裡有個操作函式的集合,每次轉換操作就是把轉換函式放入這個集合中,在 Terminal 操作的時候迴圈 Stream 對應的集合,然後對每個元素執行所有的函式。
Stream的操作分類
剛才提到的Stream的操作有Intermediate、Terminal和Short-circuiting:
Intermediate:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 skip、 parallel、 sequential、 unordered
Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、iterator
Short-circuiting:
anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
惰性求值和及早求值方法
像filter這樣只描述Stream,最終不產生新集合的方法叫作惰性求值方法;而像count這樣最終會從Stream產生值的方法叫作及早求值方法。
long count = allArtists.stream()
.filter(artist -> {
System.out.println(artist.getName());
return artist.isFrom("London");
})
.count();
如何判斷一個操作是惰性求值還是及早求值,其實很簡單,只需要看其返回值即可:如果返回值是Stream,那麼就是惰性求值;如果返回值不是Stream或者是void,那麼就是及早求值。上面的示例中,只是包含兩步:一個惰性求值-filter和一個及早求值-count。
前面,已經說過,在一個Stream操作中,可以有多次惰性求值,但有且僅有一次及早求值。
建立Stream
我們有多種方式生成Stream:
Stream介面的靜態工廠方法(注意:Java8裡介面可以帶靜態方法);
Collection介面和陣列的預設方法(預設方法,也使Java的新特性之一,後續介紹),把一個Collection物件轉換成Stream
其他
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
靜態工廠方法
of
of方法,其生成的Stream是有限長度的,Stream的長度為其內的元素個數。
- of(T... values):返回含有多個T元素的Stream
- of(T t):返回含有一個T元素的Stream
示例:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<String> stringStream = Stream.of("A");
generator
generator方法,返回一個無限長度的Stream,其元素由Supplier介面的提供。在Supplier是一個函式介面,只封裝了一個get()方法,其用來返回任何泛型的值,該結果在不同的時間內,返回的可能相同也可能不相同,沒有特殊的要求。
- generate(Supplier<T> s):返回一個無限長度的Stream
- 這種情形通常用於隨機數、常量的 Stream,或者需要前後元素間維持著某種狀態資訊的 Stream。
- 把 Supplier 例項傳遞給 Stream.generate() 生成的 Stream,預設是序列(相對 parallel 而言)但無序的(相對 ordered 而言)。
示例:
Stream<Double> generateA = Stream.generate(new Supplier<Double>() {
@Override
public Double get() {
return java.lang.Math.random();
}
});
Stream<Double> generateB = Stream.generate(()-> java.lang.Math.random());
Stream<Double> generateC = Stream.generate(java.lang.Math::random);
以上三種形式達到的效果是一樣的,只不過是下面的兩個採用了Lambda表示式,簡化了程式碼,其實際效果就是返回一個隨機值。一般無限長度的Stream會與filter、limit等配合使用,否則Stream會無限制的執行下去,後果可想而知,如果你有興趣,不妨試一下。
iterate
iterate方法,其返回的也是一個無限長度的Stream,與generate方法不同的是,其是通過函式f迭代對給指定的元素種子而產生無限連續有序Stream,其中包含的元素可以認為是:seed,f(seed),f(f(seed))無限迴圈。
- iterate(T seed, UnaryOperator<T> f)
示例:
Stream.iterate(1, item -> item + 1)
.limit(10)
.forEach(System.out::println);
// 列印結果:1,2,3,4,5,6,7,8,9,10
上面示例,種子為1,也可認為該Stream的第一個元素,通過f函式來產生第二個元素。接著,第二個元素,作為產生第三個元素的種子,從而產生了第三個元素,以此類推下去。需要主要的是,該Stream也是無限長度的,應該使用filter、limit等來擷取Stream,否則會一直迴圈下去。
empty
empty方法返回一個空的順序Stream,該Stream裡面不包含元素項。
Collection介面和陣列的預設方法
在Collection介面中,定義了一個預設方法stream(),用來生成一個Stream。
public interface Collection<E> extends Iterable<E> {
***
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
***
}
在Arrays類,封裝了一些列的Stream方法,不僅針對於任何型別的元素採用了泛型,更對於基本型別作了相應的封裝,以便提升Stream的處理效率。
public class Arrays {
***
public static <T> Stream<T> stream(T[] array) {
return stream(array, 0, array.length);
}
public static LongStream stream(long[] array) {
return stream(array, 0, array.length);
}
***
}
示例:
int ids[] = new int[]{1, 2, 3, 4};
Arrays.stream(ids)
.forEach(System.out::println);
其他
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
Intermediate
Intermediate主要是用來對Stream做出相應轉換及限制流,實際上是將源Stream轉換為一個新的Stream,以達到需求效果。
concat
concat方法將兩個Stream連線在一起,合成一個Stream。若兩個輸入的Stream都時排序的,則新Stream也是排序的;若輸入的Stream中任何一個是並行的,則新的Stream也是並行的;若關閉新的Stream時,原兩個輸入的Stream都將執行關閉處理。
示例:
Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5))
.forEach(integer -> System.out.print(integer + " "));
// 列印結果
// 1 2 3 4 5
distinct
distinct方法以達到去除掉原Stream中重複的元素,生成的新Stream中沒有沒有重複的元素。
Stream.of(1,2,3,1,2,3)
.distinct()
.forEach(System.out::println); // 列印結果:1,2,3
建立了一個Stream(命名為A),其含有重複的1,2,3等六個元素,而實際上列印結果只有“1,2,3”等3個元素。因為A經過distinct去掉了重複的元素,生成了新的Stream(命名為B),而B
中只有“1,2,3”這三個元素,所以也就呈現了剛才所說的列印結果。
filter
filter方法對原Stream按照指定條件過濾,在新建的Stream中,只包含滿足條件的元素,將不滿足條件的元素過濾掉。
示例:
Stream.of(1, 2, 3, 4, 5)
.filter(item -> item > 3)
.forEach(System.out::println);// 列印結果:4,5
建立了一個含有1,2,3,4,5等5個整型元素的Stream,filter中設定的過濾條件為元素值大於3,否則將其過濾。而實際的結果為4,5。
filter傳入的Lambda表示式必須是Predicate例項,引數可以為任意型別,而其返回值必須是boolean型別。
map
map方法將對於Stream中包含的元素使用給定的轉換函式進行轉換操作,新生成的Stream只包含轉換生成的元素。為了提高處理效率,官方已封裝好了,三種變形:mapToDouble,mapToInt,mapToLong。其實很好理解,如果想將原Stream中的資料型別,轉換為double,int或者是long是可以呼叫相對應的方法。
示例:
Stream.of("a", "b", "hello")
.map(item-> item.toUpperCase())
.forEach(System.out::println);
// 列印結果
// A, B, HELLO
傳給map中Lambda表示式,接受了String型別的引數,返回值也是String型別,在轉換行數中,將字母全部改為大寫
map傳入的Lambda表示式必須是Function例項,引數可以為任意型別,而其返回值也是任性型別,javac會根據實際情景自行推斷。
flatMap
flatMap方法與map方法類似,都是將原Stream中的每一個元素通過轉換函式轉換,不同的是,該換轉函式的物件是一個Stream,也不會再建立一個新的Stream,而是將原Stream的元素取代為轉換的Stream。如果轉換函式生產的Stream為null,應由空Stream取代。flatMap有三個對於原始型別的變種方法,分別是:flatMapToInt,flatMapToLong和flatMapToDouble。
示例:
Stream.of(1, 2, 3)
.flatMap(integer -> Stream.of(integer * 10))
.forEach(System.out::println);
// 列印結果
// 10,20,30
傳給flatMap中的表示式接受了一個Integer型別的引數,通過轉換函式,將原元素乘以10後,生成一個只有該元素的流,該流取代原流中的元素。
flatMap傳入的Lambda表示式必須是Function例項,引數可以為任意型別,而其返回值型別必須是一個Stream。
peek
peek方法生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費函式(Consumer例項),新Stream每個元素被消費的時候都會執行給定的消費函式,並且消費函式優先執行
示例:
Stream.of(1, 2, 3, 4, 5)
.peek(integer -> System.out.println("accept:" + integer))
.forEach(System.out::println);
// 列印結果
// accept:1
// 1
// accept:2
// 2
// accept:3
// 3
// accept:4
// 4
// accept:5
// 5
skip
skip方法將過濾掉原Stream中的前N個元素,返回剩下的元素所組成的新Stream。如果原Stream的元素個數大於N,將返回原Stream的後(原Stream長度-N)個元素所組成的新Stream;如果原Stream的元素個數小於或等於N,將返回一個空Stream。
示例:
Stream.of(1, 2, 3,4,5)
.skip(2)
.forEach(System.out::println);
// 列印結果
// 3,4,5
sorted
sorted方法將對原Stream進行排序,返回一個有序列的新Stream。sorterd有兩種變體sorted(),sorted(Comparator),前者將預設使用Object.equals(Object)進行排序,而後者接受一個自定義排序規則函式(Comparator),可按照意願排序。
示例:
Stream.of(5, 4, 3, 2, 1)
.sorted()
.forEach(System.out::println);
// 列印結果
// 1,2,3,4,5
Stream.of(1, 2, 3, 4, 5)
.sorted()
.forEach(System.out::println);
// 列印結果
// 5, 4, 3, 2, 1
Terminal
collect
count
count方法將返回Stream中元素的個數。
示例:
long count = Stream.of(1, 2, 3, 4, 5)
.count();
System.out.println("count:" + count);// 列印結果:count:5
forEach
forEach方法前面已經用了好多次,其用於遍歷Stream中的所元素,避免了使用for迴圈,讓程式碼更簡潔,邏輯更清晰。
示例:
Stream.of(5, 4, 3, 2, 1)
.sorted()
.forEach(System.out::println);
// 列印結果
// 1,2,3,4,5
forEachOrdered
forEachOrdered方法與forEach類似,都是遍歷Stream中的所有元素,不同的是,如果該Stream預先設定了順序,會按照預先設定的順序執行(Stream是無序的),預設為元素插入的順序。
示例:
Stream.of(5,2,1,4,3)
.forEachOrdered(integer -> {
System.out.println("integer:"+integer);
});
// 列印結果
// integer:5
// integer:2
// integer:1
// integer:4
// integer:3
max
max方法根據指定的Comparator,返回一個Optional,該Optional中的value值就是Stream中最大的元素。至於Optional是啥,後續再做介紹吧。
原Stream根據比較器Comparator,進行排序(升序或者是降序),所謂的最大值就是從新進行排序的,max就是取重新排序後的最後一個值,而min取排序後的第一個值。
示例:
Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
.max((o1, o2) -> o2 - o1);
System.out.println("max:" + max.get());// 列印結果:max:1
對於原Stream指定了Comparator,實際上是找出該Stream中的最小值,不過,在max方法中找最小值,更能體現出來Comparator的作用吧。max的值不言而喻,就是1了。
min
min方法根據指定的Comparator,返回一個Optional,該Optional中的value值就是Stream中最小的元素。至於Optional是啥,後續再做介紹吧。
示例:
Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
.max((o1, o2) -> o1 - o2);
System.out.println("max:" + max.get());// 列印結果:min:5
剛才在max方法中,我們找的是Stream中的最小值,在min中我們找的是Stream中的最大值,不管是最大值還是最小值起決定作用的是Comparator,它決定了元素比較大小的原則。
reduce
Short-circuiting
allMatch
allMatch操作用於判斷Stream中的元素是否全部滿足指定條件。如果全部滿足條件返回true,否則返回false。
示例:
boolean allMatch = Stream.of(1, 2, 3, 4)
.allMatch(integer -> integer > 0);
System.out.println("allMatch: " + allMatch); // 列印結果:allMatch: true
anyMatch
anyMatch操作用於判斷Stream中的是否有滿足指定條件的元素。如果最少有一個滿足條件返回true,否則返回false。
示例:
boolean anyMatch = Stream.of(1, 2, 3, 4)
.anyMatch(integer -> integer > 3);
System.out.println("anyMatch: " + anyMatch); // 列印結果:anyMatch: true
findAny
findAny操作用於獲取含有Stream中的某個元素的Optional,如果Stream為空,則返回一個空的Optional。由於此操作的行動是不確定的,其會自由的選擇Stream中的任何元素。在並行操作中,在同一個Stram中多次呼叫,可能會不同的結果。在序列呼叫時,Debug了幾次,發現每次都是獲取的第一個元素,個人感覺在序列呼叫時,應該預設的是獲取第一個元素。
示例:
Optional<Integer> any = Stream.of(1, 2, 3, 4).findAny();
findFirst
findFirst操作用於獲取含有Stream中的第一個元素的Optional,如果Stream為空,則返回一個空的Optional。若Stream並未排序,可能返回含有Stream中任意元素的Optional。
示例:
Optional<Integer> any = Stream.of(1, 2, 3, 4).findFirst();
limit
limit方法將擷取原Stream,擷取後Stream的最大長度不能超過指定值N。如果原Stream的元素個數大於N,將擷取原Stream的前N個元素;如果原Stream的元素個數小於或等於N,將擷取原Stream中的所有元素。
示例:
Stream.of(1, 2, 3,4,5)
.limit(2)
.forEach(System.out::println);
// 列印結果
// 1,2
傳入limit的值為2,也就是說被擷取後的Stream的最大長度為2,又由於原Stream中有5個元素,所以將擷取原Stream中的前2個元素,生成一個新的Stream。
noneMatch
noneMatch方法將判斷Stream中的所有元素是否滿足指定的條件,如果所有元素都不滿足條件,返回true;否則,返回false.
示例:
boolean noneMatch = Stream.of(1, 2, 3, 4, 5)
.noneMatch(integer -> integer > 10);
System.out.println("noneMatch:" + noneMatch); // 列印結果 noneMatch:true
boolean noneMatch_ = Stream.of(1, 2, 3, 4, 5)
.noneMatch(integer -> integer < 3);
System.out.println("noneMatch_:" + noneMatch_); // 列印結果 noneMatch_:false