집합은 중복 없는 값을 저장한다
HashSet(java.util 소속)은 각 값을 최대 한 번만 담는 컬렉션입니다. HashMap처럼 키와 값이 짝지어지는 일은 없고, 그저 서로 다른 요소들이 담긴 자루일 뿐입니다. 그 유일한 임무는 한 가지 질문에 빠르게 답하는 것입니다: "이게 여기 들어 있나요?"
타입 파라미터는 하나, <ElementType>입니다. ArrayList나 HashMap과 마찬가지로, 보통 변수의 타입은 Set 인터페이스로 두고 HashSet을 생성합니다.
add는 값이 새것이었는지를 반환한다
add는 값을 저장하기만 하는 게 아닙니다. 집합이 실제로 바뀌었는지 알려 주는 boolean을 반환합니다. 이미 들어 있는 값을 추가하면 false를 반환하고 집합은 그대로 둡니다.
이 반환값은 정말 유용합니다. if (!seen.add(x)) { /* x는 중복 */ }처럼 쓰면 진행하면서 한 줄로 중복을 잡아낼 수 있습니다.
리스트에서 중복 제거하기
집합은 반복을 거부하므로, 컬렉션을 중복 제거하는 가장 빠른 방법은 그것을 집합에 쏟아붓는 것입니다. HashSet 생성자는 다른 어떤 컬렉션이든 받아들입니다:
이것이 초보자가 집합에 손을 뻗는 가장 흔한 이유입니다. 다만 이렇게 왕복하는 과정에서 원래 순서를 잃는다는 점만 알아 두세요. 순서가 중요하다면 LinkedHashSet을 사용하세요(아래에서 다룹니다).
contains, remove, size
일상적인 연산은 다른 컬렉션의 것과 똑같습니다:
ArrayList에 비해 가장 큰 이점은 contains입니다. 리스트는 답을 내기 위해 모든 요소를 훑어야 하지만(O(n)), HashSet은 거의 곧장 답으로 건너뜁니다(대략 O(1)). 루프 안에서 list.contains(...)를 호출하고 있는 자신을 발견한다면, 보통 그것이 집합으로 바꿀 신호입니다.
집합 연산: 합집합, 교집합, 차집합
집합은 서로 결합할 때 빛을 발합니다. 어느 것이 어느 것인지 알고 나면 메서드들이 거의 평범한 말처럼 읽힙니다:
핵심 함정: addAll, retainAll, removeAll은 호출 대상이 된 집합 자체를 변경합니다. 각 예제가 먼저 a를 새 HashSet에 복사하는 이유가 바로 이것입니다. 그렇지 않으면 원본을 파괴하게 됩니다. 결과마다 새 집합을 만드세요.
HashSet은 순서를 유지하지 않는다
HashMap처럼 HashSet도 반복 순서를 전혀 보장하지 않으며, 실행할 때마다 순서가 달라질 수 있습니다. 예측 가능성이 필요하다면:
- **
LinkedHashSet**은 삽입 순서, 즉 요소를 추가한 순서를 유지합니다. - **
TreeSet**은 요소를 자연 순서(또는 직접 제공한Comparator)로 정렬해 유지합니다.
셋 모두 Set 인터페이스를 구현하므로, 이들 사이를 바꾸는 것은 생성자 한 줄을 고치는 일입니다.
요소는 해시 가능해야 한다
HashSet은 내부적으로 HashMap에 의해 뒷받침되므로 같은 규칙이 적용됩니다. 요소를 해싱해서 위치를 찾기 때문에, 요소의 hashCode()와 equals()가 서로 일치해야 합니다. String이나 Integer 같은 내장 타입은 이미 이를 올바르게 처리하며, 그래서 위에서 중복된 "java" 문자열이 제대로 하나로 합쳐진 것입니다. 직접 만든 클래스의 인스턴스를 저장한다면 equals와 hashCode를 모두 오버라이드하세요. 그렇지 않으면 의미상 "같은" 두 객체가 서로 다른 것으로 취급되어, contains와 중복 제거가 조용히 실패합니다.
다음: 컬렉션 순회하기
이제 세 가지 핵심 컬렉션 - ArrayList, HashMap, HashSet - 을 모두 만났습니다. 각각 조금씩 다른 방식으로 순회하며, 미묘한 함정도 있습니다(루프를 도는 중에 컬렉션을 수정하는 것 등). 다음에는 이 모든 것을 한데 모아, for-each 루프, 이터레이터, forEach로 컬렉션을 깔끔하게 순회하는 방법을 다루겠습니다.
자주 묻는 질문
Java에서 HashSet은 어떻게 만드나요?
타입 파라미터 하나(요소 타입)로 선언하고 생성자를 호출합니다: Set<String> tags = new HashSet<>();. tags.add("java");로 값을 추가하고 tags.contains("java");로 포함 여부를 확인합니다. java.util.HashSet과 java.util.Set을 임포트하세요.
Java에서 HashSet과 ArrayList의 차이는 무엇인가요?
ArrayList는 추가한 모든 요소를(중복 포함) 삽입 순서대로 유지하고 위치로 인덱싱됩니다. HashSet은 고유한 값만 저장하고, 순서를 보장하지 않으며, 인덱스가 없고, contains 검사가 리스트 전체를 훑는 대신 대략 상수 시간에 끝납니다. 위치가 아니라 고유성이나 빠른 포함 여부 검사가 중요할 때 HashSet을 선택하세요.
Java에서 리스트의 중복을 어떻게 제거하나요?
리스트를 HashSet 생성자에 넘기세요: Set<String> unique = new HashSet<>(list);. 집합이 반복되는 값을 자동으로 버립니다. 다시 리스트가 필요하다면(그리고 순서가 사라져도 상관없다면) 한 번 더 감싸세요: new ArrayList<>(unique). 원래 순서를 유지하고 싶다면 대신 LinkedHashSet을 사용하세요.