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.iterate 或 Stream.generate 而不通过 limit 限制,程序可能会挂起或“吃光”内存。
Stream.generate(() -> 1).forEach(System.out::println); // 永远不会结束!
错误 №2:zip 时未正确处理不同长度。 如果一个列表比另一个更长,就应该按最小长度遍历,否则会得到 IndexOutOfBoundsException。
错误 №3:尝试对普通的 Stream<T> 做 zip。 普通流无法按索引访问。实用的 zip 更常对 List 来实现。
错误 №4:在生成流的同时修改集合。 如果在流处理期间修改集合,可能会得到 ConcurrentModificationException。请生成新数据——不要在“途中”修改旧数据。
错误 №5:丢失顺序。 如果顺序很重要(例如进行 zip 时),请使用 List 而不是 Set——否则元素的顺序将不可预测。
GO TO FULL VERSION