Java8新特性之Stream使用詳解
一、Stream介紹
什么是流?
流是Java8引入的全新概念,它用來處理集合中的數(shù)據(jù),暫且可以把它理解為一種高級集合。
眾所周知,集合操作非常麻煩,若要對集合進(jìn)行篩選、投影,需要寫大量的代碼,而流是以聲明的形式操作集合,它就像SQL語句,我們只需告訴流需要對集合進(jìn)行什么操作,它就會自動進(jìn)行操作,并將執(zhí)行結(jié)果交給你,無需我們自己手寫代碼。
因此,流的集合操作對我們來說是透明的,我們只需向流下達(dá)命令,它就會自動把我們想要的結(jié)果給我們。
由于操作過程完全由Java處理,因此它可以根據(jù)當(dāng)前硬件環(huán)境選擇最優(yōu)的方法處理,我們也無需編寫復(fù)雜又容易出錯的多線程代碼了。
流的特點
- 只能遍歷一次
我們可以把流想象成一條流水線,流水線的源頭是我們的數(shù)據(jù)源(一個集合),數(shù)據(jù)源中的元素依次被輸送到流水線上,我們可以在流水線上對元素進(jìn)行各種操作。
一旦元素走到了流水線的另一頭,那么這些元素就被“消費掉了”,我們無法再對這個流進(jìn)行操作。
當(dāng)然,我們可以從數(shù)據(jù)源那里再獲得一個新的流重新遍歷一遍。
- 采用內(nèi)部迭代方式
若要對集合進(jìn)行處理,則需我們手寫處理代碼,這就叫做外部迭代。
而要對流進(jìn)行處理,我們只需告訴流我們需要什么結(jié)果,處理過程由流自行完成,這就稱為內(nèi)部迭代。
流的操作種類
流的操作分為兩種,分別為中間操作和終端操作。
- 中間操作
當(dāng)數(shù)據(jù)源中的數(shù)據(jù)上了流水線后,這個過程對數(shù)據(jù)進(jìn)行的所有操作都稱為“中間操作”。 中間操作仍然會返回一個流對象,因此多個中間操作可以串連起來形成一個流水線。
- 終端操作
當(dāng)所有的中間操作完成后,若要將數(shù)據(jù)從流水線上拿下來,則需要執(zhí)行終端操作。 終端操作將返回一個執(zhí)行結(jié)果,這就是你想要的數(shù)據(jù)。 流的操作過程
使用流一共需要三步:
- 準(zhǔn)備一個數(shù)據(jù)源
- 執(zhí)行中間操作
中間操作可以有多個,它們可以串連起來形成流水線。 - 執(zhí)行終端操作
執(zhí)行終端操作后本次流結(jié)束,你將獲得一個執(zhí)行結(jié)果。
二、Stream 接口一覽
List 轉(zhuǎn) Stream
// 轉(zhuǎn)stream list.stream() // 并發(fā)處理 list.parallelStream()
filter(過濾)
Stream<T> filter(Predicate<? super T> predicate);
map(元素轉(zhuǎn)換)
<R> Stream<R> map(Function<? super T, ? extends R> mapper); IntStream mapToInt(ToIntFunction<? super T> mapper); LongStream mapToLong(ToLongFunction<? super T> mapper); DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
flatMap(元素轉(zhuǎn)換)
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper); LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper); DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);
distinct(去除重復(fù),對象需要重寫 equals、hashCode)
Stream<T> distinct();
sorted(排序)
Stream<T> sorted(); Stream<T> sorted(Comparator<? super T> comparator);
peek(生成新的流:流是單向的,例如用于日志打?。?/p>
Stream<T> peek(Consumer<? super T> action);
limit(取前面 n 個元素)
Stream<T> limit(long maxSize);
skip(跳過 n 個元素)
Stream<T> skip(long n);
forEach(遍歷)
void forEach(Consumer<? super T> action); void forEachOrdered(Consumer<? super T> action);
toArray(轉(zhuǎn)換成數(shù)組)
Object[] toArray(); <A> A[] toArray(IntFunction<A[]> generator);
reduce(結(jié)果歸并)
T reduce(T identity, BinaryOperator<T> accumulator); Optional<T> reduce(BinaryOperator<T> accumulator); <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
collect(轉(zhuǎn)換成集合)
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner); <R, A> R collect(Collector<? super T, A, R> collector);
轉(zhuǎn)list
// 轉(zhuǎn)list Collectors.toList(); // 轉(zhuǎn)set Collectors.toSet(); // 轉(zhuǎn)map List<TestVo> testList = new ArrayList<>(10); Map<Long, TestVo> data = releaseList.stream() .collect(Collectors.toMap(TestVo::getId, x -> x));
count(計數(shù))
long count();
查找
boolean anyMatch(Predicate<? super T> predicate); boolean allMatch(Predicate<? super T> predicate); boolean noneMatch(Predicate<? super T> predicate);
查找2
Optional<T> findFirst(); Optional<T> findAny();
三、Stream API 使用流
創(chuàng)建流
在使用流之前,首先需要擁有一個數(shù)據(jù)源,并通過StreamAPI提供的一些方法獲取該數(shù)據(jù)源的流對象。數(shù)據(jù)源可以有多種形式:
1. 集合
這種數(shù)據(jù)源較為常用,通過stream()方法即可獲取流對象:
List<Person> list = new ArrayList<Person>(); Stream<Person> stream = list.stream();
2. 數(shù)組
通過Arrays類提供的靜態(tài)函數(shù)stream()獲取數(shù)組的流對象:
String[] names = {"chaimm","peter","john"}; Stream<String> stream = Arrays.stream(names);
3. 值
直接將幾個值變成流對象:
Stream<String> stream = Stream.of("chaimm","peter","john");
4. 文件
try(Stream lines = Files.lines(Paths.get(“文件路徑名”),Charset.defaultCharset())){ //可對lines做一些操作 }catch(IOException e){ }
5. iterator
創(chuàng)建無限流
Stream.iterate(0, n -> n + 2) .limit(10) .forEach(System.out::println);
PS:Java7簡化了IO操作,把打開IO操作放在try后的括號中即可省略關(guān)閉IO的代碼。
篩選 filter
filter 函數(shù)接收一個Lambda表達(dá)式作為參數(shù),該表達(dá)式返回boolean,在執(zhí)行過程中,流將元素逐一輸送給filter,并篩選出執(zhí)行結(jié)果為true的元素。 如,篩選出所有學(xué)生:
List<Person> result = list.stream() .filter(Person::isStudent) .collect(toList());
去重distinct
去掉重復(fù)的結(jié)果:
List<Person> result = list.stream() .distinct() .collect(toList());
截取
截取流的前N個元素:
List<Person> result = list.stream() .limit(3) .collect(toList());
跳過
跳過流的前n個元素:
List<Person> result = list.stream() .skip(3) .collect(toList());
映射
對流中的每個元素執(zhí)行一個函數(shù),使得元素轉(zhuǎn)換成另一種類型輸出。
流會將每一個元素輸送給map函數(shù),并執(zhí)行map中的Lambda表達(dá)式,最后將執(zhí)行結(jié)果存入一個新的流中。
如,獲取每個人的姓名(實則是將Perosn類型轉(zhuǎn)換成String類型):
List<Person> result = list.stream() .map(Person::getName) .collect(toList());
合并多個流
例:列出List中各不相同的單詞,List集合如下:
List<String> list = new ArrayList<String>(); list.add("I am a boy"); list.add("I love the girl"); list.add("But the girl loves another girl");
思路如下:
首先將list變成流:
list.stream();
按空格分詞:
list.stream() .map(line->line.split(" "));
分完詞之后,每個元素變成了一個String[]數(shù)組。
將每個 String[] 變成流:
list.stream() .map(line->line.split(" ")) .map(Arrays::stream)
此時一個大流里面包含了一個個小流,我們需要將這些小流合并成一個流。
將小流合并成一個大流:用 flatMap 替換剛才的 map
list.stream() .map(line->line.split(" ")) .flatMap(Arrays::stream)
去重
list.stream() .map(line->line.split(" ")) .flatMap(Arrays::stream) .distinct() .collect(toList());
是否匹配任一元素:anyMatch
anyMatch用于判斷流中是否存在至少一個元素滿足指定的條件,這個判斷條件通過Lambda表達(dá)式傳遞給anyMatch,執(zhí)行結(jié)果為boolean類型。
如,判斷l(xiāng)ist中是否有學(xué)生:
boolean result = list.stream() .anyMatch(Person::isStudent);
是否匹配所有元素:allMatch
allMatch用于判斷流中的所有元素是否都滿足指定條件,這個判斷條件通過Lambda表達(dá)式傳遞給anyMatch,執(zhí)行結(jié)果為boolean類型。
如,判斷是否所有人都是學(xué)生:
boolean result = list.stream() .allMatch(Person::isStudent);
是否未匹配所有元素:noneMatch
noneMatch與allMatch恰恰相反,它用于判斷流中的所有元素是否都不滿足指定條件:
boolean result = list.stream() .noneMatch(Person::isStudent);
獲取任一元素findAny
findAny能夠從流中隨便選一個元素出來,它返回一個Optional類型的元素。
Optional<Person> person = list.stream().findAny();
獲取第一個元素findFirst
Optional<Person> person = list.stream().findFirst();
歸約
歸約是將集合中的所有元素經(jīng)過指定運算,折疊成一個元素輸出,如:求最值、平均數(shù)等,這些操作都是將一個集合的元素折疊成一個元素輸出。
在流中,reduce函數(shù)能實現(xiàn)歸約。 reduce函數(shù)接收兩個參數(shù):
- 初始值
- 進(jìn)行歸約操作的Lambda表達(dá)式
元素求和:自定義Lambda表達(dá)式實現(xiàn)求和
例:計算所有人的年齡總和
@Test public void contextLoads() { List<Person> list = new ArrayList<>(); list.add(new Person().setAge(20)); list.add(new Person().setAge(25)); int age = list.stream().map(Person::getAge).reduce(0, Integer::sum); System.out.println(age); }
@Data @Accessors(chain = true) class Person { private int age; }
- reduce的第一個參數(shù)表示初試值為0;
- reduce的第二個參數(shù)為需要進(jìn)行的歸約操作,它接收一個擁有兩個參數(shù)的Lambda表達(dá)式,reduce會把流中的元素兩兩輸給Lambda表達(dá)式,最后將計算出累加之和。
元素求和:使用Integer.sum函數(shù)求和
上面的方法中我們自己定義了Lambda表達(dá)式實現(xiàn)求和運算,如果當(dāng)前流的元素為數(shù)值類型,那么可以使用Integer提供了sum函數(shù)代替自定義的Lambda表達(dá)式,如:
int age = list.stream().reduce(0, Integer::sum);
Integer類還提供了 min 、 max 等一系列數(shù)值操作,當(dāng)流中元素為數(shù)值類型時可以直接使用。
數(shù)值流的使用
采用reduce進(jìn)行數(shù)值操作會涉及到基本數(shù)值類型和引用數(shù)值類型之間的裝箱、拆箱操作,因此效率較低。 當(dāng)流操作為純數(shù)值操作時,使用數(shù)值流能獲得較高的效率。
將普通流轉(zhuǎn)換成數(shù)值流
StreamAPI提供了三種數(shù)值流:IntStream、DoubleStream、LongStream,也提供了將普通流轉(zhuǎn)換成數(shù)值流的三種方法:mapToInt、mapToDouble、mapToLong。 如,將Person中的age轉(zhuǎn)換成數(shù)值流:
IntStream stream = list.stream().mapToInt(Person::getAge);
數(shù)值計算
每種數(shù)值流都提供了數(shù)值計算函數(shù),如max、min、sum等。如,找出最大的年齡:
OptionalInt maxAge = list.stream() .mapToInt(Person::getAge) .max();
由于數(shù)值流可能為空,并且給空的數(shù)值流計算最大值是沒有意義的,因此max函數(shù)返回OptionalInt,它是Optional的一個子類,能夠判斷流是否為空,并對流為空的情況作相應(yīng)的處理。 此外,mapToInt、mapToDouble、mapToLong進(jìn)行數(shù)值操作后的返回結(jié)果分別為:OptionalInt、OptionalDouble、OptionalLong
中間操作和收集操作
操作 | 類型 | 返回類型 | 使用的類型/函數(shù)式接口 | 函數(shù)描述符 |
filter | 中間 | Stream<T> | Predicate<T> | T -> boolean |
distinct | 中間 | Stream<T> | ||
skip | 中間 | Stream<T> | long | |
map | 中間 | Stream<R> | Function<T, R> | T -> R |
flatMap | 中間 | Stream<R> | Function<T, Stream<R>> | T -> Stream<R> |
limit | 中間 | Stream<T> | long | |
sorted | 中間 | Stream<T> | Comparator<T> | (T, T) -> int |
anyMatch | 終端 | boolean | Predicate<T> | T -> boolean |
noneMatch | 終端 | boolean | Predicate<T> | T -> boolean |
allMatch | 終端 | boolean | Predicate<T> | T -> boolean |
findAny | 終端 | Optional<T> | ||
findFirst | 終端 | Optional<T> | ||
forEach | 終端 | void | Consumer<T> | T -> void |
collect | 終端 | R | Collector<T, A, R> | |
reduce | 終端 | Optional<T> | BinaryOperator<T> | (T, T) -> T |
count | 終端 | long |
四、Stream API
Collector 收集
收集器用來將經(jīng)過篩選、映射的流進(jìn)行最后的整理,可以使得最后的結(jié)果以不同的形式展現(xiàn)。
collect 方法即為收集器,它接收 Collector 接口的實現(xiàn)作為具體收集器的收集方法。
Collector 接口提供了很多默認(rèn)實現(xiàn)的方法,我們可以直接使用它們格式化流的結(jié)果;也可以自定義 Collector 接口的實現(xiàn),從而定制自己的收集器。
歸約
流由一個個元素組成,歸約就是將一個個元素“折疊”成一個值,如求和、求最值、求平均值都是歸約操作。
一般性歸約
若你需要自定義一個歸約操作,那么需要使用 Collectors.reducing 函數(shù),該函數(shù)接收三個參數(shù):
- 第一個參數(shù)為歸約的初始值
- 第二個參數(shù)為歸約操作進(jìn)行的字段
- 第三個參數(shù)為歸約操作的過程
匯總
Collectors類專門為匯總提供了一個工廠方法: Collectors.summingInt 。
它可接受一 個把對象映射為求和所需int的函數(shù),并返回一個收集器;該收集器在傳遞給普通的 collect 方法后即執(zhí)行我們需要的匯總操作。
分組
數(shù)據(jù)分組是一種更自然的分割數(shù)據(jù)操作,分組就是將流中的元素按照指定類別進(jìn)行劃分,類似于SQL語句中的 GROUPBY 。
多級分組
多級分組可以支持在完成一次分組后,分別對每個小組再進(jìn)行分組。 使用具有兩個參數(shù)的 groupingBy 重載方法即可實現(xiàn)多級分組。
- 第一個參數(shù):一級分組的條件
- 第二個參數(shù):一個新的 groupingBy 函數(shù),該函數(shù)包含二級分組的條件
Collectors 類的靜態(tài)工廠方法
工廠方法 | 返回類型 | 用途 | 示例 |
toList | List<T> | 把流中所有項目收集到一個 List | List<Project> projects = projectStream.collect(toList()); |
toSet | Set<T> | 把流中所有項目收集到一個 Set,刪除重復(fù)項 | Set<Project> projects = projectStream.collect(toSet()); |
toCollection | Collection<T> | 把流中所有項目收集到給定的供應(yīng)源創(chuàng)建的集合 | Collection<Project> projects = projectStream.collect(toCollection(), ArrayList::new); |
counting | Long | 計算流中元素的個數(shù) | long howManyProjects = projectStream.collect(counting()); |
summingInt | Integer | 對流中項目的一個整數(shù)屬性求和 | int totalStars = projectStream.collect(summingInt(Project::getStars)); |
averagingInt | Double | 計算流中項目 Integer 屬性的平均值 | double avgStars = projectStream.collect(averagingInt(Project::getStars)); |
summarizingInt | IntSummaryStatistics | 收集關(guān)于流中項目 Integer 屬性的統(tǒng)計值,例如最大、最小、 總和與平均值 | IntSummaryStatistics projectStatistics = projectStream.collect(summarizingInt(Project::getStars)); |
joining | String | 連接對流中每個項目調(diào)用 toString 方法所生成的字符串 | String shortProject = projectStream.map(Project::getName).collect(joining(", ")); |
maxBy | Optional<T> | 按照給定比較器選出的最大元素的 Optional, 或如果流為空則為 Optional.empty() | Optional<Project> fattest = projectStream.collect(maxBy(comparingInt(Project::getStars))); |
minBy | Optional<T> | 按照給定比較器選出的最小元素的 Optional, 或如果流為空則為 Optional.empty() | Optional<Project> fattest = projectStream.collect(minBy(comparingInt(Project::getStars))); |
reducing | 歸約操作產(chǎn)生的類型 | 從一個作為累加器的初始值開始,利用 BinaryOperator 與流中的元素逐個結(jié)合,從而將流歸約為單個值 | int totalStars = projectStream.collect(reducing(0, Project::getStars, Integer::sum)); |
collectingAndThen | 轉(zhuǎn)換函數(shù)返回的類型 | 包含另一個收集器,對其結(jié)果應(yīng)用轉(zhuǎn)換函數(shù) | int howManyProjects = projectStream.collect(collectingAndThen(toList(), List::size)); |
groupingBy | Map<K, List<T>> | 根據(jù)項目的一個屬性的值對流中的項目作問組,并將屬性值作 為結(jié)果 Map 的鍵 | Map<String,List<Project>> projectByLanguage = projectStream.collect(groupingBy(Project::getLanguage)); |
partitioningBy | Map<Boolean,List<T>> | 根據(jù)對流中每個項目應(yīng)用斷言的結(jié)果來對項目進(jìn)行分區(qū) | Map<Boolean,List<Project>> vegetarianDishes = projectStream.collect(partitioningBy(Project::isVegetarian)); |
轉(zhuǎn)換類型
有一些收集器可以生成其他集合。比如前面已經(jīng)見過的 toList ,生成了 java.util.List 類的實例。 還有 toSet 和 toCollection ,分別生成 Set 和 Collection 類的實例。 到目前為止, 我已經(jīng)講了很多流上的鏈?zhǔn)讲僮?,但總有一些時候,需要最終生成一個集合——比如:
- 已有代碼是為集合編寫的,因此需要將流轉(zhuǎn)換成集合傳入;
- 在集合上進(jìn)行一系列鏈?zhǔn)讲僮骱?,最終希望生成一個值;
- 寫單元測試時,需要對某個具體的集合做斷言。
使用 toCollection ,用定制的集合收集元素
stream.collect(toCollection(TreeSet::new));
還可以利用收集器讓流生成一個值。 maxBy 和 minBy 允許用戶按某種特定的順序生成一個值。
數(shù)據(jù)分區(qū)
分區(qū)是分組的特殊情況:由一個斷言(返回一個布爾值的函數(shù))作為分類函數(shù),它稱分區(qū)函數(shù)。 分區(qū)函數(shù)返回一個布爾值,這意味著得到的分組 Map 的鍵類型是 Boolean,于是它最多可以分為兩組: true是一組,false是一組。
分區(qū)的好處在于保留了分區(qū)函數(shù)返回true或false的兩套流元素列表。
并行流
并行流就是一個把內(nèi)容分成多個數(shù)據(jù)塊,并用不不同的線程分別處理每個數(shù)據(jù)塊的流。最后合并每個數(shù)據(jù)塊的計算結(jié)果。
將一個順序執(zhí)行的流轉(zhuǎn)變成一個并發(fā)的流只要調(diào)用 parallel() 方法
public static long parallelSum(long n){ return Stream.iterate(1L, i -> i +1).limit(n).parallel().reduce(0L,Long::sum); }
將一個并發(fā)流轉(zhuǎn)成順序的流只要調(diào)用 sequential() 方法
stream.parallel().filter(...).sequential().map(...).parallel().reduce();
這兩個方法可以多次調(diào)用,只有最后一個調(diào)用決定這個流是順序的還是并發(fā)的。
并發(fā)流使用的默認(rèn)線程數(shù)等于你機(jī)器的處理器核心數(shù)。
通過這個方法可以修改這個值,這是全局屬性。
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "12");
并非使用多線程并行流處理數(shù)據(jù)的性能一定高于單線程順序流的性能,因為性能受到多種因素的影響。
如何高效使用并發(fā)流的一些建議:
- 如果不確定, 就自己測試。
- 盡量使用基本類型的流 IntStream, LongStream, DoubleStream
- 有些操作使用并發(fā)流的性能會比順序流的性能更差,比如limit,findFirst,依賴元素順序的操作在并發(fā)流中是極其消耗性能的。findAny的性能就會好很多,應(yīng)為不依賴順序。
- 考慮流中計算的性能(Q)和操作的性能(N)的對比, Q表示單個處理所需的時間,N表示需要處理的數(shù)量,如果Q的值越大, 使用并發(fā)流的性能就會越高。
- 數(shù)據(jù)量不大時使用并發(fā)流,性能得不到提升。
- 考慮數(shù)據(jù)結(jié)構(gòu):并發(fā)流需要對數(shù)據(jù)進(jìn)行分解,不同的數(shù)據(jù)結(jié)構(gòu)被分解的性能時不一樣的。
流的數(shù)據(jù)源和可分解性
源 | 可分解性 |
ArrayList | 非常好 |
LinkedList | 差 |
IntStream.range | 非常好 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |
流的特性以及中間操作對流的修改都會對數(shù)據(jù)對分解性能造成影響。
比如固定大小的流在任務(wù)分解的時候就可以平均分配,但是如果有filter操作,那么流就不能預(yù)先知道在這個操作后還會剩余多少元素。
考慮終端操作的性能:如果終端操作在合并并發(fā)流的計算結(jié)果時的性能消耗太大,那么使用并發(fā)流提升的性能就會得不償失。
到此這篇關(guān)于Java8新特性之Stream使用詳解的文章就介紹到這了,更多相關(guān)Java8的Stream內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java spring webmvc如何實現(xiàn)控制反轉(zhuǎn)
這篇文章主要介紹了Java spring webmvc如何實現(xiàn)控制反轉(zhuǎn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-08-08Spring Boot 3.x 集成 Eureka Server/Cl
隨著SpringBoot 3.x版本的開發(fā)嘗試,本文記錄了在集成Eureka Server/Client時所遇到的問題和解決方案,文中詳細(xì)介紹了搭建服務(wù)、配置文件和測試步驟,感興趣的朋友跟隨小編一起看看吧2024-09-09詳解MyBatis多數(shù)據(jù)源配置(讀寫分離)
這篇文章主要介紹了詳解MyBatis多數(shù)據(jù)源配置(讀寫分離),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01Spring Boot接收單個String入?yún)⒌慕鉀Q方法
這篇文章主要給大家介紹了關(guān)于Spring Boot接收單個String入?yún)⒌慕鉀Q方法,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用spring boot具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11