四川成都網站優(yōu)化百度快速排名技術培訓教程
Stream操作流
在Java 8
中,得益于Lambda
所帶來的函數式編程,引入了一個全新的Stream
概念,用于解決已有集合類庫既有的弊端。
1.1 集合的迭代
幾乎所有的集合(如 Collection
接口或 Map
接口等)都支持直接或間接的迭代遍歷操作。而當我們需要對集合中的元素進行操作的時候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷。例如:
public class Demo {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);}}
}
這是一段非常簡單的集合遍歷操作:對集合中的每一個字符串都進行打印輸出操作。
循環(huán)遍歷的弊端
Java 8
的Lambda
讓我們可以更加專注于做什么(What
),而不是怎么做(How
),這點此前已經結合內部類進行了對比說明。現在,我們仔細體會一下上例代碼,可以發(fā)現:
for
循環(huán)的語法就是“怎么做”for
循環(huán)的循環(huán)體才是“做什么”
為什么使用循環(huán)?因為要進行遍歷。但循環(huán)是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而并不是從第一個到最后一個順次處理的循環(huán)。前者是目的,后者是方式。
試想一下,如果希望對集合中的元素進行篩選過濾:
- 將集合A根據條件一過濾為子集B;
- 然后再根據條件二過濾為子集C。
那怎么辦?在Java 8
之前的做法可能為:
這段代碼中含有三個循環(huán),每一個作用不同:
- 首先篩選所有姓張的人;
- 然后篩選名字有三個字的人;
- 最后進行對結果進行打印輸出。
public class DemoFilter {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);}}
}
每當我們需要對集合中的元素進行操作的時候,總是需要進行循環(huán)、循環(huán)、再循環(huán)。這是理所當然的么?不是。循環(huán)是做事情的方式,而不是目的。另一方面,使用線性循環(huán)就意味著只能遍歷一次。如果希望再次遍歷,只能再使用另一個循環(huán)從頭開始。
那,Lambda
的衍生物Stream
能給我們帶來怎樣更加優(yōu)雅的寫法呢?
public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("張無忌");list.add("周芷若");list.add("趙敏");list.add("陽頂天");list.add("小昭");list.add("楊逍");list.add("楊過");list.add("韋一笑");list.add("謝遜");list.add("滅絕師太");list.add("靜虛師太");//獲取list的stream流Stream<String> listStream = list.stream();//stream.filter(boolean) 當boolean = true 是保留,false移除//過濾流中的非姓張的人名 //boolean test(T t)Stream<String> zhangStream = listStream.filter(t -> t.startsWith("張"));//姓張 且名字長度是3個字的Stream<String> shortStream = zhangStream.filter(t -> t.length() == 3);//迭代輸出 //void accept(T t)shortStream.forEach(t -> System.out.println(t));//等同于list.stream().filter(s -> s.startsWith("張")).filter(s -> s.length() == 3).forEach(s -> System.out.println(s));}
直接閱讀代碼的字面意思即可完美展示無關邏輯方式的語義:獲取流、過濾姓張、過濾長度為3、逐一打印。代碼中并沒有體現使用線性循環(huán)或是其他任何算法進行遍歷,我們真正要做的事情內容被更好地體現在代碼中。流式版本比之前的寫法要更易于閱讀,因為流遵循了"做什么而非怎么做"的原則。
流表面上看起來和集合很類似,都可以讓我們轉換和獲取數據。但是,它們之間存在著顯著的差異:
-
流并不存儲其元素。這些元素可能存儲在底層的集合中,或者是按需生成的。
-
流的操作不會修改其數據源。例如,
filter
方法不會從新的流中移除元素,而是會生成一個新的流,其中不包含被過濾掉的元素。 -
流的操作是盡可能惰性執(zhí)行的。這意味著直至需要其結果時,操作才會執(zhí)行。
例如:
如果我們只想査找前5個長單詞而不是所有的長單詞,那么filter
方法就會在匹配到第 5 個單詞后就停止過濾。
1.2 流式思想
注意:請暫時忘記對傳統(tǒng)IO流的固有印象!
整體來看,流式思想類似于工廠車間的“生產流水線”
當需要對多個元素進行操作(特別是多步操作)的時候,考慮到性能及便利性,我們應該首先拼好一個“模型”步驟方案,然后再按照方案去執(zhí)行它。
這張圖中展示了過濾、映射、跳過、計數等多步操作,這是一種集合元素的處理方案,而方案就是一種“函數模型”。圖中的每一個方框都是一個“流”,調用指定的方法,可以從一個流模型轉換為另一個流模型。而最右側的數字3是最終結果。
這里的 filter
、 map
、 skip
都是在對函數模型進行操作,集合元素并沒有真正被處理。只有當終結方法 count
執(zhí)行的時候,整個模型才會按照指定策略執(zhí)行操作。而這得益于Lambda
的延遲執(zhí)行特性。
備注:“Stream流”其實是一個集合元素的函數模型,它并不是集合,也不是數據結構,其本身并不存儲任何元素(或其地址值)。
1.3 獲取流方式
生成Stream流的方式
-
Collection體系集合
使用默認方法stream()生成流, default Stream stream()
-
Map體系集合
把Map轉成Set集合,間接的生成流
-
數組
通過Arrays中的靜態(tài)方法stream生成流
-
同種數據類型的多個數據
通過Stream接口的靜態(tài)方法of(T… values)生成流
java.util.stream.Stream
是Java 8
新加入的最常用的流接口。(這并不是一個函數式接口。)
public interface Stream<T> extends BaseStream<T, Stream<T>>
獲取一個流非常簡單,有以下幾種常用的方式:
- 所有的
Collection
集合都可以通過stream
默認方法獲取流; Stream
接口的靜態(tài)方法of
可以獲取數組對應的流。
**根據Collection
獲取流 **
首先, java.util.Collection
接口中加入了default
方法 stream
用來獲取流,所以其所有實現類均可獲取流。
public class DemoGetStream {public static void main(String[] args) {/*獲取Stream流的方式1.Collection中 方法 Stream stream()2.Stream接口 中靜態(tài)方法 of(T...t) 向Stream中添加多個數據*/List<String> list = new ArrayList<>();Stream<String> stream1 = list.stream();Set<String> set = new HashSet<>();Stream<String> stream2 = set.stream();}
}
根據數組獲取流
如果使用的不是集合或映射而是數組,由于數組對象不可能添加默認方法,所以 Stream
接口中提供了靜態(tài)方法of
,使用很簡單:
public static void main(String[] args) {String[] array = { "張無忌", "張翠山", "張三豐", "張翠山" };Stream<String> stream = Stream.of(array);Stream<String> stream3 = Stream.of("張小山");Stream<String> stream4 = Stream.of("張無忌", "張翠山", "張三豐", "張一元");
}
of
方法的參數其實是一個可變參數,所以支持數組。
1.4 常用方法
流模型的操作很豐富,這里介紹一些常用的API
。這些方法可以被分成兩種:
-
終結方法:返回值類型不再是
Stream
接口自身類型的方法,因此不再支持類似StringBuilder
那樣的鏈式調用。終結方法包括count
和forEach
等 方法。 -
非終結方法(中間方法):返回值類型仍然是
Stream
接口自身類型的方法,因此支持鏈式調用。(除了終結方法外,其余方法均為非終結方法。)備注:除了以下要介紹的方法外還有更多方法,請自行參考API文檔。
方法名 | 說明 |
---|---|
Stream filter(Predicate predicate) | 用于對流中的數據進行過濾 |
Stream limit(long maxSize) | 返回此流中的元素組成的流,截取前指定參數個數的數據 |
Stream skip(long n) | 跳過指定參數個數的數據,返回由該流的剩余元素組成的流 |
static Stream concat(Stream a, Stream b) | 合并a和b兩個流為一個流 |
Stream distinct() | 返回由該流的不同元素(根據Object.equals(Object) )組成的流 |
filter:過濾
可以通過 filter
方法將一個流轉換成另一個子集流。方法聲明:
Stream<T> filter(Predicate<? super T> predicate);
該接口接收一個 Predicate
函數式接口參數(可以是一個Lambda
)作為篩選條件。
public static void main(String[] args) {Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若");Stream<String> result = original.filter((String s) -> s.startsWith("張"));}
limit:取用前幾個
limit
方法可以對流進行截取,只取用前n
個。方法:
Stream<T> limit(long maxSize)
參數是一個long
型,如果集合當前長度大于參數則進行截取;否則不進行操作?;臼褂?#xff1a;
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)
如果流的當前長度大于n
,則跳過前n
個;否則將會得到一個長度為0的空流?;臼褂?#xff1a;
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
接口的靜態(tài)方法concat
:
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
這是一個靜態(tài)方法,與 java.lang.String 當中的 concat 方法是不同的。
public static void main(String[] args) {Stream<String> streamA = Stream.of("張無忌");Stream<String> streamB = Stream.of("張翠山");Stream<String> result = Stream.concat(streamA, streamB);
}
distinct: 去重
public static void main(String[] args) {//創(chuàng)建一個集合,存儲多個字符串元素ArrayList<String> list = new ArrayList<String>();list.add("林青霞");list.add("張曼玉");list.add("王祖賢");list.add("柳巖");list.add("張敏");list.add("張無忌");// 取前4個數據組成一個流Stream<String> s1 = list.stream().limit(4);// 跳過2個數據組成一個流Stream<String> s2 = list.stream().skip(2);Stream.concat(s1,s2).distinct().forEach(s-> System.out.println(s));}
終結方法:
forEach : 逐一處理
雖然方法名字叫 forEach
,但是與for
循環(huán)中的for-each
不同,該方法并不保證元素的逐一消費動作在流中是被有序執(zhí)行的。
void forEach(Consumer<? super T> action);
該方法接收一個 Consumer
接口函數,會將每一個流元素交給該函數進行處理。例如:
public class DemoForEach {public static void main(String[] args) {Stream<String> stream = Stream.of("張無忌", "張三豐", "周芷若");stream.forEach((String str)->{System.out.println(str);});}
}
在這里,lambda
表達式 (String str)->{System.out.println(str);}
就是一個Consumer
函數式接口的示例。
count:統(tǒng)計個數
正如舊集合 Collection
當中的 size
方法一樣,流提供 count
方法來數一數其中的元素個數:
long count();
此處方法返回值是long
而不是int
public static void main(String[] args) {Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若");Stream<String> result = original.filter((String s) -> s.startsWith("張"));System.out.println(result.count()); // 2
}
在上述介紹的各種方法中,凡是返回值仍然為 Stream
接口的為函數拼接方法,它們支持鏈式調用;而返回值不再為Stream
接口的為終結方法,不再支持鏈式調用。
Files.lines : 方法
Files.lines
是 Java 8 中的一個方法,用于讀取文件中的所有行并返回為一個流(Stream)對象。
使用 Files.lines
方法可以方便地逐行讀取文本文件。下面是一個示例代碼:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;public class FileLinesExample {public static void main(String[] args) {String filename = "path/to/file.txt";try (Stream<String> lines = Files.lines(Paths.get(filename))) {lines.forEach(System.out::println);} catch (IOException e) {e.printStackTrace();}}
}
在這個示例中,我們使用 Paths.get
方法來獲取文件路徑,并將其傳遞給 Files.lines
方法。然后使用 try-with-resources
語句來確保在讀取完成后關閉流。在 lines.forEach(System.out::println)
中,我們遍歷每一行并打印到控制臺上。
請注意,你需要替換 filename
變量的值為實際的文件路徑。此外,使用 Files.lines
時要注意處理可能拋出的 IOException
異常。
常見末端方法:
返回類型 | 方法名 | 方法簽名 | 描述 |
---|---|---|---|
void | forEach | void forEach(Consumer<? super T> action) | 對流中的每個元素執(zhí)行給定的操作 |
void | forEachOrdered | void forEachOrdered(Consumer<? super T> action) | 保證按照流中元素的遍歷順序執(zhí)行給定的操作 |
long | count | long count() | 返回流中的元素數 |
Optional<T> | findFirst | Optional findFirst() | 返回流中的第一個元素 |
Optional<T> | findAny | Optional findAny() | 返回流中的任意一個元素 |
boolean | allMatch | boolean allMatch(Predicate<? super T> predicate) | 檢查流中的所有元素是否都滿足給定的謂詞 |
boolean | anyMatch | boolean anyMatch(Predicate<? super T> predicate) | 檢查流中是否有任意一個元素滿足給定的謂詞 |
boolean | noneMatch | boolean noneMatch(Predicate<? super T> predicate) | 檢查流中的所有元素是否都不滿足給定的謂詞 |
Optional<T> | max | Optional max(Comparator<? super T> comparator) | 返回流中根據給定比較器最大的元素 |
Optional<T> | min | Optional min(Comparator<? super T> comparator) | 返回流中根據給定比較器最小的元素 |
T | reduce | T reduce(T identity, BinaryOperator accumulator) | 根據給定的起始值和累加函數將流中的所有元素聚合成一個結果 |
Optional<T> | reduce | Optional reduce(BinaryOperator accumulator) | 根據給定的累加函數將流中的所有元素聚合成一個結果 |
R | collect | R collect(Collector<? super T, A, R> collector) | 將流中的所有元素收集到一個容器中 |
IntStream | mapToInt | IntStream mapToInt(ToIntFunction<? super T> mapper) | 將流中的元素映射為 int 值,返回一個 IntStream |
LongStream | mapToLong | LongStream mapToLong(ToLongFunction<? super T> mapper) | 將流中的元素映射為 long 值,返回一個 LongStream |
DoubleStream | mapToDouble | DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) | 將流中的元素映射為 double 值,返回一個 DoubleStream |
Stream<T> | distinct | Stream distinct() | 返回一個去除重復元素之后的新流 |
Stream<T> | sorted | Stream sorted() | 返回一個根據自然順序升序排序后的新流 |
Stream<T> | sorted | Stream sorted(Comparator<? super T> comparator) | 返回一個根據給定比較器排序后的新流 |
Stream<T> | skip | Stream skip(long n) | 返回一個丟棄前 n 個元素后的新流 |
Stream<T> | limit | Stream limit(long maxSize) | 截取前 maxSize 個元素返回一個新流 |
Stream流的收集操作【應用】
-
概念
對數據使用Stream流的方式操作完畢后,可以把流中的數據收集到集合中
-
常用方法
方法名 說明 R collect(Collector collector) 把結果收集到集合中 -
工具類Collectors提供了具體的收集方式
方法名 說明 public static Collector toList() 把元素收集到List集合中 public static Collector toSet() 把元素收集到Set集合中 public static Collector toMap(Function keyMapper,Function valueMapper) 把元素收集到Map集合中 -
代碼演示
// toList和toSet方法演示
public class MyStream7 {public static void main(String[] args) {ArrayList<Integer> list1 = new ArrayList<>();for (int i = 1; i <= 10; i++) {list1.add(i);}list1.add(10);list1.add(10);list1.add(10);list1.add(10);list1.add(10);//filter負責過濾數據的.//collect負責收集數據.//獲取流中剩余的數據,但是他不負責創(chuàng)建容器,也不負責把數據添加到容器中.//Collectors.toList() : 在底層會創(chuàng)建一個List集合.并把所有的數據添加到List集合中.List<Integer> list = list1.stream().filter(number -> number % 2 == 0).collect(Collectors.toList());System.out.println(list);Set<Integer> set = list1.stream().filter(number -> number % 2 == 0).collect(Collectors.toSet());System.out.println(set);
}
}
/**
Stream流的收集方法 toMap方法演示
創(chuàng)建一個ArrayList集合,并添加以下字符串。字符串中前面是姓名,后面是年齡
"zhangsan,23"
"lisi,24"
"wangwu,25"
保留年齡大于等于24歲的人,并將結果收集到Map集合中,姓名為鍵,年齡為值
*/
public class MyStream8 {public static void main(String[] args) {ArrayList<String> list = new ArrayList<>();list.add("zhangsan,23");list.add("lisi,24");list.add("wangwu,25");Map<String, Integer> map = list.stream().filter(s -> {String[] split = s.split(",");int age = Integer.parseInt(split[1]);return age >= 24;}// collect方法只能獲取到流中剩余的每一個數據.//在底層不能創(chuàng)建容器,也不能把數據添加到容器當中//Collectors.toMap 創(chuàng)建一個map集合并將數據添加到集合當中// s 依次表示流中的每一個數據//第一個lambda表達式就是如何獲取到Map中的鍵//第二個lambda表達式就是如何獲取Map中的值).collect(Collectors.toMap(s -> s.split(",")[0],s -> Integer.parseInt(s.split(",")[1]) ));System.out.println(map);}
}
Stream流綜合練習
案例需求
-
現在有兩個ArrayList集合,分別存儲6名男演員名稱和6名女演員名稱,要求完成如下的操作
- 男演員只要名字為3個字的前三人
- 女演員只要姓林的,并且不要第一個
- 把過濾后的男演員姓名和女演員姓名合并到一起
- 把上一步操作后的元素作為構造方法的參數創(chuàng)建演員對象,遍歷數據
演員類Actor已經提供,里面有一個成員變量,一個帶參構造方法,以及成員變量對應的get/set方法
-
代碼實現
演員類
public class Actor {private String name;public Actor(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;} }
測試類
public class StreamTest {public static void main(String[] args) {//創(chuàng)建集合ArrayList<String> manList = new ArrayList<String>();manList.add("周潤發(fā)");manList.add("成龍");manList.add("劉德華");manList.add("吳京");manList.add("周星馳");manList.add("李連杰");ArrayList<String> womanList = new ArrayList<String>();womanList.add("林心如");womanList.add("張曼玉");womanList.add("林青霞");womanList.add("柳巖");womanList.add("林志玲");womanList.add("王祖賢");//男演員只要名字為3個字的前三人Stream<String> manStream = manList.stream().filter(s -> s.length() == 3).limit(3);//女演員只要姓林的,并且不要第一個Stream<String> womanStream = womanList.stream().filter(s -> s.startsWith("林")).skip(1);//把過濾后的男演員姓名和女演員姓名合并到一起Stream<String> stream = Stream.concat(manStream, womanStream);// 將流中的數據封裝成Actor對象之后打印stream.forEach(name -> {Actor actor = new Actor(name);System.out.println(actor);}); }
}
forEachOrdered 和 forEach 區(qū)別
forEachOrdered
和 forEach
都是 Java 8 中 Stream API 提供的方法,用于對流中的元素進行迭代操作。它們之間的區(qū)別在于元素的處理順序。
-
forEach
方法:它在并行流上不保證元素的處理順序。在并行流中,元素會按照多個線程處理,可能會導致輸出的順序與源數據的順序不一致。 -
forEachOrdered
方法:它在并行流上保證元素的處理順序與源數據的順序一致。無論是串行流還是并行流,forEachOrdered
方法都會按照源數據的順序依次處理元素。
下面是一個示例來演示兩者之間的區(qū)別:
import java.util.Arrays;
import java.util.concurrent.TimeUnit;public class ForEachExample {public static void main(String[] args) {String[] words = {"apple", "banana", "cherry", "date"};// forEachSystem.out.println("forEach:");Arrays.stream(words).parallel().forEach(System.out::println);// forEachOrderedSystem.out.println("forEachOrdered:");Arrays.stream(words).parallel().forEachOrdered(System.out::println);}
}
在這個示例中,我們創(chuàng)建了一個包含幾個單詞的字符串數組。然后使用并行流對這些單詞進行處理。通過 forEach
方法打印輸出時,由于并行流的處理順序不確定,每次運行結果可能會有所不同。而通過 forEachOrdered
方法打印輸出時,無論是串行流還是并行流,都會按照源數據的順序依次輸出單詞。
總結來說,forEach
方法適用于不關心元素處理順序的場景,而 forEachOrdered
方法適用于需要保證元素處理順序和源數據順序一致的場景。需要根據具體需求選擇使用哪個方法。