1. Làm quen với zip
Trong lập trình, thuật ngữ zip (hay “zip hóa”, “ghép cặp”) là một thao tác lấy hai (hoặc nhiều) danh sách và hợp nhất chúng thành một luồng các cặp: mỗi cặp gồm một phần tử từ mỗi danh sách. Nếu bạn quen với Python, ở đó có hàm zip làm đúng điều này.
Ví dụ:
- Có danh sách tên: ["Anya", "Boris", "Vika"]
- Có danh sách tuổi: [20, 25, 19]
- Sau khi “zip” sẽ ra: [("Anya", 20), ("Boris", 25), ("Vika", 19)]
Điều này hữu ích khi cần xử lý đồng bộ hai collection — ví dụ tạo đối tượng trong đó tên và tuổi đi cùng nhau.
Tại sao trong Stream API không có zip?
Trong Stream API tiêu chuẩn (trước Java 22) không có phương thức zip. Lý do là stream có thể vô hạn, collection có độ dài khác nhau, và không phải lúc nào cũng rõ ràng nên xử lý thế nào nếu một collection dài hơn collection kia. Nhưng trên thực tế, zip lại thường rất cần.
2. Cài đặt zip trong Java: sống thế nào khi không có phương thức tích hợp sẵn
Cách đơn giản nhất — dùng chỉ số
Nếu bạn có hai danh sách và chắc chắn chúng là các collection “thông thường” (ví dụ List<A>, List<B>), bạn có thể dùng chỉ số:
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 + ")"; }
}
}
Kết quả:
Anya (20)
Boris (25)
Vika (19)
Điều gì đang diễn ra?
- Lấy kích thước nhỏ nhất của hai danh sách — để không vượt quá phạm vi.
- Dùng IntStream.range(0, size) — tạo luồng chỉ số.
- Với mỗi chỉ số, lấy một phần tử từ mỗi danh sách và “ghép cặp” chúng.
- Thu kết quả về danh sách qua Collectors.toList().
Có thể làm zip cho Stream<T> không?
Về kỹ thuật — có, nhưng chỉ tiện khi cả hai stream là hữu hạn và đằng sau chúng là cấu trúc có truy cập nhanh theo chỉ số (tức thực chất vẫn là List). Với “luồng thực sự” (ví dụ vô hạn), hiện thực zip đúng đắn phức tạp hơn và cần thêm logic.
Các lựa chọn thay thế: thư viện bên thứ ba
Nếu muốn một zip “sẵn dùng”, bạn có thể dùng các thư viện:
- org.apache.commons.lang3.Streams.zip (Apache Commons Lang 3.10+)
- io.vavr.collection.Stream.zip (Vavr)
- com.codepoetics.protonpack.StreamUtils.zip (ProtonPack)
Trong khóa học này, chúng ta tuân theo thư viện chuẩn, nên sẽ xem các cách “tự làm”.
3. Ví dụ thực tế sử dụng zip
Ví dụ 1. Duyệt đồng bộ hai collection (cộng phần tử)
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]
Ví dụ 2. Zip chuỗi và ký tự
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.]
Trực quan hóa (sơ đồ)
names: [Anya] [Boris] [Vika]
ages: [20 ] [25 ] [19 ]
| | |
zip ---> (Anya,20) (Boris,25) (Vika,19)
4. Stream.iterate và Stream.generate — sinh các luồng mới
Đôi khi ta không chỉ xử lý các collection đã có, mà còn cần tạo các dãy mới “tức thời”. Để làm điều đó, Stream API có hai phương thức hữu ích:
- Stream.iterate — tạo dãy theo quy tắc (ví dụ cấp số cộng).
- Stream.generate — tạo luồng trong đó mỗi phần tử được tính qua Supplier (ví dụ số ngẫu nhiên, thời gian hiện tại, v.v.).
Stream.iterate
Cú pháp:
Stream.iterate(seed, unaryOperator)
- seed — giá trị khởi đầu;
- unaryOperator — hàm tính phần tử kế tiếp.
Ví dụ 1: Cấp số cộng
Stream<Integer> numbers = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
numbers.limit(5).forEach(System.out::println);
// Sẽ in: 0 2 4 6 8
Ví dụ 2: Sinh ngày tháng
import java.time.LocalDate;
Stream<LocalDate> days = Stream.iterate(LocalDate.now(), date -> date.plusDays(1));
days.limit(3).forEach(System.out::println);
// Ví dụ: 2024-06-09, 2024-06-10, 2024-06-11
Ví dụ 3: Luồng vô hạn — đừng quên limit!
Stream<Integer> endless = Stream.iterate(1, n -> n * 2);
endless.limit(5).forEach(System.out::println); // 1 2 4 8 16
Trong Java 9+ xuất hiện biến thể nạp chồng với predicate-điều kiện:
Stream.iterate(0, n -> n < 10, n -> n + 2)
.forEach(System.out::println); // 0 2 4 6 8
Stream.generate
Cú pháp:
Stream.generate(Supplier<T>)
Mỗi phần tử được tính bằng cách gọi Supplier.get().
Ví dụ 1: Số ngẫu nhiên
import java.util.Random;
Random random = new Random();
Stream<Integer> randoms = Stream.generate(random::nextInt);
randoms.limit(5).forEach(System.out::println);
Ví dụ 2: Sinh các giá trị giống nhau
Stream<String> stars = Stream.generate(() -> "*");
stars.limit(4).forEach(System.out::print); // ****
Ví dụ 3: Định danh (ID) duy nhất
import java.util.UUID;
Stream<String> uuids = Stream.generate(() -> UUID.randomUUID().toString());
uuids.limit(3).forEach(System.out::println);
Trực quan hóa (sơ đồ)
Stream.iterate:
[seed] -> op() -> op() -> op() -> ...
n n+1 n+2 n+3
Stream.generate:
Supplier() -> Supplier() -> Supplier() -> ...
val1 val2 val3
5. Ví dụ sử dụng: sinh dữ liệu cho ứng dụng
Giả sử ta có lớp Student:
class Student {
String name;
int age;
Student(String name, int age) { this.name = name; this.age = age; }
public String toString() { return name + " (" + age + ")"; }
}
Ví dụ 1. Sinh sinh viên test
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)
Ví dụ 2. Sinh sinh viên ngẫu nhiên
Random random = new Random();
List<String> pool = Arrays.asList("Ira", "Oleg", "Maksim", "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);
// Ví dụ: Tanya (19), Oleg (21), Ira (20)
Ví dụ 3. Sinh dãy ngày cho báo cáo
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
So sánh: khi nào dùng zip, iterate, generate
- zip — khi cần xử lý đồng bộ hai (hoặc nhiều) danh sách/luồng, ghép phần tử theo chỉ số.
- iterate — khi cần dãy theo quy tắc cho trước (số, ngày tháng, bước).
- generate — khi mỗi phần tử được tính độc lập (giá trị ngẫu nhiên, ID duy nhất).
7. Lỗi thường gặp khi làm việc với zip và sinh luồng
Lỗi số 1: Luồng không giới hạn mà không dùng limit. Nếu bạn dùng Stream.iterate hoặc Stream.generate mà không giới hạn bằng limit, chương trình có thể treo hoặc “ăn” hết bộ nhớ.
Stream.generate(() -> 1).forEach(System.out::println); // Sẽ không bao giờ kết thúc!
Lỗi số 2: Xử lý độ dài khác nhau khi zip không đúng. Nếu một danh sách dài hơn danh sách kia, bạn phải đi theo độ dài nhỏ nhất, nếu không sẽ nhận IndexOutOfBoundsException.
Lỗi số 3: Cố gắng zip các Stream<T> thông thường. Các stream thông thường không có truy cập theo chỉ số. zip thực dụng thường được làm cho List.
Lỗi số 4: Sửa đổi collection trong khi sinh luồng. Nếu thay đổi collection trong lúc stream đang duyệt, bạn có thể nhận ConcurrentModificationException. Hãy sinh dữ liệu mới — đừng thay đổi dữ liệu cũ ngay “trên đường duyệt”.
Lỗi số 5: Mất thứ tự. Nếu thứ tự quan trọng (ví dụ với zip), hãy dùng List, không phải Set — nếu không, thứ tự phần tử sẽ khó đoán.
GO TO FULL VERSION