CodeGym /Các khóa học /JAVA 25 SELF /Các phép toán union, intersection, difference

Các phép toán union, intersection, difference

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

1. Phép toán union (hợp của tập hợp)

Trong Java, interface Set<T> được dùng để biểu diễn tập hợp. Khác với danh sách (List), tập hợp bảo đảm tính duy nhất của phần tử và thường không quan tâm đến thứ tự (trừ khi dùng các hiện thực đặc biệt như LinkedHashSet). Những hiện thực phổ biến nhất — HashSetTreeSet. Nhiệm vụ chính của chúng — xác định nhanh sự có mặt của phần tử và đảm bảo không có bản sao.

Khi nào cần dùng tập hợp?

  • Khi tính duy nhất quan trọng: ví dụ, danh sách tất cả người dùng duy nhất đã truy cập trang web.
  • Khi cần kiểm tra nhanh sự có mặt của phần tử: phương thức contains của HashSet thường chạy trong thời gian hằng.
  • Khi cần thực hiện các phép toán điển hình trên tập: hợp, giao, hiệu.

Union là phép hợp của hai hoặc nhiều tập hợp: kết quả chứa tất cả phần tử từ cả hai tập nguồn (không trùng lặp).

Ví dụ thực tế

Giả sử chúng ta có hai tập sinh viên tham gia các câu lạc bộ “Robotics” và “Lập trình”:

Set<String> robotics = Set.of("Anya", "Boris", "Vika");
Set<String> programming = Set.of("Vika", "Gleb", "Dasha");

Cần lấy tập tất cả sinh viên tham gia ít nhất một câu lạc bộ.

Giải pháp với Stream API

Cách trực tiếp nhất — hợp hai stream và thu thập chúng vào Set:

Set<String> all = Stream.concat(
        robotics.stream(),
        programming.stream()
    ).collect(Collectors.toSet());

System.out.println(all); // [Anya, Boris, Vika, Gleb, Dasha]

Giải thích:

  • Stream.concat hợp hai stream.
  • collect(Collectors.toSet()) thu thập phần tử vào một tập (tự động loại bỏ trùng lặp).

Phương án khác: nhiều hơn hai tập

Nếu có ba câu lạc bộ trở lên, dùng Stream.offlatMap:

Set<String> math = Set.of("Zhenya", "Vika", "Boris");

Set<String> all = Stream.of(robotics, programming, math)
    .flatMap(Set::stream)
    .collect(Collectors.toSet());

System.out.println(all); // [Anya, Boris, Vika, Gleb, Dasha, Zhenya]

Vì sao lại là Set?
Set tự động loại bỏ trùng lặp. Nếu thu thập vào List, cùng một tên có thể xuất hiện nhiều lần.

2. Phép toán intersection (giao của tập hợp)

Intersection là các phần tử có mặt đồng thời ở cả hai tập hợp.

Ví dụ thực tế

Tìm sinh viên tham gia cả “Robotics” và “Lập trình”:

Set<String> robotics = Set.of("Anya", "Boris", "Vika");
Set<String> programming = Set.of("Vika", "Gleb", "Dasha");

Giải pháp với Stream API:

Set<String> both = robotics.stream()
    .filter(programming::contains)
    .collect(Collectors.toSet());

System.out.println(both); // [Vika]

Giải thích:
Chúng ta duyệt qua tất cả thành viên của “Robotics” và lọc chỉ những người cũng có trong “Lập trình”. Kết quả là một tập tên có ở cả hai câu lạc bộ.

Cách khác (không dùng Stream API)

Có thể dùng phương thức tích hợp retainAll (nó thay đổi tập hiện tại):

Set<String> intersection = new HashSet<>(robotics);
intersection.retainAll(programming);
System.out.println(intersection); // [Vika]

Tuy nhiên, trong phạm vi chủ đề Stream API, chúng ta nhấn mạnh vào streams.

3. Phép toán difference (hiệu của tập hợp)

Difference là các phần tử của tập thứ nhất không có trong tập thứ hai.

Ví dụ thực tế

Tìm sinh viên chỉ tham gia “Robotics”, nhưng không tham gia “Lập trình”:

Set<String> robotics = Set.of("Anya", "Boris", "Vika");
Set<String> programming = Set.of("Vika", "Gleb", "Dasha");

Giải pháp với Stream API:

Set<String> onlyRobotics = robotics.stream()
    .filter(name -> !programming.contains(name))
    .collect(Collectors.toSet());

System.out.println(onlyRobotics); // [Anya, Boris]

Giải thích:
Chúng ta lọc các thành viên của “Robotics”, chỉ giữ những người không có trong “Lập trình”.

Cách khác (không dùng Stream API)

Set<String> difference = new HashSet<>(robotics);
difference.removeAll(programming);
System.out.println(difference); // [Anya, Boris]

4. Bài tập thực hành: xử lý danh sách người dùng

