본문으로 건너뛰기
김신건의 로그

[Java] Set

· 수정 · 📖 약 2분 · 661자/단어 #java #collection #set #interface #jcf
Set, java.util.Set, Java Set, Set interface, 집합

정의

java.util.Set<E>중복을 허용하지 않는 Collection. 수학의 집합 (set) 에 대응. 같은 원소를 두 번 추가하면 두 번째는 무시된다.

Collection 인터페이스의 모든 메서드를 그대로 사용하면서, equalshashCode (또는 compareTo) 로 중복 판단.

Set<String> s = new HashSet<>();
s.add("apple");
s.add("banana");
s.add("apple");   // 중복, 무시
s.size();         // 2

언제 쓰나

  • 중복 제거: 컬렉션에서 유일한 원소만 남길 때
  • 멤버십 검사: “이 값이 집합에 있는가?” 를 빠르게 확인
  • 집합 연산: 교집합, 합집합, 차집합
  • 방문 여부 추적: 그래프 탐색에서 방문한 노드 기록
  • 고유 ID 관리: 중복 없는 식별자 집합 유지

시각화: Set 구현체 계층

flowchart TD
    Iterable["Iterable"] --> Collection["Collection"]
    Collection --> Set["Set"]
    Set --> HashSet["HashSet"]
    Set --> SortedSet["SortedSet"]
    HashSet --> LinkedHashSet["LinkedHashSet"]
    SortedSet --> NavigableSet["NavigableSet"]
    NavigableSet --> TreeSet["TreeSet"]
    Set --> CopyOnWriteArraySet["CopyOnWriteArraySet"]
    Set --> EnumSet["EnumSet"]

핵심 메서드

메서드반환의미
add(E e)boolean추가, 이미 있으면 false
remove(Object o)boolean제거, 없으면 false
contains(Object o)boolean포함 여부
size()int원소 개수
isEmpty()boolean비어 있는지
iterator()Iterator순회
addAll(Collection)boolean합집합 (in-place)
retainAll(Collection)boolean교집합 (in-place)
removeAll(Collection)boolean차집합 (in-place)
containsAll(Collection)boolean부분집합 여부

추가/조회/삭제의 시간 복잡도는 구현체에 따라 다르다.

구현체 비교

구현백킹 구조add/contains순서nullThread-safe
HashSetHashMapO(1) avg없음✓ (1개)
LinkedHashSetLinkedHashMapO(1) avg삽입 순서✓ (1개)
TreeSetTreeMapO(log n)정렬
CopyOnWriteArraySetCopyOnWriteArrayListO(n)삽입 순서
ConcurrentSkipListSetConcurrentSkipListMapO(log n)정렬
EnumSetbit vectorO(1)enum 선언 순서

대부분의 Set 구현은 내부적으로 Map 을 사용, key 만 노출하고 value 는 dummy 상수.

불변 Set (Set.of)

Java 9+ 에서 도입된 팩토리 메서드.

// Set.of: 불변, null 비허용, 중복 인자 시 IllegalArgumentException
Set<Integer> primes = Set.of(2, 3, 5, 7, 11);
primes.add(13);      // UnsupportedOperationException
primes.add(null);    // NullPointerException

// 중복 인자 → 예외
Set<String> bad = Set.of("a", "a");   // IllegalArgumentException

// Set.copyOf: 기존 컬렉션을 불변 Set 으로 복사
List<String> list = List.of("x", "y", "z");
Set<String> copy = Set.copyOf(list);   // 불변

// Collections.unmodifiableSet: 기존 Set 의 불변 뷰 (원본 변경 반영)
Set<String> mutable = new HashSet<>(Set.of("a", "b"));
Set<String> view = Collections.unmodifiableSet(mutable);
mutable.add("c");
view.contains("c");   // true (원본 변경 반영)

IMPORTANT

Set.of순서를 보장하지 않는다. 반복 순서가 실행마다 다를 수 있다. 순서가 필요하면 LinkedHashSet 또는 TreeSet 사용.

집합 연산

Set<Integer> a = new HashSet<>(List.of(1, 2, 3, 4));
Set<Integer> b = new HashSet<>(List.of(3, 4, 5, 6));

// 합집합 (union): a ∪ b
Set<Integer> union = new HashSet<>(a);
union.addAll(b);           // {1, 2, 3, 4, 5, 6}

// 교집합 (intersection): a ∩ b
Set<Integer> inter = new HashSet<>(a);
inter.retainAll(b);        // {3, 4}

// 차집합 (difference): a \ b
Set<Integer> diff = new HashSet<>(a);
diff.removeAll(b);         // {1, 2}

// 대칭 차집합 (symmetric difference): (a ∪ b) \ (a ∩ b)
Set<Integer> symDiff = new HashSet<>(union);
symDiff.removeAll(inter);  // {1, 2, 5, 6}

// 부분집합 여부
a.containsAll(inter);      // true (inter ⊆ a)

addAll, retainAll, removeAll 의 비용은 인자의 contains 가 빠를수록 좋다. 인자로 HashSet 을 전달하면 O(n), ArrayList 를 전달하면 O(n*m).

Java 17+ 실전: 중복 제거

import java.util.*;
import java.util.stream.*;

// 리스트에서 중복 제거 (순서 무관)
List<String> words = List.of("apple", "banana", "apple", "cherry", "banana");
Set<String> unique = new HashSet<>(words);

// 삽입 순서 유지
Set<String> ordered = new LinkedHashSet<>(words);
// [apple, banana, cherry]

// Stream.distinct() (내부적으로 LinkedHashSet 유사 동작)
List<String> deduped = words.stream().distinct().toList();

// 정렬된 고유 원소
Set<String> sorted = new TreeSet<>(words);
// [apple, banana, cherry] (알파벳 순)

Java 17+ 실전: EnumSet 활용

import java.util.EnumSet;

enum Permission { READ, WRITE, EXECUTE, DELETE }

// EnumSet: bit vector 기반, 가장 빠른 Set 구현
EnumSet<Permission> adminPerms = EnumSet.allOf(Permission.class);
EnumSet<Permission> readOnly = EnumSet.of(Permission.READ);
EnumSet<Permission> readWrite = EnumSet.of(Permission.READ, Permission.WRITE);

// 집합 연산
EnumSet<Permission> extra = EnumSet.copyOf(readWrite);
extra.add(Permission.EXECUTE);   // {READ, WRITE, EXECUTE}

// 여집합
EnumSet<Permission> complement = EnumSet.complementOf(readOnly);
// {WRITE, EXECUTE, DELETE}

boolean canWrite = adminPerms.contains(Permission.WRITE);   // true

Java 17+ 실전: 권한 검사 시스템

import java.util.*;

record User(String name, Set<String> roles) {}

class AccessControl {
    private final Map<String, Set<String>> rolePermissions = new HashMap<>();

    void grantPermission(String role, String permission) {
        rolePermissions.computeIfAbsent(role, k -> new HashSet<>()).add(permission);
    }

    // 사용자가 특정 권한을 가지는지 확인
    boolean hasPermission(User user, String permission) {
        return user.roles().stream()
            .anyMatch(role -> {
                Set<String> perms = rolePermissions.getOrDefault(role, Set.of());
                return perms.contains(permission);
            });
    }

    // 사용자의 모든 권한 (합집합)
    Set<String> allPermissions(User user) {
        Set<String> result = new HashSet<>();
        user.roles().forEach(role ->
            result.addAll(rolePermissions.getOrDefault(role, Set.of()))
        );
        return Collections.unmodifiableSet(result);
    }
}

동기화

// 옵션 1: Collections.synchronizedSet (복합 연산은 외부 동기화 필요)
Set<Integer> sync = Collections.synchronizedSet(new HashSet<>());
synchronized (sync) {
    if (!sync.contains(x)) sync.add(x);   // check-then-act 원자적으로
}

// 옵션 2: ConcurrentHashMap.newKeySet (권장, 복합 연산도 원자적)
Set<Integer> concurrent = ConcurrentHashMap.newKeySet();
concurrent.add(x);   // 내부적으로 putIfAbsent

// 옵션 3: CopyOnWriteArraySet (읽기 多, 쓰기 少)
Set<String> cow = new CopyOnWriteArraySet<>();

// 옵션 4: ConcurrentSkipListSet (정렬 + 동시성)
Set<Integer> skipList = new ConcurrentSkipListSet<>();

equals / hashCode 규약

Setequals 는 두 집합이 같은 원소를 가지면 true.

Set<Integer> s1 = new HashSet<>(List.of(1, 2, 3));
Set<Integer> s2 = new TreeSet<>(List.of(1, 2, 3));
s1.equals(s2);   // true (구현체 달라도 원소 같으면 같음)

// Set 의 hashCode = 원소 hashCode 의 합
s1.hashCode() == s2.hashCode();   // true

원소의 equals/hashCode 가 잘못 구현되면 HashSet/LinkedHashSet 이 잘못 동작한다.

함정

1. 가변 객체를 원소로

