CodeGym /Các khóa học /JAVA 25 SELF /Zip (zip), tạo luồng (iterate, generate)

Zip (zip), tạo luồng (iterate, generate)

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

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.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION