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

[Java] HashSet

· 수정 · 📖 약 2분 · 767자/단어 #java #collection #set #hash #jcf
HashSet, java.util.HashSet, Java HashSet, 해시셋

정의

java.util.HashSet<E>HashMap 을 백킹으로 사용하는 Set 구현. 내부적으로 모든 원소를 HashMap 의 key 로 저장하고, value 는 dummy 상수 PRESENT 로 채운다.

  • 평균 O(1) 의 add/remove/contains
  • 순서 보장 없음 (삽입 순서, 정렬 순서 모두 아님)
  • null 원소 하나 허용
  • thread-safe 하지 않음

JDK 1.2 도입. Java 컬렉션 프레임워크에서 가장 흔히 쓰이는 Set 구현.

언제 쓰나

  • 중복 제거: 리스트에서 중복 원소를 빠르게 제거할 때
  • 멤버십 검사: “이 값이 집합에 있는가?” 를 O(1) 로 확인
  • 집합 연산: 교집합, 합집합, 차집합 (단일 스레드)
  • 방문 여부 추적: BFS/DFS 에서 방문한 노드 기록
  • 순서가 필요 없고 빠른 조회가 우선일 때

시각화: HashMap 기반 구조

flowchart TD
    subgraph "HashSet"
        Add["add(e)"] --> HM["HashMap.put(e, PRESENT)"]
        Contains["contains(o)"] --> HMC["HashMap.containsKey(o)"]
        Remove["remove(o)"] --> HMR["HashMap.remove(o)"]
    end

    subgraph "내부 HashMap"
        HM --> Bucket["버킷 배열"]
        HMC --> Bucket
        HMR --> Bucket
        Bucket --> Node1["Node: key=e1, value=PRESENT"]
        Bucket --> Node2["Node: key=e2, value=PRESENT"]
        Bucket --> Node3["Node: key=e3, value=PRESENT"]
    end

내부 구조

public class HashSet<E> extends AbstractSet<E>
        implements Set<E>, Cloneable, Serializable {

    private transient HashMap<E, Object> map;

    // 모든 원소의 value 로 사용되는 dummy 객체
    private static final Object PRESENT = new Object();

    public HashSet() {
        map = new HashMap<>();
    }

    // 초기 capacity 와 loadFactor 지정 가능
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    public boolean add(E e) {
        return map.put(e, PRESENT) == null;
        // map.put 이 null 반환 = 새로 삽입됨 = true
        // map.put 이 PRESENT 반환 = 이미 있었음 = false
    }

    public boolean contains(Object o) {
        return map.containsKey(o);
    }

    public boolean remove(Object o) {
        return map.remove(o) == PRESENT;
    }

    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }

    public int size() {
        return map.size();
    }
}

HashMap 의 모든 동작과 성능 특성을 그대로 물려받는다. capacity/loadFactor, treeify 같은 내용은 HashMap 참고.

복잡도

작업평균최악 (Java 8+)
add(e)O(1)O(log n)
contains(o)O(1)O(log n)
remove(o)O(1)O(log n)
순회O(n + capacity)같음
size()O(1)O(1)

최악 O(log n) 은 Java 8 에서 도입된 treeify (버킷 내 노드 8개 이상 시 red-black tree 변환) 덕분. Java 7 이전은 최악 O(n).

equals / hashCode 규약

HashSet 의 정확한 동작은 원소의 equalshashCode 구현에 달려 있다.

규약: a.equals(b) 이면 반드시 a.hashCode() == b.hashCode().

// Java 17+ record: equals/hashCode 자동 생성
record Point(int x, int y) {}

Set<Point> points = new HashSet<>();
points.add(new Point(1, 2));
points.add(new Point(1, 2));   // 중복 → 추가 안 됨
points.size();                  // 1

// 일반 클래스: 직접 구현 필요
class BadPoint {
    int x, y;
    BadPoint(int x, int y) { this.x = x; this.y = y; }
    // equals/hashCode 없음 → 참조 동등성 사용
}

Set<BadPoint> bad = new HashSet<>();
bad.add(new BadPoint(1, 2));
bad.add(new BadPoint(1, 2));   // 다른 참조 → 중복 허용됨
bad.size();                     // 2 (기대: 1)

IMPORTANT

equals 만 override 하고 hashCode 를 override 하지 않으면 HashSet 이 잘못 동작한다. 두 객체가 equals 로 같아도 hashCode 가 다르면 다른 버킷에 들어가 중복 허용.

null 허용

원소 하나는 null 가능 (HashMap 의 null key 처럼).

Set<String> s = new HashSet<>();
s.add(null);                     // OK
s.add(null);                     // false (이미 있음)
s.contains(null);                // true
s.remove(null);                  // true

// Set.of() 는 null 비허용
Set<String> immutable = Set.of("a", null);   // NullPointerException

Java 17+ 실전: 중복 제거

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

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

// Stream 으로 중복 제거 후 리스트 반환
List<String> deduped = words.stream()
    .distinct()
    .toList();   // Java 16+

Java 17+ 실전: 집합 연산

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

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

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

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

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

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

Java 17+ 실전: BFS 방문 추적

import java.util.*;

// 그래프 BFS (방문 여부 O(1) 확인)
List<Integer> bfs(Map<Integer, List<Integer>> graph, int start) {
    Set<Integer> visited = new HashSet<>();
    Queue<Integer> queue = new ArrayDeque<>();
    List<Integer> order = new ArrayList<>();

    queue.add(start);
    visited.add(start);

    while (!queue.isEmpty()) {
        int node = queue.poll();
        order.add(node);
        for (int neighbor : graph.getOrDefault(node, List.of())) {
            if (visited.add(neighbor)) {   // add 가 true = 처음 방문
                queue.add(neighbor);
            }
        }
    }
    return order;
}

LinkedHashSet: 삽입 순서 유지

LinkedHashSetHashSet 의 서브클래스로, 내부적으로 LinkedHashMap 을 사용해 삽입 순서를 유지한다.

import java.util.LinkedHashSet;
import java.util.Set;

Set<String> linked = new LinkedHashSet<>();
linked.add("banana");
linked.add("apple");
linked.add("cherry");
linked.add("apple");   // 중복, 무시

System.out.println(linked);   // [banana, apple, cherry] (삽입 순서)

// HashSet 은 순서 불정
Set<String> hash = new HashSet<>(linked);
System.out.println(hash);     // [apple, banana, cherry] 또는 다른 순서

LinkedHashSet 의 성능은 HashSet 과 거의 동일하지만, 이중 연결 리스트 유지 비용으로 약간의 메모리/시간 오버헤드가 있다.

HashSet vs LinkedHashSet vs TreeSet

항목HashSetLinkedHashSetTreeSet
백킹 구조HashMapLinkedHashMapTreeMap
순서없음삽입 순서정렬 순서
add/containsO(1) avgO(1) avgO(log n)
null 허용
Range 쿼리
Thread-safe
메모리기준약간 더더 많음

초기 capacity 최적화

// 1000 개 원소 예상 → capacity 가 1000 / 0.75 ≈ 1334 보다 커야 resize 안 함
Set<String> set = new HashSet<>(2048);

// Guava 스타일 (Java 표준 없음)
// int capacity = (int)(expectedSize / 0.75f) + 1;
Set<String> optimized = new HashSet<>((int)(1000 / 0.75f) + 1);

함정

1. 가변 객체를 원소로

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

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

2. equals 만 override, hashCode 미구현

Object 의 규약 위반. HashSet 이 조용히 잘못 동작.

3. thread-safe 가 아님

동시 수정 시 데이터 손상. 동시성 필요 시:

// 옵션 1: Collections.synchronizedSet (복합 연산은 외부 동기화 필요)
Set<String> sync = Collections.synchronizedSet(new HashSet<>());

// 옵션 2: ConcurrentHashMap.newKeySet (권장)
Set<String> concurrent = ConcurrentHashMap.newKeySet();

4. iterator 는 fail-fast iterator

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

순회 중 수정은 removeIf 또는 별도 컬렉션에 모아 처리.

5. 순회 비용: O(n + capacity)

// 초기 capacity 가 크면 순회 비용 증가
Set<String> set = new HashSet<>(1_000_000);
set.add("only-one");
// iterator 는 1,000,000 개 버킷을 모두 스캔

원소가 적고 capacity 가 크면 순회가 느리다. trimToSize() 에 해당하는 메서드는 없으므로 처음부터 적절한 capacity 를 지정.

관련 위키

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

💬 댓글

사이트 검색 / 명령어

검색

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