본문으로 건너뛰기

11.3 Set: HashSet, LinkedHashSet, TreeSet

Set중복을 허용하지 않는 집합(Set) 이라는 수학 개념을 그대로 구현한 컬렉션입니다. 같은 값을 두 번 저장하려고 하면 자동으로 무시합니다. 주로 "이 항목이 존재하는지 여부" 확인이나, 중복 없이 고유한 데이터만 보관 할 때 씁니다.

1. Set 인터페이스 특징

특징설명
중복 불허동일한 값은 한 번만 저장됩니다
순서 없음HashSet 기준. 입력 순서와 출력 순서가 다를 수 있습니다
null 허용HashSet은 null 하나만 저장 가능
빠른 검색HashSet은 O(1), TreeSet은 O(log n)
import java.util.HashSet;
import java.util.Set;

Set<String> set = new HashSet<>();
set.add("사과");
set.add("바나나");
set.add("사과"); // 중복 → 무시됨
System.out.println(set.size()); // 2 (중복 제거)
System.out.println(set); // [바나나, 사과] (순서 불규칙)

2. HashSet

가장 많이 쓰이는 Set 구현체입니다. 저장 순서를 보장하지 않습니다(넣은 순서와 꺼내는 순서가 다를 수 있습니다). 내부적으로 HashMap을 사용하여 구현됩니다.

주요 메서드

import java.util.HashSet;
import java.util.Iterator;

public class HashSetMethods {
public static void main(String[] args) {
HashSet<String> tags = new HashSet<>();

// ========== 추가 ==========
tags.add("Java");
tags.add("Spring");
tags.add("MySQL");
tags.add("Java"); // 중복! → false 반환, 무시됨
boolean added = tags.add("Python");
System.out.println("Python 추가 성공: " + added); // true
boolean dup = tags.add("Java");
System.out.println("Java 재추가 성공: " + dup); // false

System.out.println("tags: " + tags);
System.out.println("크기: " + tags.size()); // 4

// ========== 조회 ==========
System.out.println("Java 있음?: " + tags.contains("Java")); // true
System.out.println("Go 있음?: " + tags.contains("Go")); // false
System.out.println("비어있음?: " + tags.isEmpty()); // false

// ========== 삭제 ==========
tags.remove("MySQL");
System.out.println("삭제 후: " + tags);

// ========== 순회 ==========
System.out.println("=== for-each 순회 ===");
for (String tag : tags) {
System.out.println("- " + tag);
}

System.out.println("=== Iterator 순회 ===");
Iterator<String> it = tags.iterator();
while (it.hasNext()) {
System.out.println("- " + it.next());
}

// ========== 초기화 ==========
tags.clear();
System.out.println("clear 후 isEmpty: " + tags.isEmpty()); // true
}
}

equals와 hashCode의 중요성

HashSet에서 중복을 판단하는 기준은 equals() 메서드hashCode() 메서드 입니다. 두 메서드 모두 오버라이드해야 올바르게 동작합니다.

import java.util.HashSet;
import java.util.Objects;

public class HashSetEqualsExample {

// equals/hashCode를 오버라이드하지 않은 클래스
static class PointBad {
int x, y;
PointBad(int x, int y) { this.x = x; this.y = y; }
}

// equals/hashCode를 올바르게 오버라이드한 클래스
static class PointGood {
int x, y;
PointGood(int x, int y) { this.x = x; this.y = y; }

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PointGood)) return false;
PointGood p = (PointGood) o;
return x == p.x && y == p.y;
}

@Override
public int hashCode() {
return Objects.hash(x, y); // x, y 기반 해시코드
}

@Override
public String toString() { return "(" + x + "," + y + ")"; }
}

public static void main(String[] args) {
// 문제: equals/hashCode 없이는 내용이 같아도 다른 객체로 판단
HashSet<PointBad> badSet = new HashSet<>();
badSet.add(new PointBad(1, 1));
badSet.add(new PointBad(1, 1)); // 내용 같지만 다른 객체!
System.out.println("PointBad 크기: " + badSet.size()); // 2 (중복 제거 실패!)

// 해결: equals/hashCode 오버라이드
HashSet<PointGood> goodSet = new HashSet<>();
goodSet.add(new PointGood(1, 1));
goodSet.add(new PointGood(1, 1)); // 내용 같음 → 중복으로 처리
System.out.println("PointGood 크기: " + goodSet.size()); // 1 (올바르게 중복 제거!)
System.out.println("PointGood 내용: " + goodSet); // [(1,1)]
}
}
equals와 hashCode 규칙

HashSet(또는 HashMap의 키)에 사용자 정의 객체를 넣을 때는 반드시 equals()hashCode()함께 오버라이드해야 합니다.

  • equals()가 true이면 hashCode()도 같아야 합니다.
  • hashCode()가 같아도 equals()가 false일 수 있습니다 (해시 충돌).

3. LinkedHashSet

LinkedHashSetHashSet과 동일하지만 삽입 순서를 유지 합니다. 내부적으로 이중 연결 리스트를 추가로 관리합니다.

import java.util.LinkedHashSet;

public class LinkedHashSetExample {
public static void main(String[] args) {
LinkedHashSet<String> langs = new LinkedHashSet<>();

langs.add("Python");
langs.add("Java");
langs.add("Go");
langs.add("Java"); // 중복 → 무시
langs.add("Kotlin");

// 삽입 순서 그대로 출력!
System.out.println(langs); // [Python, Java, Go, Kotlin]
System.out.println("크기: " + langs.size()); // 4

// 중복 제거 + 순서 유지가 모두 필요할 때 사용
System.out.println("Java 있음? " + langs.contains("Java")); // true
}
}
LinkedHashSet vs HashSet
  • 순서가 중요없고 속도만 중요: HashSet 사용
  • 중복 제거 + 입력 순서 유지: LinkedHashSet 사용
  • LinkedHashSetHashSet보다 약간 더 메모리를 사용합니다.

4. TreeSet

TreeSet은 저장되는 요소를 자동으로 정렬 해서 보관합니다. 내부적으로 Red-Black 트리(자가 균형 이진 탐색 트리)를 사용합니다. 기본적으로 오름차순(자연 정렬)이며, Comparator로 정렬 기준을 커스터마이즈할 수 있습니다.

기본 사용

import java.util.TreeSet;

public class TreeSetBasic {
public static void main(String[] args) {
TreeSet<Integer> scores = new TreeSet<>();
scores.add(85);
scores.add(92);
scores.add(70);
scores.add(100);
scores.add(85); // 중복 제거됨

System.out.println("정렬된 점수: " + scores); // [70, 85, 92, 100]

// NavigableSet 기능
System.out.println("최솟값: " + scores.first()); // 70
System.out.println("최댓값: " + scores.last()); // 100
System.out.println("80보다 크거나 같은 첫 값: " + scores.ceiling(80)); // 85
System.out.println("90보다 작거나 같은 첫 값: " + scores.floor(90)); // 85
System.out.println("85보다 큰 첫 값: " + scores.higher(85)); // 92
System.out.println("85보다 작은 첫 값: " + scores.lower(85)); // 70

// 범위 조회: 80~95 사이 (포함) → headSet, tailSet, subSet
System.out.println("80~95 범위: " + scores.subSet(80, true, 95, true)); // [85, 92]
System.out.println("90 초과: " + scores.tailSet(90, false)); // [92, 100]
System.out.println("90 이하: " + scores.headSet(90, true)); // [70, 85]
}
}

커스텀 정렬 Comparator

import java.util.Comparator;
import java.util.TreeSet;

public class TreeSetComparator {
public static void main(String[] args) {
// 문자열을 길이 내림차순, 같으면 알파벳 순으로 정렬
TreeSet<String> words = new TreeSet<>(
Comparator.comparingInt(String::length).reversed()
.thenComparing(Comparator.naturalOrder())
);

words.add("banana");
words.add("apple");
words.add("fig");
words.add("cherry");
words.add("kiwi");

System.out.println(words); // [banana, cherry, apple, kiwi, fig]
// 길이: banana(6), cherry(6), apple(5), kiwi(4), fig(3)
// 길이 같으면 알파벳 순: banana < cherry
}
}

5. Set 집합 연산 (합집합, 교집합, 차집합)

Set의 강력한 기능 중 하나는 수학적 집합 연산 을 쉽게 수행할 수 있다는 점입니다.

import java.util.HashSet;
import java.util.Set;

public class SetOperations {
public static void main(String[] args) {
Set<String> groupA = new HashSet<>(Set.of("Java", "Python", "Go", "Rust"));
Set<String> groupB = new HashSet<>(Set.of("Python", "Kotlin", "Go", "Swift"));

System.out.println("그룹A: " + groupA);
System.out.println("그룹B: " + groupB);

// 합집합 (Union): A ∪ B
Set<String> union = new HashSet<>(groupA);
union.addAll(groupB);
System.out.println("합집합 (A ∪ B): " + union);
// [Java, Python, Go, Rust, Kotlin, Swift]

// 교집합 (Intersection): A ∩ B
Set<String> intersection = new HashSet<>(groupA);
intersection.retainAll(groupB);
System.out.println("교집합 (A ∩ B): " + intersection);
// [Python, Go]

// 차집합 (Difference): A - B
Set<String> difference = new HashSet<>(groupA);
difference.removeAll(groupB);
System.out.println("차집합 (A - B): " + difference);
// [Java, Rust]

// 대칭 차집합 (Symmetric Difference): (A ∪ B) - (A ∩ B)
Set<String> symDiff = new HashSet<>(union);
symDiff.removeAll(intersection);
System.out.println("대칭 차집합: " + symDiff);
// [Java, Rust, Kotlin, Swift]

// 부분집합 판단: groupA가 union의 부분집합인가?
System.out.println("A ⊆ union? " + union.containsAll(groupA)); // true
}
}
집합 연산 메서드 정리
연산메서드설명
합집합addAll(other)other의 모든 요소를 추가
교집합retainAll(other)other에 없는 요소 모두 제거
차집합removeAll(other)other에 있는 요소 모두 제거
부분집합containsAll(other)other의 모든 요소가 포함되어 있는지 확인

6. Set 구현체 비교표

비교 항목HashSetLinkedHashSetTreeSet
내부 구조해시 테이블해시 테이블 + 연결 리스트Red-Black 트리
순서없음삽입 순서 유지정렬 순서 유지
중복불허불허불허
add 성능O(1)O(1)O(log n)
contains 성능O(1)O(1)O(log n)
null 허용1개 허용1개 허용불허
범위 검색불가불가가능 (NavigableSet)

7. 실전 예제 1: 중복 이메일 제거

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;

public class DuplicateEmailRemover {
public static void main(String[] args) {
// 가입 신청 이메일 목록 (중복 포함)
List<String> signupEmails = new ArrayList<>();
signupEmails.add("alice@example.com");
signupEmails.add("bob@example.com");
signupEmails.add("alice@example.com"); // 중복
signupEmails.add("charlie@example.com");
signupEmails.add("bob@example.com"); // 중복
signupEmails.add("diana@example.com");

System.out.println("신청 목록 (" + signupEmails.size() + "개): " + signupEmails);

// LinkedHashSet: 중복 제거 + 신청 순서 유지
LinkedHashSet<String> uniqueEmails = new LinkedHashSet<>(signupEmails);
System.out.println("중복 제거 후 (" + uniqueEmails.size() + "개): " + uniqueEmails);

// 다시 리스트로 변환해서 사용
List<String> finalList = new ArrayList<>(uniqueEmails);
System.out.println("최종 가입 처리: " + finalList);

// 이미 가입된 이메일 목록
LinkedHashSet<String> registeredEmails = new LinkedHashSet<>();
registeredEmails.add("alice@example.com");
registeredEmails.add("eve@example.com");

// 새로 가입할 이메일 = 신청 이메일 - 이미 등록된 이메일
LinkedHashSet<String> newEmails = new LinkedHashSet<>(uniqueEmails);
newEmails.removeAll(registeredEmails);
System.out.println("신규 가입 처리 대상: " + newEmails);
// [bob@example.com, charlie@example.com, diana@example.com]
}
}

