CodeGym /課程 /JAVA 25 SELF /Zip 合併(zip)、串流生成(iterate、generate)

Zip 合併(zip)、串流生成(iterate、generate)

JAVA 25 SELF
等級 32 , 課堂 3
開放

1. 認識 zip

在程式設計中,zip(或稱「zip 合併」、「縫合」)是一種操作:從兩個(或更多)清單各取一個元素,將它們組成一個配對的串流。若你熟悉 Python,其中的 zip 函式正是做這件事。

範例:

  • 有一個姓名清單:["Anya", "Boris", "Vika"]
  • 有一個年齡清單:[20, 25, 19]
  • 「zip 合併」後得到:[("Anya", 20), ("Boris", 25), ("Vika", 19)]

當你需要同步處理兩個集合時很方便——例如建立同時包含姓名與年齡的物件。

為什麼 Stream API 沒有 zip?
在標準的 Stream API(到 Java 22 為止)中並沒有 zip 方法。原因是串流可能是無窮的、集合長度可能不同,而當其中一個集合較長時該怎麼處理並不總是明確。不過在實務上,zip 仍然很常用。

2. 在 Java 中實作 zip:沒有內建方法該怎麼辦

最簡單的作法——透過索引

如果你有兩個清單,並且確定它們是「一般」集合(例如 List<A>List<B>),可以利用索引來實作:

import java.util.*;
import java.util.stream.*;

public class ZipExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Anya", "Boris", "Vika");
        List<Integer> ages = Arrays.asList(20, 25, 19);

        int size = Math.min(names.size(), ages.size());

        List<Person> people = IntStream.range(0, size)
            .mapToObj(i -> new Person(names.get(i), ages.get(i)))
            .collect(Collectors.toList());

        people.forEach(System.out::println);
    }

    static class Person {
        String name;
        int age;
        Person(String name, int age) { this.name = name; this.age = age; }
        public String toString() { return name + " (" + age + ")"; }
    }
}

輸出:

Anya (20)
Boris (25)
Vika (19)

發生了什麼?

  • 先取兩個清單的最小長度——避免越界。
  • 使用 IntStream.range(0, size)——建立索引的串流。
  • 對每個索引各取一個元素並「縫合」起來。
  • 透過 Collectors.toList() 收集成清單。

能否對 Stream<T> 做 zip?

技術上可以,但只有在兩個串流都是有限的,且其背後有可用索引快速存取的結構時才方便(也就是實際上是 List)。對「真正的」串流(例如無窮串流)而言,正確的 zip 實作更複雜,需要額外的邏輯。

替代方案:第三方函式庫

如果想要「現成」的 zip,可以使用以下函式庫:

  • org.apache.commons.lang3.Streams.zip (Apache Commons Lang 3.10+)
  • io.vavr.collection.Stream.zip (Vavr)
  • com.codepoetics.protonpack.StreamUtils.zip (ProtonPack)

在本課程我們偏好標準函式庫,因此著重於「手動」方式。

3. zip 的實務使用範例

範例 1:同步遍歷兩個集合(元素加總)

List<Integer> a = Arrays.asList(1, 2, 3, 4);
List<Integer> b = Arrays.asList(10, 20, 30, 40);

List<Integer> sums = IntStream.range(0, Math.min(a.size(), b.size()))
    .mapToObj(i -> a.get(i) + b.get(i))
    .collect(Collectors.toList());

System.out.println(sums); // [11, 22, 33, 44]

範例 2:字串與字元的 zip 合併

String[] words = {"cat", "dog", "fox"};
char[] marks = {'!', '?', '.'};

List<String> zipped = IntStream.range(0, Math.min(words.length, marks.length))
    .mapToObj(i -> words[i] + marks[i])
    .collect(Collectors.toList());

System.out.println(zipped); // [cat!, dog?, fox.]

視覺化(示意圖)

names:   [Anya] [Boris] [Vika]
ages:    [20 ] [25   ] [19  ]
          |      |      |
zip ---> (Anya,20) (Boris,25) (Vika,19)

4. Stream.iterate 與 Stream.generate——生成新的串流

有時我們不僅要處理既有的集合,還需要即時建立新的序列。為此,Stream API 中有兩個實用的方法:

  • Stream.iterate——依規則建立序列(例如等差數列)。
  • Stream.generate——建立一個由 Supplier 計算每個元素的串流(例如隨機數、目前時間等)。

Stream.iterate

語法:

