Stream 自 Java 8 開始支援,對 「資料集合」(Collection) 作 「管線式處理」 的工具。這對大資料有很多方便,不過也請別走火入魔,因為效能不會因此而提升。
目的
假設有一個類別 Member 如下
class Membert{
public int age;
public String name;
public Member(int age, String name){
this.age = age;
this.name = name;
}
}
然後使用如下收集數十萬,百萬個會員資料。
List<Member> users=new ArrayList<>(); users.add(new Member(18, "Thomas")); ..........
如果我們要將 age >=18 的會員名字收集起來,在 7版及之前的版本需使用如下
List<String> names=new ArrayList<>();
for (var u : users){
if (u.age>=10){
names.add(u.name);
}
}
若使用 7 版後的 Stream,可以精簡如下
List<String> names = users.stream()
.filter(x->x.age>=18)
.map(x->x.name)
.toList();
System.out.println(names);
乍看之下,Stream 是為了把 for 迴圈拿掉,讓操作類似 Python 的 Numpy。其實這還真的是 Stream 的目地,少了 for 及 if,提高程式的可讀性。但效能上,二者的效能幾乎是一樣的。
三個核心
Stream 有三個核心概念,分別說明如下
資料來源
可以由 List.stream()、Arrays.stream(陣列) 及 Stream.of() 三種方法產生 Stream。
中介操作
有 filter、map、sorted、distinct、limit,會回傳新的 Stream,所以可以一直接。
終端操作
真正的執行,回傳結果。常看到的有 forEach、peek、toList、collect、count、findFirst、anyMatch。
資料來源
直接由 List.stream() 產生 Stream
Stream<Member> stream = users.stream();
Arrays.stream(陣列) 也可以產生 Stream,但 users.toArray() 裏要加 new Member[0] 讓 JVM 知道這個 Array 的型態,這是 Java 泛型設計的歷史包袱,寫法怪異,所以比較少用。
Stream<Member> stream=Arrays.stream(users.toArray(new Member[0]));
Stream.of() 亦可產生 Stream,請看如下代碼
Stream<Member> stream=Stream.of(
new Member(20,"thomas"),
new Member(18,"john"),
new Member(10,"kevin"));
List<String> names=stream
.filter(x->x.age>=18)
.map(x->x.name)
.toList();
System.out.println(names);
一般 Stream 不用 close 。但如果有 I/O 類型的 Stream,則需 close。
try(Stream stream=Files.walk(Paths.get(path));){
......
}
中介操作
filter 篩選,要傳入 Predicate<? super T> predicate 物件。Predicate 回傳 true/false。底下會過瀘掉小於 18 的值,如下所示。
List<Integer>ages=new ArrayList<>(Arrays.asList(10,20,30,40,50)); List<Integer> ages_filter=ages.stream() .filter(x->x>=18) .collect(Collectors.toList()); System.out.println(ages_filter);
map 轉換,要傳入 Function<? super T, ? extends R> mapper 物件(有一個回傳值)。
經過過瀘後,如果要傳回另一個值,可用 map,如下所示。
List<Integer> ages = new ArrayList<>(Arrays.asList(10,20,30,40,50));
List<Integer> age = ages.stream()
.filter(x->x>=18)
.map(x->"age:%s".formatted(x))
.collect(Collectors.toList());
System.out.println(age);
要傳回的資料若是由方法產生,可以指定 「類別:方法」取得,比如 Member::getName。注意,方法不能加 ()。
List<Integer> ages = new ArrayList<>(Arrays.asList(10,20,30,40,50));
List<Integer> age = ages.stream()
.filter(x->x>=18)
.map(Member::getName)
.collect(Collectors.toList());
System.out.println(age);
sorted 要傳入 Comparator<? super T> comparator 物件
底下代碼中,若要降冪,則加入 Comparator.reverseOrder。若是升冪,則不用傳入任何資料。
List<Integer>ages=new ArrayList<>(Arrays.asList(10,40,20,30,50)); List<Integer> ages_filter=ages.stream() .filter(x->x>=18) .sorted(Comparator.reverseOrder()) .collect(Collectors.toList()); System.out.println(ages_filter);
distinc 會去除重複的資料,不需傳入任何參數
List<Integer>ages=new ArrayList<>(Arrays.asList(20,40,20,30,50));
List<Integer> ages_filter=ages.stream()
.filter(x->x>=18)
.sorted(Comparator.reverseOrder())
.distinct()
.collect(Collectors.toList());
結果 :
[50, 40, 30, 20]
limit() 則會限制返回特定的數量。比如 limit(2) 則只會傳出前二個數量。
終端操作
forEach 要傳入 Comsumer<? super T> action 物件,無傳回值
collect 要傳入 Collector<? super T, A, R> collector 物件
anyMatch 要傳入 Predicate<? super T> predicate 物件
.collect(Collector.toList()) 回傳一個可變的 List。toList() 回傳不可變的 List,是 Java 16 才新加的。
矛盾
終端操作的 forEach 跟 collect 不能同時使用。
蒙地卡羅求 π 值
底下的代碼可以利用 stream 特性,求請 π 值
package monte;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Monte {
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
Random rand=new Random();
long batch=30_000_000L;
var epochs=3000;
double incircle=0;
for(int epoch=1;epoch<=epochs;epoch++){
List<Double> x = rand
.doubles(batch)
.boxed()
.collect(Collectors.toCollection(ArrayList::new));
x.replaceAll(n->n*n);
List<Double> y = rand
.doubles(batch)
.mapToObj(i->i*i)
.collect(Collectors.toCollection(ArrayList::new));
List<Double> dist = IntStream.range(0, x.size())
.mapToObj(i->Math.sqrt(x.get(i)+y.get(i)))
.filter(i->i<=1)
.collect(Collectors.toCollection(ArrayList::new));
//dist.removeIf(n->n>1);
incircle+=dist.size();
var area=incircle/(epoch*batch);
var pi=4*area;
System.out.printf("Epoch:%03d, %.10f\n", epoch, pi);
}
}
}
上述的 .collect(Collectors.toCollection(ArrayList::new)); 跟 .collect(Collectors.toList()); 都可以。
toCollection 可以保証傳回的是 ArrayList。
toList() 目前也是傳回 ArrayList,但在未來則可能會改成其它 List。