Bài 1: Tìm sinh viên chỉ tham gia đúng một câu lạc bộ

Cần biết ai chỉ tham gia “Robotics” hoặc chỉ “Lập trình”, nhưng không phải cả hai. Đây là hiệu đối xứng (xor cho tập hợp):

Set<String> onlyOne = Stream.concat(
        robotics.stream().filter(name -> !programming.contains(name)),
        programming.stream().filter(name -> !robotics.contains(name))
    ).collect(Collectors.toSet());

System.out.println(onlyOne); // [Anya, Boris, Gleb, Dasha]

Bài 2: Danh sách tất cả sinh viên duy nhất từ nhiều câu lạc bộ

Set<String> all = Stream.of(robotics, programming, math)
    .flatMap(Set::stream)
    .collect(Collectors.toSet());

System.out.println(all); // [Anya, Boris, Vika, Gleb, Dasha, Zhenya]

Bài 3: Tìm sinh viên không tham gia bất kỳ câu lạc bộ nào

Giả sử chúng ta có danh sách tất cả học sinh trong lớp:

Set<String> allStudents = Set.of("Anya", "Boris", "Vika", "Gleb", "Dasha", "Zhenya", "Igor’", "Katya");

Cần biết ai không tham gia bất kỳ câu lạc bộ nào:

Set<String> attendees = Stream.of(robotics, programming, math)
    .flatMap(Set::stream)
    .collect(Collectors.toSet());

Set<String> notInAny = allStudents.stream()
    .filter(name -> !attendees.contains(name))
    .collect(Collectors.toSet());

System.out.println(notInAny); // [Igor’, Katya]

5. Lưu ý quan trọng: equals, hashCode và hiệu năng

Vì sao cần triển khai đúng equals và hashCode?

Mọi thao tác với tập hợp (Set) đều phụ thuộc vào tính đúng đắn của các phương thức equalshashCode. Nếu bạn lưu trữ các đối tượng của lớp riêng (ví dụ, Student), hãy chắc chắn ghi đè các phương thức này, nếu không việc so sánh sẽ hoạt động không chính xác.

Ví dụ:

class Student {
    String name;
    int age;

    // Đừng quên ghi đè equals và hashCode!
}

Nếu không làm điều đó, hai sinh viên có cùng tên và tuổi sẽ bị coi là các đối tượng khác nhau đối với Set.

Vì sao nên dùng Set thay vì List?

  • Toán tử containsSet hoạt động nhanh (thường trong thời gian hằng).
  • List, việc tìm phần tử diễn ra trong thời gian tuyến tính, có thể là vấn đề nghiêm trọng với các bộ sưu tập lớn.
  • Với các phép toán trên tập hợp (union, intersection, difference), Set hiệu quả và hợp lý hơn nhiều.

6. Lỗi thường gặp khi làm việc với các phép toán trên tập hợp

Lỗi №1: Dùng List thay cho Set cho các phép toán trên tập hợp. Nếu bạn thu thập phần tử vào List, các bản sao sẽ không bị loại bỏ, và phép contains hoạt động chậm. Với union/intersection/difference hãy dùng Set.

Lỗi №2: Không triển khai equals/hashCode cho đối tượng. Nếu bạn lưu trong Set các đối tượng của lớp riêng mà không ghi đè equalshashCode, thì phép giao và hiệu sẽ hoạt động “kỳ lạ” — các đối tượng thực chất giống nhau sẽ không được coi là bằng nhau.

Lỗi №3: Sửa đổi collection trong khi đang stream. Nếu bạn ngay trong stream cố gắng thay đổi Set nguồn (ví dụ, thêm hoặc xóa phần tử), bạn sẽ nhận ConcurrentModificationException. Hãy luôn làm việc với một tập mới.

Lỗi №4: Mất thứ tự một cách không rõ ràng. HashSet không bảo đảm thứ tự phần tử. Nếu thứ tự quan trọng — hãy dùng LinkedHashSet hoặc TreeSet.

Lỗi №5: Dùng Stream.concat cho nhiều hơn hai collection. Stream.concat chỉ hợp hai stream. Với số lượng lớn hơn, hãy dùng Stream.of(...)flatMap.

Lỗi №6: Lỗi với null. Tập hợp không “ưa” các giá trị null, đặc biệt nếu bạn dùng Set.of(...) — nó không chấp nhận null. Để làm việc với null, hãy dùng các hiện thực khác hoặc lọc trước.

1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Phát hiện các nghi phạm duy nhất từ nhiều nguồn 🕵️‍♂️
Phát hiện các nghi phạm duy nhất từ nhiều nguồn 🕵️‍♂️
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Lập danh mục hiện vật từ nhiều cuộc khai quật khảo cổ 🏺
Lập danh mục hiện vật từ nhiều cuộc khai quật khảo cổ 🏺
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION