자바를 활용하다 보면 중복되지 않는 유일한 값을 저장하고 관리해야 할 때가 있습니다.
이럴 때 사용할 수 있는 클래스가 HashSet입니다.
HashSet은 고유한 요소들을 저장하는 데 최적화된 컬렉션 클래스입니다. 이번 글에서는 HashSet의 선언 방식, 장단점, 주요 메서드, 사용 예제 등을 자세히 알아보겠습니다.
Hash란 ❓
HashSet을 제대로 이해하기 위해서는 해시(Hash) 개념을 아는 것이 매우 중요합니다. 해시는 컴퓨터 과학에서 데이터 관리와 검색을 효율적으로 하기 위해 사용되는 중요한 개념입니다.
즉 임의의 길이를 갖는 데이터를 고정된 길이의 데이터로 변환(매핑)하는 과정
이때 사용되는 함수가 해시 함수(Hash Function)입니다. 해시 함수는 입력 데이터를 받아, 고정된 크기의 해시 값을 반환합니다.
예로 들어 Park라는 문자열이 있다면 이를 특정 길이(ex 256bit, 512bit...)의 데이터로 변환시킨다는 것입니다. 만약 특정 길이를 32bit로 고정된다고 가정하에 보면 위와 같은 이미지라고 볼 수 있습니다.
이때 32bit는 2진수일 때 기준이므로, 16진수로 보자면 4bit당 한 묶음이기 때문에 16진수로 표현하면 8개의 문자를 얻게 될 것이다. 즉, 해시함수를 돌리기 전 문자열의 길이가 얼마건 일정한 길이를 얻는다는 것입니다.
실제로 대표적인 해시함수인 SHA계열의 SHA-512는 512bit의 고정된 길이를, SHA-256은 256bit의 고정된 길이로 매핑한다는 의미라고 합니다)
이러한 과정을 '해싱(hashing)'하며, 해시함수에 얻어진 값을 보통 다이제스트(digest)라고 합니다.
즉, 해싱 알고리즘은 주어진 입력에서 고정 길이 결과(해시 또는 해시 값)를 생성하는 함수입니다.
그럼 왜 해시함수를 돌려야 하냐면, 해시 함수는 입력 값을 고정된 크기의 해시 값(다이제스트)으로 변환하고, 이 해시 값을 배열의 인덱스로 사용하여 데이터를 저장하거나 찾는 데 활용할 수 있기 때문입니다.
쉽게 말해, 특정 값에 대해 생성된 다이제스트 값을 배열의 특정 위치(index)로 지정해 두는 것과 같다고 볼 수 있습니다. 이를 통해 데이터의 무결성과 보안을 보장하면서도 효율적으로 데이터를 관리할 수 있습니다.
정리하자면 해시 함수는 각 요소를 고유한 해시 값으로 변환하여 HashSet의 배열 인덱스로 사용합니다. 해시 값을 통해 배열의 특정 위치를 지정하고 요소를 저장합니다.
새로운 요소를 추가할 때, 이미 동일한 해시 값을 가진 요소가 존재하는지 검사함으로써 중복된 요소가 저장되지 않도록 합니다. 이를 통해 HashSet은 효율적으로 요소를 관리하면서 중복을 방지할 수 있습니다.
HashSet란 ❓
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
}
}
HashSet은 자바 컬렉션 프레임워크의 한 종류로, 중복되지 않는 고유한 요소들을 저장하는 데 사용되는 클래스입니다. HashSet은 내부적으로 해시 테이블을 사용하여 요소를 저장하고 관리합니다.
HashSet의 다양한 선언방식
기본 생성자 사용
HashSet<String> hashSet = new HashSet<>();
기본 생성자를 사용하여 HashSet을 생성합니다. 초기 용량은 16이고, 적재율(load factor)은 0.75입니다.
초기 용량 지정하여 생성
HashSet<String> hashSet = new HashSet<>(50);
초기 용량을 50으로 지정하여 HashSet을 생성합니다. 적재율은 기본값인 0.75가 사용됩니다.
초기 용량과 적재율을 지정하여 생성
HashSet<String> hashSet = new HashSet<>(50, 0.9f);
초기 용량을 50으로, 적재율을 0.9로 지정하여 HashSet을 생성합니다. 적재율이 높을수록 해시 테이블의 공간 효율성이 높아지지만, 성능은 저하될 수 있습니다.
다른 컬렉션으로부터 생성
HashSet<String> hashSet = new HashSet<>(list);
다른 컬렉션(예: List)의 요소들을 포함하는 HashSet을 생성합니다. 이 방식은 초기화된 컬렉션을 기반으로 HashSet을 만들 때 유용합니다.
제네릭 타입을 사용하여 생성
HashSet<Integer> intSet = new HashSet<>();
HashSet<Double> doubleSet = new HashSet<>();
제네릭을 사용하여 특정 타입의 요소만을 포함하는 HashSet을 생성합니다. 이 방식은 타입 안전성을 보장합니다.
HashSet의 장단점
장점
- 중복 허용 안 함
- HashSet은 중복된 값을 허용하지 않습니다. 따라서 고유한 값들을 저장할 때 유용합니다.
- 빠른 검색 속도
- 해시 테이블 기반으로 구현되어 있어 검색, 삽입, 삭제 작업이 평균적으로 O(1)의 시간 복잡도를 가집니다.
- 순서가 중요하지 않음
- 요소의 순서가 중요하지 않은 경우 유용합니다.
단점
- 순서 보장 안 됨
- HashSet은 요소의 저장
순서를 보장하지 않습니다. 순서가 중요한 경우에는 다른 컬렉션을 사용해야 합니다.
- HashSet은 요소의 저장
- 메모리 사용량
- 해시 테이블 구조로 인해 메모리 사용량이 다소 많을 수 있습니다.
- Null 값 제한
- 하나의 null 값만 허용합니다.
HashSet의 주요 메서드
요소 추가 및 삭제
HashSet<String> hashSet = new HashSet<>();
hashSet.add("Apple"); // "Apple" 추가
System.out.println(hashSet); // 출력: [Apple]
boolean add(E e): 지정된 요소를 집합에 추가합니다. 이미 존재하는 경우 false를 반환합니다.
hashSet.remove("Apple"); // "Apple" 제거
System.out.println(hashSet); // 출력: []
boolean remove(Object o): 지정된 요소를 집합에서 제거합니다. 성공하면 true를 반환합니다.
hashSet.add("Banana");
hashSet.add("Cherry");
hashSet.clear(); // 모든 요소 제거
System.out.println(hashSet); // 출력: []
void clear(): 집합의 모든 요소를 제거합니다.
요소 검사
hashSet.add("Banana");
boolean containsBanana = hashSet.contains("Banana"); // true 반환
System.out.println(containsBanana); // 출력: true
boolean contains(Object o): 지정된 요소가 집합에 포함되어 있는지 검사합니다.
boolean isEmpty = hashSet.isEmpty(); // 비어있는지 확인
System.out.println(isEmpty); // 출력: false
boolean isEmpty(): 집합이 비어 있는지 검사합니다.
int size = hashSet.size(); // 요소 개수 반환
System.out.println(size); // 출력: 1
int size(): 집합의 요소 개수를 반환합니다.
집합 연산
HashSet<String> anotherSet = new HashSet<>();
anotherSet.add("Date");
anotherSet.add("Elderberry");
hashSet.addAll(anotherSet); // anotherSet의 모든 요소 추가
System.out.println(hashSet); // 출력: [Banana, Date, Elderberry]
boolean addAll(Collection <? extends E> c): 지정된 컬렉션의 모든 요소를 집합에 추가합니다.
hashSet.removeAll(anotherSet); // anotherSet의 모든 요소 제거
System.out.println(hashSet); // 출력: [Banana]
boolean removeAll(Collection <?> c): 지정된 컬렉션의 요소를 모두 제거합니다.
hashSet.add("Date");
hashSet.retainAll(anotherSet); // anotherSet에 포함된 요소만 남김
System.out.println(hashSet); // 출력: [Date]
boolean retainAll(Collection <?> c): 지정된 컬렉션에 포함된 요소만을 남기고 나머지는 제거합니다.
요소 접근
Iterator<String> iterator = hashSet.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next()); // 출력: Date
}
Iterator <E> iterator(): 집합의 요소를 반복할 수 있는 Iterator를 반환합니다.
Object[] array = hashSet.toArray();
System.out.println(Arrays.toString(array)); // 출력: [Date]
Object [] toArray(): 집합의 모든 요소를 포함하는 배열을 반환합니다.
String[] array = hashSet.toArray(new String[0]);
System.out.println(Arrays.toString(array)); // 출력: [Date]
<T> T [] toArray(T [] a): 지정된 배열에 집합의 모든 요소를 포함하여 반환합니다.
- 주어진 배열의 크기가 HashSet의 크기보다 작을 때, toArray 메서드는 새로운 배열을 생성하여 반환합니다. new String [0]을 전달하면 항상 새로운 배열이 생성되며, 이는 다음과 같은 이유로 효율적입니다.
- 배열 크기 확인: 메서드는 먼저 주어진 배열의 크기를 확인합니다. 만약 크기가 충분하지 않다면 새로운 배열을 생성합니다.
- 배열 생성 비용: 배열 생성 비용이 상대적으로 적기 때문에, 크기가 0인 배열을 전달하여 새로운 배열을 생성하는 비용이 크지 않습니다.
- 타입 추론: new String [0]을 전달함으로써, 메서드는 반환할 배열의 타입을 정확하게 알 수 있습니다.
즉 new String[0]을 전달하는 방법은 타입 안전성을 보장하면서도 효율적으로 배열을 생성하는 방법입니다. new String [1]을 넣어도 되지만 크기가 0인 배열을 넣는 것이 더 관습적입니다.
import java.util.HashSet;
import java.util.Iterator;
public class HashSetExample {
public static void main(String[] args) {
HashSet<String> hashSet = new HashSet<>();
// 요소 추가
hashSet.add("Apple");
hashSet.add("Banana");
hashSet.add("Cherry");
// 요소 검사
System.out.println("Contains Banana? " + hashSet.contains("Banana"));
System.out.println("Is the set empty? " + hashSet.isEmpty());
System.out.println("Set size: " + hashSet.size());
// 요소 접근
Iterator<String> iterator = hashSet.iterator();
while (iterator.hasNext()) {
System.out.println("Element: " + iterator.next());
}
// 요소 삭제
hashSet.remove("Banana");
System.out.println("After removing Banana: " + hashSet);
// 집합 연산
HashSet<String> anotherSet = new HashSet<>();
anotherSet.add("Date");
anotherSet.add("Elderberry");
hashSet.addAll(anotherSet);
System.out.println("After adding another set: " + hashSet);
hashSet.retainAll(anotherSet);
System.out.println("After retaining only elements in another set: " + hashSet);
hashSet.clear();
System.out.println("After clearing the set: " + hashSet);
}
}
실행 결과
Contains Banana? true
Is the set empty? false
Set size: 3
Element: Apple
Element: Banana
Element: Cherry
After removing Banana: [Apple, Cherry]
After adding another set: [Apple, Cherry, Date, Elderberry]
After retaining only elements in another set: [Date, Elderberry]
After clearing the set: []
정리하자면 HashSet은 중복되지 않는 요소들을 빠르게 관리하는 데 유용한 클래스입니다.
요소의 추가, 삭제, 검사 등의 작업이 매우 효율적으로 이루어지며, 순서에 민감하지 않은 데이터 집합을 관리할 때 매우 적합합니다.
다양한 집합 연산을 통해 다른 HashSet과의 연산도 쉽게 수행할 수 있어, 여러 가지 상황에서 유용하게 활용될 수 있습니다.
'자료구조' 카테고리의 다른 글
[JAVA] Deque (데크, 덱) 자료구조 알아보기 (0) | 2024.07.14 |
---|---|
[JAVA] 그래프 (Graph)알아보기 (0) | 2024.07.07 |
[JAVA] HashMap_entrySet() : 데이터 접근과 반복을 간편하게 하는 방법(2/2) (0) | 2024.06.28 |
[자료구조 JAVA] 우선순위 큐(Priority Queue) 클래스 알아보기 ✔ (0) | 2024.06.16 |
[자료구조 JAVA] 선형 구조 큐(Queue) 클래스 알아보기 ✔ (0) | 2024.06.15 |