註意:Stream和IO流(InputStream/OutputStream)沒有任何關係,請暫時忘記對傳統IO流的固有印象 傳統集合的多步遍歷代碼 幾乎所有的集合(如Collection介面或Map介面等)都支持直接或間接的遍歷操作。而當我們需要對集合中的元素進行操作的時候,除了必需的添加、刪除、 ...
註意:Stream和IO流(InputStream/OutputStream)沒有任何關係,請暫時忘記對傳統IO流的固有印象
傳統集合的多步遍歷代碼
幾乎所有的集合(如Collection
介面或Map
介面等)都支持直接或間接的遍歷操作。而當我們需要對集合中的元素進行操作的時候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷。例如:
public class Demo10ForEach {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三豐");
for (String name : list) {
System.out.println(name);
}
}
}
這是一段非常簡單的集合遍歷操作:對集合中的每一個字元串都進行列印輸出操作。
迴圈遍歷的弊端
Java 8的Lambda讓我們可以更加專註於做什麼(What),而不是怎麼做(How),這點此前已經結合內部類進行了對比說明。現在,我們仔細體會一下上例代碼,可以發現:
- for迴圈的語法就是“怎麼做”
- for迴圈的迴圈體才是“做什麼”
為什麼使用迴圈?因為要進行遍歷。但迴圈是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而並不是從第一個到最後一個順次處理的迴圈。前者是目的,後者是方式。
試想一下,如果希望對集合中的元素進行篩選過濾:
- 將集合A根據條件一過濾為子集B;
- 然後再根據條件二過濾為子集C。
那怎麼辦?在Java 8之前的做法可能為:
這段代碼中含有三個迴圈,每一個作用不同:
- 首先篩選所有姓張的人;
- 然後篩選名字有三個字的人;
- 最後進行對結果進行列印輸出。
public class Demo11NormalFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三豐");
List<String> zhangList = new ArrayList<>();
for (String name : list) {
if (name.startsWith("張")) {
zhangList.add(name);
}
}
List<String> shortList = new ArrayList<>();
for (String name : zhangList) {
if (name.length() == 3) {
shortList.add(name);
}
}
for (String name : shortList) {
System.out.println(name);
}
}
}
每當我們需要對集合中的元素進行操作的時候,總是需要進行迴圈、迴圈、再迴圈。這是理所當然的麽?不是。迴圈是做事情的方式,而不是目的。另一方面,使用線性迴圈就意味著只能遍歷一次。如果希望再次遍歷,只能再使用另一個迴圈從頭開始。
那,Lambda的衍生物Stream能給我們帶來怎樣更加優雅的寫法呢?
Stream的更優寫法
下麵來看一下藉助Java 8的Stream API,什麼才叫優雅:
public class Demo12StreamFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三豐");
list.stream()
.filter(s -> s.startsWith("張"))
.filter(s -> s.length() == 3)
.forEach(s -> System.out.println(s));
}
}
直接閱讀代碼的字面意思即可完美展示無關邏輯方式的語義:獲取流、過濾姓張、過濾長度為3、逐一列印。代碼中並沒有體現使用線性迴圈或是其他任何演算法進行遍歷,我們真正要做的事情內容被更好地體現在代碼中。
獲取流方式
java.util.stream.Stream<T>
是Java 8新加入的最常用的流介面。(這並不是一個函數式介面。)
獲取一個流非常簡單,有以下幾種常用的方式:
- 所有的
Collection
集合都可以通過stream
預設方法獲取流; Stream
介面的靜態方法of
可以獲取數組對應的流。
方式1 : 根據Collection獲取流
首先,java.util.Collection
介面中加入了default方法stream
用來獲取流,所以其所有實現類均可獲取流。
import java.util.*;
import java.util.stream.Stream;
/*
獲取Stream流的方式
1.Collection中 方法
Stream stream()
2.Stream介面 中靜態方法
of(T...t) 向Stream中添加多個數據
*/
public class Demo13GetStream {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// ...
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
// ...
Stream<String> stream2 = set.stream();
}
}
方式2: 根據數組獲取流
如果使用的不是集合或映射而是數組,由於數組對象不可能添加預設方法,所以Stream
介面中提供了靜態方法of
,使用很簡單:
import java.util.stream.Stream;
public class Demo14GetStream {
public static void main(String[] args) {
String[] array = { "張無忌", "張翠山", "張三豐", "張一元" };
Stream<String> stream = Stream.of(array);
}
}
備註:
of
方法的參數其實是一個可變參數,所以支持數組。
常用方法
流模型的操作很豐富,這裡介紹一些常用的API。這些方法可以被分成兩種:
- 終結方法:返回值類型不再是
Stream
介面自身類型的方法,因此不再支持類似StringBuilder
那樣的鏈式調用。本小節中,終結方法包括count
和forEach
方法。 - 非終結方法:返回值類型仍然是
Stream
介面自身類型的方法,因此支持鏈式調用。(除了終結方法外,其餘方法均為非終結方法。)
備註:本小節之外的更多方法,請自行參考API文檔。
forEach : 逐一處理
雖然方法名字叫forEach
,但是與for迴圈中的“for-each”昵稱不同,該方法並不保證元素的逐一消費動作在流中是被有序執行的。
void forEach(Consumer<? super T> action);
該方法接收一個Consumer
介面函數,會將每一個流元素交給該函數進行處理。例如:
import java.util.stream.Stream;
public class Demo15StreamForEach {
public static void main(String[] args) {
Stream<String> stream = Stream.of("大娃","二娃","三娃","四娃","五娃","六娃","七娃","爺爺","蛇精","蝎子精");
//Stream<String> stream = Stream.of("張無忌", "張三豐", "周芷若");
stream.forEach((String str)->{System.out.println(str);});
}
}
在這裡,lambda表達式(String str)->{System.out.println(str);}
就是一個Consumer函數式介面的示例。
filter:過濾
可以通過filter
方法將一個流轉換成另一個子集流。方法聲明:
Stream<T> filter(Predicate<? super T> predicate);
該介面接收一個Predicate
函數式介面參數(可以是一個Lambda)作為篩選條件。
基本使用
Stream流中的filter
方法基本使用的代碼如:
public class Demo16StreamFilter {
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若");
Stream<String> result = original.filter((String s) -> {return s.startsWith("張");});
}
}
在這裡通過Lambda表達式來指定了篩選的條件:必須姓張。
count:統計個數
正如舊集合Collection
當中的size
方法一樣,流提供count
方法來數一數其中的元素個數:
long count();
該方法返回一個long值代表元素個數(不再像舊集合那樣是int值)。基本使用:
public class Demo17StreamCount {
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若");
Stream<String> result = original.filter(s -> s.startsWith("張"));
System.out.println(result.count()); // 2
}
}
limit:取用前幾個limit
方法可以對流進行截取,只取用前n個。方法簽名:
Stream<T> limit(long maxSize):獲取Stream流對象中的前n個元素,返回一個新的Stream流對象
參數是一個long型,如果集合當前長度大於參數則進行截取;否則不進行操作。
基本使用:
import java.util.stream.Stream;
public class Demo18StreamLimit {
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若");
Stream<String> result = original.limit(2);
System.out.println(result.count()); // 2
}
}
skip:跳過前幾個
如果希望跳過前幾個元素,可以使用skip
方法獲取一個截取之後的新流:
Stream<T> skip(long n): 跳過Stream流對象中的前n個元素,返回一個新的Stream流對象
如果流的當前長度大於n,則跳過前n個;否則將會得到一個長度為0的空流。
基本使用:
import java.util.stream.Stream;
public class Demo19StreamSkip {
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若");
Stream<String> result = original.skip(2);
System.out.println(result.count()); // 1
}
}
concat:組合
如果有兩個流,希望合併成為一個流,那麼可以使用Stream
介面的靜態方法concat
:
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b): 把參數列表中的兩個Stream流對象a和b,合併成一個新的Stream流對象
備註:這是一個靜態方法,與
java.lang.String
當中的concat
方法是不同的。
該方法的基本使用代碼如:
import java.util.stream.Stream;
public class Demo20StreamConcat {
public static void main(String[] args) {
Stream<String> streamA = Stream.of("張無忌");
Stream<String> streamB = Stream.of("張翠山");
Stream<String> result = Stream.concat(streamA, streamB);
}
}
distinct:去重
如果需要去除重覆數據,可以使用 distinct方法。方法簽名:
Stream<T> distinct()
基本使用:
public class Test {
public static void main(String[] args) {
Stream.of(22, 33, 22, 11, 33)
.distinct()
.forEach(s-> System.out.println(s));
}
}
map:映射
如果需要將流中的元素映射到另一個流中,可以使用 map方法。方法簽名:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
該介面需要一個 Function函數式介面參數,可以將當前流中的T類型數據轉換為另一種R類型的流。
基本使用:
public class Test {
public static void main(String[] args) {
Stream<String> original = Stream.of("11", "22", "33");
Stream<Integer> result = original.map(Integer::parseInt);
result.forEach(s -> System.out.println(s + 10));
}
}
練習
現在有兩個ArrayList
集合存儲隊伍當中的多個成員姓名,要求使用傳統的for迴圈(或增強for迴圈)依次進行以下若幹操作步驟:
- 第一個隊伍只要名字為3個字的成員姓名;
- 第一個隊伍篩選之後只要前3個人;
- 第二個隊伍只要姓張的成員姓名;
- 第二個隊伍篩選之後不要前2個人;
- 將兩個隊伍合併為一個隊伍;
- 列印整個隊伍的姓名信息。
兩個隊伍(集合)的代碼如下:
public class Demo21ArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
one.add("迪麗熱巴");
one.add("宋遠橋");
one.add("蘇星河");
one.add("老子");
one.add("莊子");
one.add("孫子");
one.add("洪七公");
List<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("張無忌");
two.add("張三豐");
two.add("趙麗穎");
two.add("張二狗");
two.add("張天愛");
two.add("張三");
// ....
}
}
傳統方式
使用for迴圈 , 示例代碼:
public class Demo22ArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一個隊伍只要名字為3個字的成員姓名;
List<String> oneA = new ArrayList<>();
for (String name : one) {
if (name.length() == 3) {
oneA.add(name);
}
}
// 第一個隊伍篩選之後只要前3個人;
List<String> oneB = new ArrayList<>();
for (int i = 0; i < 3; i++) {
oneB.add(oneA.get(i));
}
// 第二個隊伍只要姓張的成員姓名;
List<String> twoA = new ArrayList<>();
for (String name : two) {
if (name.startsWith("張")) {
twoA.add(name);
}
}
// 第二個隊伍篩選之後不要前2個人;
List<String> twoB = new ArrayList<>();
for (int i = 2; i < twoA.size(); i++) {
twoB.add(twoA.get(i));
}
// 將兩個隊伍合併為一個隊伍;
List<String> totalNames = new ArrayList<>();
totalNames.addAll(oneB);
totalNames.addAll(twoB);
// 列印整個隊伍的姓名信息。
for (String name : totalNames) {
System.out.println(name);
}
}
}
運行結果為:
宋遠橋
蘇星河
洪七公
張二狗
張天愛
張三
Stream方式
等效的Stream流式處理代碼為:
public class Demo23StreamNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一個隊伍只要名字為3個字的成員姓名;
// 第一個隊伍篩選之後只要前3個人;
Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);
// 第二個隊伍只要姓張的成員姓名;
// 第二個隊伍篩選之後不要前2個人;
Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("張")).skip(2);
// 將兩個隊伍合併為一個隊伍;
// 根據姓名創建Person對象;
// 列印整個隊伍的Person對象信息。
Stream.concat(streamOne, streamTwo).forEach(s->System.out.println(s));
}
}
運行效果完全一樣:
宋遠橋
蘇星河
洪七公
張二狗
張天愛
張三
函數拼接與終結方法
在上述介紹的各種方法中,凡是返回值仍然為Stream
介面的為函數拼接方法,它們支持鏈式調用;而返回值不再為Stream
介面的為終結方法,不再支持鏈式調用。如下表所示:
方法名 | 方法作用 | 方法種類 | 是否支持鏈式調用 |
---|---|---|---|
count | 統計個數 | 終結 | 否 |
forEach | 逐一處理 | 終結 | 否 |
filter | 過濾 | 函數拼接 | 是 |
limit | 取用前幾個 | 函數拼接 | 是 |
skip | 跳過前幾個 | 函數拼接 | 是 |
concat | 組合 | 函數拼接 | 是 |
Stream流中的結果到集合中
Stream流提供 collect方法,其參數需要一個 java.util.stream.Collector<T,A, R>介面對象來指定收集到哪 種集合中。java.util.stream.Collectors 類提供一些方法,可以作為 Collector`介面的實例:
- public static
Collector<T, ?, List > toList():轉換為 List集合。 - public static
Collector<T, ?, Set > toSet():轉換為 Set集合。
下麵是這兩個方法的基本使用代碼
public class Test {
public static void main(String[] args) {
Stream<String> stream = Stream.of("aa", "bb", "cc");
//轉換為list集合
List<String> list = stream.collect(Collectors.toList());
//轉換為set集合
Set<String> set = stream.collect(Collectors.toSet());
//轉換為ArrayList集合
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
//轉換為HashSet集合
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));
}
}
Stream流中的結果到數組中
Stream提供 toArray方法來將結果放到一個數組中,返回值類型是Object[]的:
Object[] toArray();
其使用場景如:
public class Test {
public static void main(String[] args) {
Stream<String> stream = Stream.of("aa", "bb", "cc");
Object[] objects = stream.toArray();
System.out.println(Arrays.toString(objects));
String[] strings = stream.toArray(String[]::new);
System.out.println(Arrays.toString(strings));
}
}