8. 실전 예제 2: 두 그룹의 공통 관심사 찾기

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class CommonInterests {
public static void main(String[] args) {
// 사용자 A의 관심사
Set<String> userA = new HashSet<>(Arrays.asList(
"독서", "영화", "코딩", "요리", "여행", "사진"
));

// 사용자 B의 관심사
Set<String> userB = new HashSet<>(Arrays.asList(
"음악", "코딩", "여행", "운동", "요리", "게임"
));

System.out.println("사용자 A 관심사: " + userA);
System.out.println("사용자 B 관심사: " + userB);

// 공통 관심사 (교집합)
Set<String> common = new HashSet<>(userA);
common.retainAll(userB);
System.out.println("공통 관심사: " + new TreeSet<>(common));
// [요리, 여행, 코딩] (TreeSet으로 정렬하여 출력)

// A만의 관심사
Set<String> onlyA = new HashSet<>(userA);
onlyA.removeAll(userB);
System.out.println("A만의 관심사: " + new TreeSet<>(onlyA));

// B만의 관심사
Set<String> onlyB = new HashSet<>(userB);
onlyB.removeAll(userA);
System.out.println("B만의 관심사: " + new TreeSet<>(onlyB));

// 매칭 점수 계산 (공통 관심사 / 전체 관심사 * 100)
Set<String> allInterests = new HashSet<>(userA);
allInterests.addAll(userB);
double matchScore = (double) common.size() / allInterests.size() * 100;
System.out.printf("매칭 점수: %.1f%%%n", matchScore);
}
}

9. Set.of() 불변 Set (Java 9+)

import java.util.Set;

public class ImmutableSet {
public static void main(String[] args) {
// 불변 Set 생성 (중복, null 불허)
Set<String> weekdays = Set.of("월", "화", "수", "목", "금");
System.out.println(weekdays);
System.out.println("월요일 있음? " + weekdays.contains("월")); // true
// weekdays.add("토"); // UnsupportedOperationException!
// weekdays.remove("월"); // UnsupportedOperationException!

// Set.copyOf(): 기존 Set을 불변으로 복사
java.util.HashSet<String> mutable = new java.util.HashSet<>();
mutable.add("Java");
mutable.add("Python");
Set<String> immutableCopy = Set.copyOf(mutable);
System.out.println(immutableCopy); // [Java, Python]

// mutable을 변경해도 immutableCopy는 영향 없음
mutable.add("Go");
System.out.println("mutable: " + mutable); // [Java, Python, Go]
System.out.println("immutableCopy: " + immutableCopy); // [Java, Python]
}
}

고수 팁: Set 성능 고려사항

import java.util.EnumSet;

public class SetAdvancedTips {
// Enum과 함께 사용하는 EnumSet: 비트벡터 기반으로 가장 빠른 Set
enum Permission { READ, WRITE, EXECUTE, DELETE }

public static void main(String[] args) {
// EnumSet: Enum 타입 전용 Set, 매우 빠름 (비트 연산)
EnumSet<Permission> adminPerms = EnumSet.allOf(Permission.class);
EnumSet<Permission> readOnlyPerms = EnumSet.of(Permission.READ);

System.out.println("관리자 권한: " + adminPerms); // [READ, WRITE, EXECUTE, DELETE]
System.out.println("읽기 전용: " + readOnlyPerms); // [READ]

// 교집합: 읽기 권한이 있는가?
EnumSet<Permission> commonPerms = EnumSet.copyOf(adminPerms);
commonPerms.retainAll(readOnlyPerms);
System.out.println("공통 권한: " + commonPerms); // [READ]
}
}
Set 선택 가이드 요약
  • 단순 중복 제거, 빠른 검색 → HashSet(가장 일반적)
  • 중복 제거 + 입력 순서 유지 → LinkedHashSet
  • 중복 제거 + 자동 정렬 + 범위 검색 → TreeSet
  • Enum 타입 집합 → EnumSet(가장 빠름)