CodeGym /课程 /JAVA 25 SELF /zip(拉链),流的生成(iterate、generate)

zip(拉链),流的生成(iterate、generate)

JAVA 25 SELF
第 32 级 , 课程 3
可用

1. 初识 zip

在编程中,术语 zip(也称“拉链”“缝合”)指的是一种操作:从两个(或更多)列表中取元素,并把它们合并为一条由配对组成的流——每个列表各取一个元素。若你熟悉 Python,其中的函数 zip就是做这件事的。

示例:

  • 有一个姓名列表: ["Anya", "Boris", "Vika"]
  • 有一个年龄列表: [20, 25, 19]
  • “拉链”之后得到: [("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:字符串与字符的“拉链”

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", "Maxim", "Tanya", "Sergey");

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。 普通流无法按索引访问。实用的 zip 更常对 List 来实现。

错误 №4:在生成流的同时修改集合。 如果在流处理期间修改集合,可能会得到 ConcurrentModificationException。请生成新数据——不要在“途中”修改旧数据。

错误 №5:丢失顺序。 如果顺序很重要(例如进行 zip 时),请使用 List 而不是 Set——否则元素的顺序将不可预测。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION