1. 소개
현실적인 예로 시작해 봅시다. 당신이 파티를 열고 초대 손님 명단을 만든다고 상상해 보세요. 초대장을 보냈는데, 같은 사람이 명단에 두 번(심지어 세 번 — 파티를 정말 좋아하니까요!) 들어가 있음을 알게 됩니다. 일반적인 리스트(List)를 사용하면 이런 중복이 쉽게 생길 수 있습니다. 하지만 같은 손님을 두 번 추가하지 못하게 하는 컬렉션이 있다면 — 훨씬 편해지겠죠.
바로 여기서 Set 컬렉션(집합)이 등장합니다.
Set은 오직 고유한 요소만 저장하는 컬렉션입니다. 이미 존재하는 요소를 추가하려고 하면 그냥 추가되지 않습니다(아무도 기분 상하지 않죠).
Set 인터페이스: 기본 속성
Java에서 Set은 중복이 없는 컬렉션의 동작을 정의하는 인터페이스입니다. 인터페이스 Collection을 상속하므로, 추가(add), 삭제(remove), 포함 여부 확인(contains), 순회 같은 연산을 지원합니다.
핵심 특징:
- Set에는 동일한 요소가 둘 이상 존재할 수 없습니다.
- 요소의 저장 순서는 구현에 따라 임의일 수 있습니다.
- 인덱스가 없습니다: 리스트처럼 번호로 요소에 접근할 수 없습니다.
선언 문법
Set<String> guests = new HashSet<>();
2. HashSet: 빠르고 단순하지만 순서 없음
HashSet은 Set 인터페이스의 가장 인기 있는 구현입니다. 해시 테이블에 기반하며(HashMap과 유사하지만 "키-값" 쌍이 없고 고유한 값만 저장), 가장 큰 장점은 추가, 삭제, 검색 연산의 속도입니다.
HashSet은 어떻게 동작할까?
상자를 하나 떠올려 보세요. 안에 이미 비슷한 것이 있는지 빠르게 알아보려면, 각 물건에 고유한 "번호" — 해시 코드가 부여됩니다. HashSet에 요소를 추가할 때 먼저 이 해시 코드를 계산합니다. 이전에 없던 해시라면 요소가 컬렉션에 무난히 들어갑니다. 같은 해시가 있으면 equals()로 추가로 동일성 검사를 합니다. 객체가 실제로 동일하다면 새 요소는 추가되지 않습니다.
즉, HashSet은 고유성을 자동으로 보장합니다: 동일한 객체가 둘 이상 나타나지 않습니다.
흥미로운 점: 사용자 정의 클래스를 HashSet에 저장하려면 equals()와 hashCode() 메서드를 재정의해야 합니다. 그렇지 않으면 컬렉션이 예측 불가능하게 동작할 수 있습니다 — 겉보기엔 같은 객체가 서로 다른 것으로 간주될 수 있습니다.
HashSet의 주요 메서드
Set<String> guests = new HashSet<>();
guests.add("이반");
guests.add("마리야");
guests.add("표트르");
guests.add("이반"); // 중복! 추가되지 않습니다.
System.out.println(guests); // [이반, 마리야, 표트르] — 순서는 임의일 수 있습니다
guests.remove("표트르"); // 요소를 삭제
System.out.println(guests.contains("마리야")); // true
System.out.println(guests.size()); // 2
코드로 직접 시도해 봅시다
예를 들어, 우리 애플리케이션에서 작업 이름이 중복되지 않도록 고유한 작업 이름만 저장하고 싶다고 해봅시다:
import java.util.HashSet;
import java.util.Set;
public class UniqueTasksDemo {
public static void main(String[] args) {
Set<String> tasks = new HashSet<>();
tasks.add("Java 숙제 하기");
tasks.add("고양이 쓰다듬기");
tasks.add("Java 숙제 하기"); // 중복!
System.out.println("작업 목록:");
for (String task : tasks) {
System.out.println("- " + task);
}
// 목록에는 두 개의 작업만 있고, 중복은 추가되지 않습니다
}
}
3. TreeSet: 순서가 중요할 때!
때로는 단순한 고유성뿐 아니라 정렬된 요소 집합이 필요합니다. 예컨대 손님 이름을 임의가 아니라 사전 순으로 보고 싶을 수 있죠. 이럴 때 TreeSet이 유용합니다.
TreeSet은 요소를 정렬된 순서(오름차순)로 저장하는 Set의 구현입니다. "레드-블랙 트리" 구조를 기반으로 합니다.
TreeSet 사용 예
import java.util.Set;
import java.util.TreeSet;
public class SortedGuestsDemo {
public static void main(String[] args) {
Set<String> guests = new TreeSet<>();
guests.add("블라디미르");
guests.add("알렉세이");
guests.add("예카테리나");
guests.add("알렉세이"); // 중복!
System.out.println("손님(사전 순):");
for (String guest : guests) {
System.out.println("- " + guest);
}
// 출력:
// - 알렉세이
// - 블라디미르
// - 예카테리나
}
}
주의: 중복을 추가하면 집합에 나타나지 않습니다. 당연히 그래야 하죠!
언제 TreeSet을 사용할까?
- 정렬된 고유 요소 집합이 필요할 때.
- 검색이 중요하지만 추가 속도가 아주 중요하지 않을 때(HashSet보다 약간 느립니다).
- 요소가 사용자 정의 클래스인 경우, 요소가 "비교 가능"(인터페이스 Comparable 구현)해야 하거나 Comparator를 제공해야 합니다.
4. 유용한 뉘앙스
HashSet vs TreeSet: 무엇을 선택할까?
| 기준 | HashSet | TreeSet |
|---|---|---|
| 저장 순서 | 보장되지 않음 | 오름차순으로 정렬됨 |
| 연산 속도 | 더 빠름 (O(1)) | 더 느림 (O(log n)) |
| 타입 요구사항 | 제한 없음 (단, equals()/hashCode() 필요) | Comparable 또는 Comparator |
| 일반적인 시나리오 | 고유 요소에 빠르게 접근해야 할 때 | 정렬된 출력/순회가 중요할 때 |
Set 사용상의 특징
- 인덱스 없음. List와 달리 Set에는 get(int index) 메서드가 없습니다. 인덱스로 접근이 필요하면 List를 사용하세요.
- 중복 없음. 이미 있는 요소를 추가하면 추가되지 않습니다. add 메서드는 false를 반환합니다.
- 순서가 보장되지 않음(TreeSet 제외). HashSet의 요소 순서는 실행할 때마다 달라질 수 있습니다. 삽입 순서가 필요하면 LinkedHashSet을 사용하세요.
- null 값.
- HashSet은 null 요소 하나를 허용합니다.
- TreeSet은 특별한 Comparator 없이 null을 추가하면 NullPointerException이 발생합니다.
5. Set의 전형적인 사용 과제
리스트에서 중복 제거
예를 들어, 어떤 학생 리스트에 일부 학생이 두 번씩 기록되어 있다고 합시다. 고유한 이름만 남겨야 합니다:
import java.util.*;
public class RemoveDuplicatesDemo {
public static void main(String[] args) {
List<String> students = Arrays.asList("안나", "이고리", "안나", "마리야", "이고리", "파벨");
Set<String> uniqueStudents = new HashSet<>(students);
System.out.println("고유한 학생들: " + uniqueStudents);
// 순서는 보장되지 않습니다!
}
}
정렬된 결과가 필요하다면 TreeSet을 사용하세요:
Set<String> sortedUniqueStudents = new TreeSet<>(students);
System.out.println("고유한 학생들(사전 순): " + sortedUniqueStudents);
고유성 검사(예: 사용자 로그인)
Set<String> usedLogins = new HashSet<>();
usedLogins.add("student1");
usedLogins.add("java_lover");
String newLogin = "student1";
if (usedLogins.contains(newLogin)) {
System.out.println("해당 로그인은 이미 사용 중입니다!");
} else {
System.out.println("로그인을 사용할 수 있습니다!");
}
집합 요소 순회
for-each 루프를 사용합니다:
for (String name : uniqueStudents) {
System.out.println(name);
}
6. Set 사용 시 흔한 실수
오류 №1: HashSet에서 특정한 요소 순서를 기대함. 많은 초보자들이 요소가 "이상한" 순서로 출력되는 것에 놀랍니다. 이는 정상입니다 — HashSet은 순서를 보장하지 않습니다. 삽입 순서가 필요하면 LinkedHashSet을, 정렬이 필요하면 TreeSet을 사용하세요.
오류 №2: 인덱스로 요소에 접근하려고 함. 가끔 set.get(0) 같은 코드를 작성하려고 합니다. 이렇게는 할 수 없습니다: Set은 인덱싱을 지원하지 않습니다. 인덱스로 접근이 필요하면 List를 사용하세요.
오류 №3: 변경 가능한 객체를 저장함. equals()/hashCode()에 참여하는 필드를 변경할 수 있는 객체를 저장하면, 해당 필드를 변경한 뒤에는 그 요소가 집합에서 "사라진" 것처럼 보일 수 있습니다. 요소를 불변으로 만들거나 식별 필드는 변경하지 마세요.
오류 №4: 중복이 추가될 것이라 기대함. 같은 요소를 여러 번 추가해도 집합의 크기는 늘어나지 않습니다 — 중복은 무시되며, add 메서드는 false를 반환합니다.
오류 №5: 기본형을 사용함. Set<int> 같은 표기는 컴파일되지 않습니다. 래퍼 클래스를 사용하세요: Set<Integer>, Set<Double> 등.
GO TO FULL VERSION