Stream

      在〈Stream〉中尚無留言

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。

 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *