綜述
Java 8 新增了 Stream API割以。Stream API有點(diǎn)類似使用SQL 語(yǔ)句近她,可以將集合中的元素進(jìn)行過(guò)濾。使用時(shí)戚嗅,類似于從一個(gè)管道中抽取元素霸株,并對(duì)他們進(jìn)行操作相味。使用流的一個(gè)優(yōu)點(diǎn)是秽梅,他可以使得我們的程序更小,并且更容易理解拯辙。
與Stream API相關(guān)的接口有Stream
郭变、IntStream
, LongStream
, DoubleStream
(因?yàn)?Java 的泛型不支持基本數(shù)據(jù)類型,而又因頻繁的裝箱涯保、拆箱存在效率問(wèn)題诉濒,故額外有后三者)。
使用Stream
操作時(shí)夕春,我們通常使用鏈?zhǔn)讲僮?/strong>未荒,即將多條代碼合并成一條代碼(事例將在使用Supplier
創(chuàng)建中給出)。
Java Collection 體系數(shù)據(jù)處理的演進(jìn)
本小節(jié)用于測(cè)試的代碼如下:
public record User(Integer id, String name, Integer money) { }
final var users = Arrays.asList(
new User(1, "張三", 200),
new User(2, "李四", 200),
new User(3, "王五", 10000),
new User(4, "趙六", 20000),
new User(5, "王強(qiáng)", 80000)
);
通過(guò)不同方法來(lái)過(guò)濾不同數(shù)據(jù)
我們過(guò)濾數(shù)據(jù)首先想到的方法是針對(duì)各個(gè)需求來(lái)定義一個(gè)個(gè)的方法及志。
例如片排,產(chǎn)品經(jīng)理給了你一個(gè)篩選出所有 id 大于 3 用戶的需求寨腔,可以定義如下getIdGreaterThan3
的方法。
public class CollectionStream {
public static void main(String[] args) {
// users 定義
final var newUsers = getIdGreaterThan3(users);
for (User user : newUsers) {
System.out.println(user);
}
}
public static List<User> getIdGreaterThan3(List<User> users) {
final var newUsers = new ArrayList<User>();
for (User user : users) {
if (user.id() > 3) {
newUsers.add(user);
}
}
return newUsers;
}
}
/*
輸出:
User[id=4, name=趙六, money=20000]
User[id=5, name=王強(qiáng), money=80000]
*/
第二天率寡,產(chǎn)品經(jīng)理要求你篩選出所有姓“王”的用戶的需求迫卢,定義getAllWang
方法:
public class CollectionStream {
public static void main(String[] args) {
// users 定義
final var newUsers = getAllWang(users);
for (User user : newUsers) {
System.out.println(user);
}
}
public static List<User> getAllWang(List<User> users) {
final var newUsers = new ArrayList<User>();
for (User user : users) {
if (user.name().startsWith("王")) {
newUsers.add(user);
}
}
return newUsers;
}
}
/*
輸出:
User[id=3, name=王五, money=10000]
User[id=5, name=王強(qiáng), money=80000]
*/
第三天,產(chǎn)品經(jīng)理要求你開(kāi)發(fā)所有錢大于 10000 的用戶的需求冶共,你瞅了瞅他乾蛤,寫(xiě)出了如下代碼:
public class CollectionStream {
public static void main(String[] args) {
// users 定義
final var newUsers = getRichPeople(users);
for (User user : newUsers) {
System.out.println(user);
}
}
public static List<User> getRichPeople(List<User> users) {
final var newUsers = new ArrayList<User>();
for (User user : users) {
if (user.money() > 10000) {
newUsers.add(user);
}
}
return newUsers;
}
}
/*
輸出:
User[id=4, name=趙六, money=20000]
User[id=5, name=王強(qiáng), money=80000]
*/
此時(shí)此刻,你會(huì)發(fā)現(xiàn)我們似乎寫(xiě)了很多重復(fù)的方法...
使用接口來(lái)代替重復(fù)操作
在 Java 世界中比默,對(duì)于相似的操作我們通常使用接口定義幻捏,對(duì)于不同的操作我們相應(yīng)的定義不同的實(shí)現(xiàn)類來(lái)實(shí)現(xiàn)不同的功能。
public class CollectionStream {
public static void main(String[] args) {
// users 定義
for (User user : getUsers(users, new JudgeIdGreaterThan3())) {
// 判斷ID是否大于3
System.out.println(user);
}
for (User user : getUsers(users, new JudgeIsWang())) {
// 判斷是否姓王
System.out.println(user);
}
for (User user : getUsers(users, new JudgeIsRich())) {
// 判斷是否有錢
System.out.println(user);
}
}
public static List<User> getUsers(List<User> users, Judge condition) {
final var newUsers = new ArrayList<User>();
for (User user : users) {
if (condition.test(user)) {
newUsers.add(user);
}
}
return newUsers;
}
public static class JudgeIdGreaterThan3 implements Judge {
@Override
public boolean test(User user) {
return user.id() > 3;
}
}
public static class JudgeIsWang implements Judge {
@Override
public boolean test(User user) {
return user.name().startsWith("王");
}
}
public static class JudgeIsRich implements Judge {
@Override
public boolean test(User user) {
return user.money() > 10000;
}
}
public interface Judge {
boolean test(User user);
}
}
當(dāng)然命咐,我們也可以使用匿名內(nèi)部類來(lái)實(shí)現(xiàn)同樣的功能。
使用 Java 8 提供的Predicate
接口
事實(shí)上谐岁,從 Java 8 開(kāi)始醋奠,JDK 提供了一個(gè)名為Predicate
的接口,其作用與上方自己寫(xiě)的Judge
接口類似伊佃。同時(shí)窜司,因?yàn)樗?strong>函數(shù)式接口,我們可以很輕松地使用 Lambda 表達(dá)式航揉。
public class CollectionStream {
public static void main(String[] args) {
// users 定義
for (User user : getUsers(users, user -> user.id() > 3)) {
// 判斷ID是否大于3
System.out.println(user);
}
for (User user : getUsers(users, user -> user.name().startsWith("王"))) {
// 判斷是否姓王
System.out.println(user);
}
for (User user : getUsers(users, user -> user.money() > 10000)) {
// 判斷是否有錢
System.out.println(user);
}
}
public static List<User> getUsers(List<User> users, Predicate<User> condition) {
final var newUsers = new ArrayList<User>();
for (User user : users) {
if (condition.test(user)) {
newUsers.add(user);
}
}
return newUsers;
}
}
總結(jié)
從最初編寫(xiě)一個(gè)一個(gè)獨(dú)立的方法塞祈,到后面自行開(kāi)發(fā)接口逐步地通用化,再到使用 Lambda 表達(dá)式帅涂,我們重復(fù)的工作被逐步逐步地簡(jiǎn)化议薪。
事實(shí)上,在 Java 推出Predicate
接口媳友,開(kāi)源世界早已對(duì)于集合操作有了簡(jiǎn)化斯议。例如以Google Guava為代表的第三方框架,以及以Groovy醇锚、Scala哼御、Kotlin為代表的編程語(yǔ)言。
Steam 核心知識(shí)
創(chuàng)建 Stream
使用 Stream.of() 創(chuàng)建
最簡(jiǎn)單的方法是使用Stream.of()
來(lái)創(chuàng)建 Stream:
Stream<String> foo = Stream.of("Java", "Python", "Kotlin", "JavaScript");
foo.forEach(System.out::println);
以上代碼創(chuàng)建了一個(gè)由 4 個(gè)編程語(yǔ)言組成的流焊唬,并使用forEach()
方法將其打印出來(lái)(forEach
方法的參數(shù)為Consumer<T>
函數(shù)式接口恋昼,可直接使用 Lambda 表達(dá)式)
使用數(shù)組創(chuàng)建
使用數(shù)組創(chuàng)建 Stream 可以使用Arrays.stream()
方法來(lái)創(chuàng)建。
String[] foo = new String[]{"Java", "Python", "Kotlin", "JavaScript"};
Stream<String> bar = Arrays.stream(foo);
bar.forEach(System.out::println);
使用集合框架創(chuàng)建
同樣赶促,Stream 也可以基于集合框架來(lái)創(chuàng)建液肌,Collection
接口提供了stream()
的抽象方法,使得Set
芳杏、List
矩屁、Map
等集合擁有創(chuàng)建 Stream 的能力辟宗。
這里以 List
為例:
List<String> foo = List.of("Java", "Python", "Kotlin", "JavaScript");
Stream<String> bar = foo.stream();
bar.forEach(System.out::println);
使用Supplier
創(chuàng)建
我們也可以通過(guò)Stream.generate(Supplier<? extends T> s)
方法來(lái)創(chuàng)建 Stream。這里參數(shù)要求為Supplier
吝秕,它同樣是個(gè)函數(shù)式接口泊脐。
// 以下事例均使用`鏈?zhǔn)讲僮鱜
Stream.generate(() -> new Random().nextInt(100))
.limit(10) // 此處使用`limit`來(lái)閑置元素個(gè)數(shù)
.forEach(System.out::println);
中間操作
中間操作是指調(diào)用方法以后,仍然返回Stream
對(duì)象烁峭。Java Stream 中容客,允許有多個(gè)中間操作。
map
Stream.map(Function<? super T,? extends R> mapper)
是將一個(gè)某個(gè)操作映射到 Stream 中每個(gè)元素上约郁。同樣缩挑,map
的參數(shù)為函數(shù)式接口。
例如鬓梅,如下代碼實(shí)現(xiàn)了對(duì)于每個(gè)元素進(jìn)行平方:
Stream.of(1, 2, 3, 4, 5)
.map(i -> i * i)
.forEach(System.out::println); // 1, 4, 9, 16, 25
map
方法也可以對(duì)于元素中的對(duì)象進(jìn)行操作供置,例如:
List.of("Java", "Kotlin", "JavaScript")
.stream()
.map(String::toUpperCase) // 將元素轉(zhuǎn)為大寫(xiě)
.forEach(System.out::println);
filter
Stream.filter(Predicate<? super T> predicate)
可以對(duì)于 Stream 中元素進(jìn)行過(guò)濾。
例如绽快,以下代碼將一組數(shù)字中所有偶數(shù)打印出來(lái):
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(i -> i % 2 == 0)
.forEach(System.out::println);
如果 Stream 中元素為對(duì)象芥丧,同樣可以進(jìn)行過(guò)濾。例如坊罢,如下代碼實(shí)現(xiàn)了將年齡為 18 歲以下的未成年人過(guò)濾:
record Person(String name, int age) { } // 需要使用 Java 16 及以上版本
List<Person> peoples = List.of(
new Person("張三", 30),
new Person("李四", 16),
new Person("王五", 18),
new Person("王強(qiáng)", 22),
new Person("小宋", 8)
);
peoples.stream()
.filter(it -> it.age() >= 18)
.forEach(System.out::println);
/*
輸出:
Person[name=張三, age=30]
Person[name=王五, age=18]
Person[name=王強(qiáng), age=22]
*/
parallel
通常情況下续担,對(duì) Stream 的元素進(jìn)行處理是單線程的,即一個(gè)一個(gè)元素進(jìn)行處理活孩。但是很多時(shí)候物遇,我們希望可以并行處理 Stream 的元素,因?yàn)樵谠財(cái)?shù)量非常大的情況憾儒,并行處理可以大大加快處理速度询兴。
record Person(String name, int age) { } // 需要使用 Java 16 及以上版本
List<Person> peoples = List.of(
new Person("張三", 30),
new Person("李四", 16),
new Person("王五", 18),
new Person("王強(qiáng)", 22),
new Person("小宋", 8)
);
peoples.stream()
.parallel() // 將普通 stream 轉(zhuǎn)換為并行 stream
.filter(it -> it.age() >= 18) // 并行篩選
.forEach(System.out::println);
sorted
Stream.sorted()
可以實(shí)現(xiàn)對(duì) Stream 中元素進(jìn)行排序,所排序的元素必須實(shí)現(xiàn)Comparable
航夺。當(dāng)然也可以在參數(shù)中填入自己的Comparator
蕉朵。
如下代碼對(duì)隨機(jī)數(shù)進(jìn)行從小到大的排序:
IntStream.of(5, 7, 3, 2, 6, 0, 9)
.sorted()
.forEach(System.out::println);
// 輸出:0 2 3 5 6 7 9
distinct
Stream.distinct()
可以對(duì)于 Stream 中的元素進(jìn)行去重:
IntStream.of(5, 8, 3, 4, 5, 3, 6, 9, 5, 3, 7)
.distinct()
.forEach(System.out::println);
// 輸出:5 8 3 4 6 9 7
skip
Stream.skip()
可以對(duì)于 Stream 中前幾個(gè)元素進(jìn)行跳過(guò)
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.skip(3)
.forEach(System.out::println);
// 輸出:4 5 6 7 8 9
limit
Stream.limit()
可以只保留前幾個(gè)元素:
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.limit(5)
.forEach(System.out::println);
// 輸出:1 2 3 4 5
concat
Stream.concat()
用于將兩個(gè) Stream 合并:
IntStream foo = IntStream.of(1, 2, 3);
IntStream bar = IntStream.of(4, 5, 6);
IntStream.concat(foo, bar)
.forEach(System.out::println);
// 輸出:1 2 3 4 5 6
終結(jié)操作
終結(jié)操作是指調(diào)用方法后,返回非Stream
的操作阳掐,包括void
始衅。Java Stream 中,只允許有一個(gè)終結(jié)操作缭保。
終結(jié)操作主要有如下方法:
forEach
:對(duì)于Stream
中每個(gè)元素進(jìn)行遍歷汛闸,常見(jiàn)用途如打印元素。count
/max
/min
:返回元素個(gè)數(shù)/最大值/最小值艺骂。anyMatch
/allMatch
/noneMatch
:任意一個(gè)符合/全部符合/都不符合給定的Predicate
條件返回true
诸老。findFirst
/findAny
:返回流中第一個(gè)/任意一個(gè)元素。-
??
collect
:幾乎可以將一個(gè)Stream
對(duì)象轉(zhuǎn)換為任何內(nèi)容钳恕,例如以下代碼可以將姓王的用戶篩選出來(lái)别伏,并轉(zhuǎn)換為 List 集合蹄衷。final var ls = users.stream() .filter(it -> it.name().startsWith("王")) .collect(Collectors.toList());
因?yàn)?code>collect方法較為復(fù)雜,有興趣可以自行閱讀 JDK 文檔厘肮。
IDEA 流調(diào)試器
IDEA 中內(nèi)置了一個(gè)名為Java Stream Debugger插件(如果沒(méi)有請(qǐng)確保自己為最新版的 IDEA愧口,或者嘗試前往 IDEA 插件市場(chǎng)安裝),該插件可以通過(guò)可視化的方式直觀地看到 Stream 的處理過(guò)程类茂。
使用方式:
在 Stream 流中打上斷點(diǎn)耍属;
啟動(dòng) Debug 模式;
-
斷點(diǎn)暫停后巩检,點(diǎn)擊 Debug 面板上的Trace Current Stream Chain按鈕(如圖所示)
`Trace Current Stream Chain` Button
該插件可以分步地將 Stream 操作以可視化的形式呈現(xiàn)出來(lái)(當(dāng)然也可以通過(guò)下方的Flat Mode按鈕在同一個(gè)窗口中看到所有操作)
演示 1 - filter
public class CollectionStream {
public record User(Integer id, String name, Integer money) { }
public static void main(String[] args) {
final var users = Arrays.asList(
new User(1, "張三", 200),
new User(2, "李四", 200),
new User(3, "王五", 10000),
new User(4, "趙六", 20000),
new User(5, "王強(qiáng)", 80000)
);
users.stream()
.filter(it -> it.money() > 10000)
.collect(Collectors.toList());
}
}
演示 2 - distinct
public class CollectionStream {
public static void main(String[] args) {
final var list = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 1, 2);
list.stream()
.distinct()
.forEach(System.out::println);
}
}
演示 3 - sorted
public class CollectionStream {
public static void main(String[] args) {
final var list = Arrays.asList(6, 4, 3, 5, 6, 7, 8, 2);
list.stream()
.sorted()
.forEach(System.out::println);
}
}
演示 4 - map
public class CollectionStream {
public record User(Integer id, String name, Integer money) { }
public static void main(String[] args) {
final var users = Arrays.asList(
new User(1, "張三", 200),
new User(2, "李四", 200),
new User(3, "王五", 10000),
new User(4, "趙六", 20000),
new User(5, "王強(qiáng)", 80000)
);
users.stream()
.filter(it -> it.name().startsWith("王"))
.map(User::name)
.forEach(System.out::println);
}
}