[Java] 컬렉션 프레임워크 - HashSet
HashSet은 Set인터페이스를 구현한 가장 대표적인 컬렉션 이며, Set인터페이스의 특징대로 HashSet은 중복된 요소를 저장하지 않는다.
HashSet에 새로운 요소를 추가할 때는 add메서드나 addAll메서드를 사용하는데, 만일 HashSet에 이미 저장되어 있는 요소와 중복된 요소를 추가하고자 한다면 이 메서드들은 false를 반환함으로써 중복된 요소이기 때문에 추가에 실패했음을 알린다.
이러한 HashSet의 특징을 이용하면, 컬렉션 내의 중복 요소들을 쉽게 제거할 수 있다.
생성자 또는 메서드 | 설명 |
HashSet() | HashSet객체를 생성한다. |
HashSet(Collection c) | 주어진 컬렉션을 포함하는 HashSet객체를 생성한다. |
HashSet(int initialCapacity) | 주어진 값을 초기용량으로 하는 HashSet객체를 생성한다. |
HashSet(int initialCapacity, float loadFactor) | 초기용량과 load factor를 지정하는 생성자. |
boolean add(Object o) | 새로운 객체를 저장한다. |
boolean addAll(Collection c) | 주어진 컬렉션에 저장된 모든 객체들을 추가한다.(합집합) |
void clear() | 저장된 모든 객체를 삭제한다. |
Object clone() | HashSet을 복제해서 반환한다.(얕은 복사) |
boolean contains(Obejct o) | 지정된 객체를 포함하고 있는지 알려준다. |
boolean containsAll(Collection c) | 주어진 컬렉션에 저장된 모든 객체들을 포함하고 있는지 알려준다. |
boolean isEmpty() | HashSet이 비어있는지 알려준다. |
Iterator iterator() | Iterator를 반환한다. |
boolean remove(Object o) | 지정된 객체를 HashSet에서 삭제한다. |
boolean remveAll(Collection c) | 주어진 컬렉션에 저장된 모든 객체와 동일한 것들을 HashSet에서 삭제한다.(차집합) |
boolean retainAll(Collection c) | 주어진 컬렉션에 저장된 객체와 동일한 것만 남기고 삭제한다.(교집합) |
int size() | 저장된 객체의 개수를 반환한다. |
Object[] toArray() | 저장된 객체들을 객체배열의 형태로 반환한다. |
Object[] toArray(Object[] a) | 저장된 객체들을 주어진 객체배열(a)에 담는다. |
|참고| load factor는 컬렉션 클래스에 저장공간이 가득 차기 전에 미리 용량을 확보하기 위한 것으로 이 값을 0.8로 지정하면, 저장공간의 80%가 채워졌을 때 용량이 두 배로 늘어난다. 기본값은 0.75
// HashSetEx1.java
import java.util.*;
class HashSetEx1 {
public static void main(String[] args) {
Object[] objArr = {"1", new Integer(1), "2", "2", "3", "3", "4", "4", "4"};
Set set = new HashSet();
for(int i=0; i<objArr.length; i++) {
set.add(objArr[i]);
}
System.out.println(set);
}
}
결과에서 알 수 있듯이 중복된 값은 저장되지 않는다. add메서드는 객체를 추가할 때 HashSet에 이미 같은 객체가 있으면 중복으로 간주하고 저장하지 않는다. 그리고는 작업이 실패했다는 의미로 false를 반환한다.
'1'이 두 번 출력되었는데, 둘 다 '1'로 보이기 때문에 구별이 안되지만, 사실 하나는 String 인스턴스이고 다른 하나는 Integer인스턴스로 서로 다른 객체이므로 중복으로 간주하지 않는다.
Set을 구현한 컬렉션 클래스는 List를 구현한 컬렉션 클래스와 달리 순서를 유지하지 않기 때문에 저장한 순서와 다를 수 있다.
만일 중복을 제거하는 동시에 저장한 순서를 유지하고자 한다면 HashSet 대신 Linked HashSet을 사용해야 한다.
import java.util.*;
class HashSetLotto {
public static void main(String[] args) {
Set set = new HashSet();
for(int i=0; i<set.length; i++) {
int num = (int) (Math.random() * 45) + 1;
set.add(new Integer(num))
}
List list = new LinkedList(set);
Collections.sort(list);
System.out.println(list);
}
}
중복된 값은 저장되지 않는 HashSet의 성질을 통해서 로또번호를 만드는 예제이다. Math.random()을 사용했기 때문에 실행할 때 마다 결과가 다를 것이다.
번호를 크기 순으로 정렬하기 위해서 Collections클래스의 sort(List list)를 사용했다. 이 메소드는 인자로 List인터페이스 타입을 필요로 하기 때문에 LinkedList클래스의 생성자 LinkedList(Collection c)를 이용해서 HashSet에 저장된 객체들을 LinkedList에 담아서 처리했다.
실행결과의 정렬기준은 컬렉션에 저장된 객체가 integer이기 때문에 Integer클래스에 정의된 기본정렬이 사용되었다.
import java.util.*;
class Bingo {
public static void main(String[] args) {
Set set = new HashSet();
Set set = new LinkedHashSet();
int[][] board = new int[5][5];
for(int i=0; set.size()<25; i++) {
set.add((int)(Math.random()*50 + 1+""));
}
Iterator it = set.iterator
for(int i=0; i< board.length; i++) {
for(int j=0; j< board.length; j++){
board[i][j] = Integer.parseInt((String)it.next());
System.out.print(board[i][j] < 10 ? " " : " ") + board[i][j];
}
System.out.println();
}
}
}
1부터 50까지의 숫자 중에서 25개를 골라 5x5 크기의 빙고판을 만드는 예제이다. next()는 반환값이 Object타입이므로 형변환해서 원래의 타입으로 되돌려 놓아야 한다. 이 예제 역시 random()을 사용했기 때문에 실행할 때마다 다른 결과를 얻을 것이다.
그런데 몇번 실행해보면 같은 숫자가 비슷한 위치에 나온다는 사실을 발견할 수 있을 것이다. 앞서 언급한것과 같이 HashSet은 저장된 순서를 보장하지 않고 자체적인 저장방식에 따라 순서가 결정되기 때문이다. 이 경우에는 HashSet보다 LinkedHashSet이 더 나은 선택이다.
import java.util.*;
class HashSetEx3 {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add("abc");
set.add("abc");
set.add(new Person("David", 10));
set.add(new Person("David", 10));
System.out.println(set);
}
}
}
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + ":" + age;
}
}
Person 클래스는 name과 age를 멤버변수로 갖는다. 이름과 나이가 같으면 같은 사람으로 인식하도록 하려는 의도로 작성되었다. 하지만 실행결과를 보면 두 인스턴스의 name과 age의 값이 같음에도 불구하고 서로 다른 것으로 인식하여 'David:10'이 두 번 출력되었다.
클래스의 작성의도대로 이 두 인스턴스를 같은 것으로 인식하게 하려면 어떻게 해야할까?
import java.util.*;
class HashSetEx4 {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add("abc");
set.add("abc");
set.add(new Person2("David", 10));
set.add(new Person2("David", 10));
System.out.println(set);
}
}
}
class Person2 {
String name;
int age;
Person2(String name, int age) {
this.name = name;
this.age = age;
}
public boolean equals(Object obj) {
if(obj instanceof Person2) {
Person2 tmp = (Person2) obj;
return name.equals(tmp.name) && age == tmp.age;
}
return false;
}
public int hashCode(){
return (name+age).hashCode();
}
public String toString() {
return name + ":" + age;
}
}
HashSet의 add메서드는 새로운 요소를 추가하기 전에 기존에 저장된 요소와 같은 것인지 판별하기 위해 추가하려는 요소의 equals()와 hashCode()를 호출하기 때문에 equals()와 hashCode()를 목적에 맞게 오버라이딩 해야한다.
그래서 String 클래스에서 같은 내용의 문자열에 대한 equals()의 호출결과가 true인 것과 같이, Person2클래스에서도 두 인스턴스의 name과 age가 서로 같으면 true를 반환하도록 equals()를 오버라이딩 했다. 그리고 hashCode()는 String 클래스의 hashCode()를 이용해서 구현했다. String클래스의 hashCode()는 잘 구현되어 있기 때문에 이를 활용하면 간단히 처리할 수 있다.
public int hashCode(){
return (name+age).hashCode();
}
위 코드를 아래처럼 java.util.Objects 클래스의 hash()를 이용해서 작성할 수 있다. 메서드의 괄호안에 클래스의 인스턴스 변수들을 넣으면 된다. 위 코드와 별반 다르지 않지만, 가능하면 아래 코드를 쓰자.
public int hashCode() {
return Objects.hash(name, age);
}
오버라이딩을 통해 작성된 hashCode()는 다음의 세 가지 조건을 만족 시켜야 한다.
1. 실행중인 애플리케이션 내의 동일한 객체에 대해서 여러 번 hashCode()를 호출해도 동일한 int값을 반환해야한다. 하지만, 실행시마다 동일한 int값을 반환할 필요는 없다.(단, equals메서드의 구현에 사용된 멤버변수의 값이 바뀌지 않았다고 가정한다.)
예를 들어, Person2 클래스의 equals메서드에 사용된 멤버변수 name과 age의 값이 바뀌지 않는 한, 하나의 Person2인스턴스에 대해 hashCode()를 여러 번 호출했을 때 항상 같은 int 값을 얻어야 한다.
Person2 p = new Person2("David", 10);
int hashCode1() = p.hashCode();
int hashCode2() = p.hashCode();
p.age = 20;
int hashCode3 = p.hashCode();
위의 코드에서 hashCode1의 값과 hashCode2의 값은 항상 일치해야하지만 이 두 값이 매번 실행할 때마다 반드시 같은 값일 필요는 없다. hashCode3은 equals메서드에 사용된 멤버변수 age를 변경한 후에 hashCode메서드를 호출한 결과이므로 ahshCode1이나 hashCode2와 달라도 된다.
2. equals메서드를 이용한 비교에 의해서 true를 얻은 두 객체에 대해 각각 hashCode()를 호출해서 얻은 결과는 반드시 같아야 한다.
인스턴스 p1과 p2에 대해서 equals메서드를 이용한 비교의 결과인 변수 b의 값이 true라면, hashCode1과 hashCode2의 값은 같아야 한다는 것이다.
Person2 p1 = new Person2("David", 10);
Person2 p2 = new Person2("David", 10);
boolean b = p1.equals(p2);
int hashCode1 = p1.hashCode();
int hashCode2 = p2.hashCode();
3. equals메서드를 호출했을 때 false를 반환하는 두 객체는 hashCode() 호출에 대해 같은 int값을 반환하는 경우가 있어도 괜찮지만, hashing을 사용하는 컬렉션의 성능을 향상시키기 위해서는 다른 int값을 반환하는 것이 좋다.
위의 코드에서 변수 b의 값이 false일지라도 hashCode1과 hashCode2의 값이 같은 경우가 발생하는 것을 허용한다. 하지만, 해시코드를 사용하는 Hashtable이나 HashMap과 같은 컬렉션의 성능을 높이기 위해서는 가능한 한 서로 다른 값을 반환하도록 hashCode()를 작성해야 한다는 것이다.
서로 다른 객체에 대해 해시코드값이 중복되는 경우가 많아질수록 해싱을 사용하는 컬렉션의 검색속도가 떨어진다.
두 객체에 대해 equals 메서드를 호출한 결과가 true 이면, 두 객체의 해시코드는 반드시 같아야하지만, 두 객체의 해시코드가 같다고 해서 equals메서드의 호출결과가 반드시 true이어야 하는 것은 아니다.
사용자 정의 클래스를 작성할 때 equals 메서드를 오버라이딩해야 한다면 hashCode()도 클래스의 작성의도에 맞게 오버라이딩하는 것이 원칙이지만, 경우에 따라 위의 에제에서와 같이 간단히 구현하거나 생략해도 별 문제가 되지 않으므로 hashCode()를 구현하는데 너무 부담 갖지 않았으면 한다.
import java.util.*;
class HashSetEx5 {
public static void main(String[] args) {
HashSet setA = new HashSet();
HashSet setB = new HashSet();
HashSet setHab = new HashSet();
HashSet setKyo = new HashSet();
HashSet setCha = new HashSet();
setA.add("1"); setA.add("2"); setA.add("3");
setA.add("4"); setA.add("5");
System.out.println("A = " + setA);
setB.add("4"); setB.add("5"); setB.add("6");
setB.add("7"); setB.add("8");
System.out.println("B = " + setB);
Iterator it = setB.iterator();
while(it.hasNext()) {
Object tmp = it.next();
if (setA.contains(tmp))
setKyo.add(tmp);
}
it = setA.iterator();
while(it.hasNext()) {
Object tmp = it.next();
if (!setB.contains(tmp))
setCha.add(tmp);
}
it = setA.iterator();
while(it.hasNext)
setHab.add(it.next());
it = setB.iterator();
while(it.hasNext)
setHab.add(it.next());
System.out.println("A ∩ B = " + setKyo);
System.out.println("A ∪ B = " + setHab);
System.out.println("A - B = " + setCha);
}
}
이 예제는 두 개의 HashSet에 저장된 객체들을 비교해서 합집합, 교집합, 차집합을 구하는 방법을 보여준다. 사실 Set은 중복을 허용하지 않으므로 HashSet의 메서드를 호출하는 것만으로도 간단하게 합집합, 교집합, 차집합을 구할 수 있다.