Set<List<Integer>> set = new HashSet<>();
List<Integer> elem = new ArrayList<>(List.of(1, 2));
set.add(elem);
elem.add(3);                 // hashCode 변경
set.contains(elem);          // false! (다른 버킷)
set.size();                  // 1 이지만 찾을 수 없음

불변 객체 (String, Integer, record) 를 원소로 사용 권장.

2. equals 만 override, hashCode 미구현

Object 의 규약 위반. HashSet, LinkedHashSet 이 잘못 동작.

class Point {
    int x, y;
    @Override public boolean equals(Object o) {
        if (!(o instanceof Point p)) return false;
        return x == p.x && y == p.y;
    }
    // hashCode 없음 → Object.hashCode() 사용 (참조 기반)
}

Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.contains(new Point(1, 2));   // false! (hashCode 다름)

3. Set.of 의 순서 불정

Set<String> s = Set.of("a", "b", "c");
// 반복 순서가 실행마다 다를 수 있음
// 순서가 필요하면 LinkedHashSet 또는 TreeSet 사용

4. iterator 는 fail-fast iterator (HashSet, TreeSet)

Set<Integer> set = new HashSet<>(Set.of(1, 2, 3));
for (Integer x : set) {
    set.add(4);   // ConcurrentModificationException
}

// 안전한 제거
set.removeIf(x -> x % 2 == 0);

5. TreeSet 에 null 추가

TreeSet<String> ts = new TreeSet<>();
ts.add(null);   // NullPointerException (compareTo(null) 호출)

null 이 필요하면 HashSet 또는 null-friendly Comparator 를 명시.

관련 위키

이 글의 용어 (11개)
[Java] Collectionjava
정의 는 그룹으로 묶인 객체들을 표현하는 최상위 인터페이스. JCF (Java Collections Framework) 의 입구이자, / / / 모두 이를 확장한다. 자체는 직접…
[Java] ConcurrentHashMapjava
정의 는 고동시성 환경에서 사용 가능한 구현. JSR-166 (Java 5) 도입, Java 8 에서 내부 구조가 크게 재작성됐다. 과 같은 인터페이스를 제공하면서 thread-…
[Java] ConcurrentModificationExceptionjava
정의 (CME) 는 fail-fast iterator 가 컬렉션의 구조 변경을 탐지했을 때 던지는 unchecked 예외 ( ). 이름과 달리 반드시 동시성 (멀티스레드) 문제일…
[Java] ConcurrentSkipListSetjava
정의 는 을 백킹으로 사용하는 정렬된 thread-safe . 인터페이스를 구현하며 의 동시성 버전에 해당한다. - JDK 1.6 추가 - 내부는 skip list (확률적 자료…
[Java] CopyOnWriteArrayListjava
정의 는 쓰기 시 배열 전체를 복사 하는 thread-safe 구현. 읽기에는 lock 이 전혀 없고, 쓰기에는 으로 직렬화한다. (JSR-166) 의 컬렉션. 읽기 압도적 다,…
[Java] Fail-Fast Iteratorjava
정의 Fail-Fast Iterator 는 순회 도중 컬렉션의 구조가 변경되면 즉시 을 던지는 iterator. , , 등 대부분의 컬렉션의 iterator 가 이 방식. 목적은…
[Java] HashSetjava
정의 는 을 백킹으로 사용하는 구현. 내부적으로 모든 원소를 의 key 로 저장하고, value 는 dummy 상수 로 채운다. - 평균 O(1) 의 / / - 순서 보장 없음 …
[Java] Iterablejava
정의 는 루프로 순회 가능한 모든 타입의 최상위 인터페이스. 단 하나의 추상 메서드, 를 정의한다. 인터페이스가 을 extends 하므로 , , , 등 모든 컬렉션이 자동으로 대…
[Java] Objectjava
정의 는 Java 의 모든 클래스의 최상위 부모 (root) 클래스. 가 명시되지 않은 클래스는 컴파일러가 자동으로 를 붙인다. 인터페이스는 클래스가 아니라 를 직접 상속하지는 …
[Java] TreeSetjava
정의 는 을 백킹으로 사용하는 정렬된 . 인터페이스를 구현해 정렬 기반 연산 ( , , 등) 을 제공한다. - JDK 1.2 도입 - 내부: Red-Black Tree (자가 균…
집합과 맵 (Set and Map)algorithm
정의 집합 (Set) 은 고유한 원소들의 모임을 나타내는 추상 자료구조로, 멤버십 검사, 삽입, 삭제를 지원한다. 맵 (Map, Dictionary) 은 키-값 쌍을 저장하며, …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기