Stream.iterate(seed, unaryOperator)
  • seed——起始值;
  • unaryOperator——用來計算下一個元素的函式。

範例 1:等差數列

Stream<Integer> numbers = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
numbers.limit(5).forEach(System.out::println);
// 輸出:0 2 4 6 8

範例 2:日期生成

import java.time.LocalDate;

Stream<LocalDate> days = Stream.iterate(LocalDate.now(), date -> date.plusDays(1));
days.limit(3).forEach(System.out::println);
// 例如:2024-06-09, 2024-06-10, 2024-06-11

範例 3:無窮串流——別忘了加上 limit!

Stream<Integer> endless = Stream.iterate(1, n -> n * 2);
endless.limit(5).forEach(System.out::println); // 1 2 4 8 16

在 Java 9+ 中 出現了帶有條件述詞的多載版本:

Stream.iterate(0, n -> n < 10, n -> n + 2)
    .forEach(System.out::println); // 0 2 4 6 8

Stream.generate

語法:

Stream.generate(Supplier<T>)

每個元素都透過呼叫 Supplier.get() 來計算。

範例 1:隨機數

import java.util.Random;

Random random = new Random();
Stream<Integer> randoms = Stream.generate(random::nextInt);
randoms.limit(5).forEach(System.out::println);

範例 2:生成相同的值

Stream<String> stars = Stream.generate(() -> "*");
stars.limit(4).forEach(System.out::print); // ****

範例 3:唯一識別碼

import java.util.UUID;

Stream<String> uuids = Stream.generate(() -> UUID.randomUUID().toString());
uuids.limit(3).forEach(System.out::println);

視覺化(示意圖)

Stream.iterate:

[seed] -> op() -> op() -> op() -> ...
       n      n+1    n+2    n+3

Stream.generate:

Supplier() -> Supplier() -> Supplier() -> ...
    val1         val2          val3

5. 使用範例:為應用程式生成資料

假設我們有一個 Student 類別:

class Student {
    String name;
    int age;
    Student(String name, int age) { this.name = name; this.age = age; }
    public String toString() { return name + " (" + age + ")"; }
}

範例 1:生成測試學生資料

List<String> names = Arrays.asList("Anya", "Boris", "Vika", "Gleb", "Dasha");
Stream<Student> students = IntStream.range(0, names.size())
    .mapToObj(i -> new Student(names.get(i), 18 + i));

students.forEach(System.out::println);
// Anya (18), Boris (19), Vika (20), Gleb (21), Dasha (22)

範例 2:生成隨機學生

Random random = new Random();
List<String> pool = Arrays.asList("Ira", "Oleg", "Maksim", "Tanya", "Sergei");

Stream<Student> randomStudents = Stream.generate(() ->
    new Student(
        pool.get(random.nextInt(pool.size())),
        18 + random.nextInt(5)
    )
);

randomStudents.limit(3).forEach(System.out::println);
// 例如:Tanya (19), Oleg (21), Ira (20)

範例 3:為報表生成日期序列

import java.time.LocalDate;

Stream<LocalDate> dates = Stream.iterate(LocalDate.of(2024, 6, 1), d -> d.plusDays(1));
dates.limit(5).forEach(System.out::println);
// 2024-06-01, 2024-06-02, ..., 2024-06-05

比較:何時使用 zip、iterate、generate

  • zip——當需要同步處理兩個(或更多)清單/串流,並按索引合併元素時。
  • iterate——當需要依規則產生序列(數字、日期、步驟)。
  • generate——當每個元素彼此獨立計算(隨機值、唯一 ID)。

7. 使用 zip 與生成串流時的常見錯誤

錯誤 №1:未加 limit 的無界串流。 若你使用 Stream.iterateStream.generate 卻沒有透過 limit 加以限制,程式可能會卡住或吃光記憶體。

Stream.generate(() -> 1).forEach(System.out::println); // 永遠不會結束!

錯誤 №2:zip 時未妥善處理長度不一。 若一個清單比另一個長,應以最小長度為準,否則會得到 IndexOutOfBoundsException

錯誤 №3:嘗試對一般的 Stream<T> 做 zip。 一般串流沒有索引式存取。實務上更常針對 List 來做 zip

錯誤 №4:在生成串流時修改集合。 若在串流處理期間修改集合,可能導致 ConcurrentModificationException。請生成新資料——不要即時修改舊資料。

錯誤 №5:順序遺失。 若順序很重要(例如進行 zip 時),請使用 List 而非 Set——否則元素順序將不可預期。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION