자바의정석 3rd Edition 2권을 참조하였습니다.
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=76083001
HashSet은 set인터페이스를 구현한 가장 대표적인 컬렉션이며,
Set인터페이스의 특징대로 HashSet은 중복된 요소를 저장하지 않는다.
HashSet에 새로운 요소를 추가할 때는 add메서드나 addAll메서드를 사용하는데, 만일 HashSet에 이미 저장되어 있는 요소와 중복된 요소를 추가하고자 한다면 이 메서드들은 false를 반환함으로써 중복된 요소이기 때문에 추가에 실패했다는 것을 알린다.
이러한 HashSet의 특징을 이용하면, 컬렉션 내의 중복 요소들을 쉽게 제거할 수 있다.
ArrayList와 같이 List인터페이스를 구현한 컬렉션과 달리 HashSet은 저장순서를 유지하지 않으므로 저장순서를 유지하고자 한다면 LinkedHashSet을 사용해야 한다.
HashSet은 내부적으로 HashMap을 이용해서 만들어졌으며, HashSet이란 이름은 해싱(hashing)을 이용해서 구현했기 때문에 붙여진 것이다. 해싱(hashing)에 대한 자세한 내용은 HashMap에서 설명한다.
생성자 또는 메서드 | 설명 |
HashSet() | HashSet객체를 생성한다. |
HashSet(Collection c) | 주어진 컬렉션을 포함하는 HashSet객체를 생성한다. |
HashSet(int initialCapacity) | 주어진 값을 초기용량으로하는 HashSet객체를 생성한다. |
HashSet(it initialCapacity, float loadFactor) | 초기용량과 load factor를 지정하는 생성자. |
boolean add(Object o) | 새로운 객체를 저장한다. |
boolean addAll(Collection c) | 주어진 컬렉션에 저장된 모든 객체들을 추가한다.(합집합) |
void clear() | 저장된 모든 객체를 삭제한다. |
Object clone() | HashSet을 복제해서 반환한다.(얕은 복사) |
boolean contains(Object o) | 지정된 객체를 포함하고 있는지 알려준다. |
boolean containsAll(Collection c) | 주어진 컬렉션에 저장된 모든 객체들을 포함하고 있는지 알려준다. |
boolean isEmpty() | HashSet이 비어있는지 알려준다. |
Iterator iterator() | Iterator를 반환한다. |
boolean remove(Object o) | 지정된 객체를 HashSet에서 삭제한다.(성공하면 true, 실패하면 false) |
boolean removeAll(Collection c) | 주어진 컬렉션에 저장된 모든 객체와 동일한 것들을 HashSet에서 모두 삭제한다. (차집합) |
boolean retainAll(Collection c) | 주어진 컬렉션에 저장된 객체와 동일한 것만 남기고 삭제한다. (교집합) |
int size() | 저장된 객체의 개수를 반환한다. |
Object[] toArray() | 저장된 객체들을 객체배열의 형태로 반환한다. |
Object[] toArray(Object[] a) | 저장된 객체들을 주어진 객체배열(a)에 담는다. |
load factor는 컬렉션 클래스에 저장공간이 가득 차기 전에 미리 용량을 확보하기 위한 것으로 이 값을 0.8로 지정하면, 저장공간의 80%가 채워졌을 때 용량이 두 배 늘어난다. 기본값은 0.75, 즉 75%이다.
Ex1
package kr.co.dong.datastructure;
import java.util.*;
public 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]); // HashSet에 objArr 요소들을 저장한다.
}
// HashSet에 저장된 요소들을 출력한다.
System.out.println(set);
}
}
// 실행 결과 [1, 1, 2, 3, 4]
결과에서 알 수 있듯이 중복된 값은 저장되지 않았다.
두 번 출력된 1은 String 1과 Integer인스턴스로 서로 다른 객체이므로 중복으로 간주하지 않는다.
Set을 구현한 컬렉션 클래스는 List를 구현한 컬렉션 클래스와 달리
순서를 유지하지 않기 때문에 저장한 순서와 다를 수 있다.
순서를 유지하고 싶다면 LinkedList를 사용하자.
Ex2
package kr.co.dong.datastructure;
import java.util.*;
public class HashSetLotto {
public static void main(String[] args) {
Set set = new HashSet();
for(int idx = 0; set.size() < 6; idx++) {
int num = (int)(Math.random() *45) + 1;
set.add(new Integer(num));
}
List list = new LinkedList(set); // LinkedList(Collection c)
Collections.sort(list); // Collections.sort(List list)
System.out.println(list);
}
}
// 실행 결과 [14, 20, 23, 25, 33, 44]
중복값을 저장하지 않는 HashSet의 성질을 이용하여 로또번호를 만드는 예제
번호를 크기순으로 정렬하기 위해서 Collections클래스의 sort(List list)사용
이 메서드는 인자로 List인터페이스 타입을 요구하기에 LinkedList로 변환
Ex3
package kr.co.dong.datastructure;
import java.util.*;
public class Bingo {
public static void main(String[] args) {
Set set = new HashSet();
// Set set = new LinkedHashSet();
int[][] board = new int[5][5];
for(int idx = 0; set.size() < 25; idx++) {
set.add((int)(Math.random() * 50)+1+"");
}
Iterator it = set.iterator();
for(int idx = 0; idx < board.length; idx++) {
for(int sidx = 0; sidx < board[idx].length; sidx++) {
board[idx][sidx] = Integer.parseInt((String)it.next());
System.out.print((board[idx][sidx] < 10 ? " " : " ") + board[idx][sidx]);
}
System.out.println();
}
}
}
/*
실행결과
22 45 25 47 26
27 49 29 30 32
11 35 14 15 38
39 17 18 19 2
4 9 20 42 21
*/
1~50사이의 숫자 중에서 25개를 골라서 '5x5'크기의 빙고판을 만드는 예제
next()는 반환값이 Object타입이므로 형변환해서 원래의 타입으로 되돌려 놓아야 한다.
몇번 실행하면 같은 숫자가 비슷한 위치에 나오는데 이는 HashSet은 저장된 순서를 보장하지 않고 자체적인 저장방식에 따라 순서가 결정되기 때문이다. 이 경우에는 HashSet보다 LinkedHashSet이 더 나은 선택이다.
Ex4
package kr.co.dong.datastructure;
import java.util.*;
public class HashSetEx4 {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add(new String("abc"));
set.add(new String("abc"));
set.add(new Person2("David", 10));
set.add(new Person2("David", 10));
System.out.println(set);
}
}
class Person2 {
String name;
int age;
public Person2(String name, int age) {
this.name = name;
this.age = age;
}
public boolean equals(Object obj) {
if(obj instanceof Person2) {
Person2 temp = (Person2)obj;
return name.equals(temp.name) && age == temp.age;
}
return false;
}
public int hashCode() {
return (name + age).hashCode();
// return Objects.hash(name, age) // int hash(Object... values)
// jdk1.8부터 추가된 java.util.Objects클래스의 hash()
}
public String toString() {
return name + ":" + age;
}
}
// 실행 결과 [abc, David:10]
HashSet의 add메서드는 새로운 요소를 추가하기 전에 기존에 저장된 요소와 같은 것인지 판별하기 위해 추가하려는 요소의 equals()와 hashCode()를 호출하기 때문에 equals()와 hashCode()를 목적에 맞게 오버라이딩해야 한다.
오버라이딩을 통해 작성된 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메서드를 호출한 결과이므로 hashCode1이나 hashCode2와 달라도 된다.
String클래스는 문자열의 내용으로 해시코드를 만들어 내기 때문에 내용이 같은 물자열에 대한 hashCode()호출은 항상 동일한 해시코드를 반환한다. 반면에 Object클래스는 객체의 주소로 해시코드를 만들어 내기 때문에 실행할 때마다 해시코드값이 달라질 수 있다.
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값을 반환하는 경우가 있어도 괜찮지만 해싱(hasing)을 사용하는 컬렉션의 성능을 향상시키기 위해서는 다른 int값을 반환하는 것이 좋다.
위의 크도에서 변수 b의 값이 false일지라도 hashCode1과 hashCode2의 값이 같은 경우가 발생하는 것을 허용한다. 하지만, 해시코드를 사용하는 Hashtable이나 HashMap과 같은 컬렉션의 성능을 높이기 위해서는 가능한 한 서로 다른 값을 반환하도록 hashCode()를 잘 작성해야 한다는 뜻이다.
서로 다른 객체에 대해서 해시코드값 (hasCode()를 호출한 결과)이 중복되는 경우가 많아질 수록 해싱을 사용하는 Hashtable, HashMap과 같은 컬렉션의 검색속도가 떨어진다.
두 객체에 대해 equals메서드를 호출한 결과가 true이면, 두 객체의 해시코드는 반드시 같아야하지만, 두 객체의 해시코드가 같다고 해서 equals메서드의 호출결과가 반드시 true이어야 하는 것은 아니다.
사용자정의 클래스를 작성할 때 equals메서드를 오버라이딩해야 한다면 hashCode()도 클래스의 작성의도에 맞게 오버라이딩하는 것이 원칙이지만, 경우에 따라 위의 예제에서와 같이 간단히 구현하거나 생략해도 별 문제가 되지 않으므로 hashCode()를 구현하는데 너무 부담 갖지 않았으면 한다.
Ex5
package kr.co.dong.datastructure;
import java.util.*;
public 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 temp = it.next();
if(setA.contains(temp))
setKyo.add(temp);
}
it = setA.iterator();
while(it.hasNext()) {
Object temp = it.next();
if(setB.contains(temp))
setCha.add(temp);
}
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);
}
}
'Computer Science > DataStructure' 카테고리의 다른 글
HashMap과 Hashtable (0) | 2022.11.18 |
---|---|
TreeSet (0) | 2022.11.18 |
Compareator와 Comparable (0) | 2022.11.15 |
Arrays (0) | 2022.11.15 |
Iterator with Vector (0) | 2022.11.14 |