본글은 'Do it 자바 완전 정복' 책을 통한 공부내용을 정리한 글입니다.
Set<E> 컬렉션의 특징
Set<E>는 동일한 타입의 묶음이라는 특징은 그대로 갖고 있지만, 인덱스 정보를 포함하고 있지않아 집합의 개념과 같은 컬렉션이다. 인덱스 정보가 없으므로 데이터를 중복해 저장할 경우 중복된 데이터 중 특정 데이터를 지칭해 꺼낼 수 있는 방법이 없다. 따라서 동일한 데이터의 중복 저장을 허용하지 않는다.
Set<E>의 주요 메서드
Set<E>에는 데이터의 추가, 삭제, 정보 추출, Set<E>를 배열로 변환하는 메서드들이 포함되어 있다.
구분 | 리턴타입 | 메서드명 | 기능 |
데이터 추가 | boolean | add(E element) | 매개변수로 입력된 원소를 리스트에 추가 |
boolean | addAll(Collection<? Extends E> c) | 매개변수로 입력된 컬렉션 전체를 추가 | |
데이터 삭제 | boolean | remove(Object o) | 원소 중 매개변수 입력과 동일한 객체 삭제 |
void | clear() | 전체 원소 삭제 | |
데이터 정보 추출 | boolean | isEmpty() | Set<E> 객체가 비어 있는지 여부를 리턴 |
boolean | contains(Object o) | 매개변수로 입력된 원소가 있는지 여부를 리턴 | |
int | size() | 리스트 객체 내에 포함된 원소의 개수 | |
Iterator<E> | iterator() | Set<E> 객체 내의 데이터를 연속해 꺼내는 Iterator객체 리턴 | |
Set<E> 객체 배열 변환 | Object[] | toArray() | 리스트를 Object 배열로 변환 |
T[] | toArray(T[] t) | 입력매개변수로 전달한 타입의 배열로 변환 |
앞의 List<E>에서 소개되지 않았던 2개의 메서드에 대한 설명을 덧붙인다. contains(Object o)는 해당 Set<E> 매개변수로 넘어온 데이터가 객체 내에 포함되어 있는지 boolean 값으로 리턴한다.
iterator() 메서드는 Iterator<E> 객체를 리턴하는데, 이 객체는 Set<E> 객체에서 데이터를 1개씩 꺼내는 기능을 포함하고 있다. Set<E>는 인덱스 정보를 갖고 있지 않으므로 일반적인 for 문으로 데이터를 꺼낼 수 없어 iterator() 메서드를 사용해 모든 데이터를 1개씩 모두 꺼낼 수 있다. 해당 방법 외에도 for-each 구문을 통해 꺼낼 수 있다.
Set<E>도 인터페이스이므로 자체적으로 객체를 생성할 수 없고, 자바 컬렉션 프레임워크에서 제공하는 Set<E> 인터페이스를 상속한 구현 클래스를 이용해야 한다. 대표적으로 HashSet<E>, LinkedHashSet<E>, TreeSet<E> 클래스가 있다.
HashSet<E> 구현 클래스
HashSet<E>은 Set<E> 인터페이스의 대표적인 구현 클래스다. HashSet<E> 컬렉션도 저장 용량(capacity)을 동적 관리하며, 기본 생성자로 생성할 때 기본값은 16이다. 이후 데이터의 개수가 많아지면 동적으로 증가한다.
데이터 추가 - add()
Set<String> hSet1 = new HashSet<String>();
// 1. add(E element)
hSet1.add("가");
hSet1.add("나");
hSet1.add("가");
System.out.println(hSet1.toString()); // [가, 나]
// 2. addAll(Collection<? extends E> c)
Set<String> hSet2 = new HashSet<String>();
hSet2.add("나");
hSet2.add("다");
hSet2.addAll(hSet1);
System.out.println(hSet2.toString()); // [가, 나, 다]
HashSet<E>은 중복된 데이터를 추가하지 않는다. 그리고 모든 데이터를 하나의 주머니에 넣어 관리하므로 입력 순서와 다르게 출력될 수 있다.
데이터 삭제 - remove(), clear()
// 3. remove(Object o)
// hSet2 = [가, 나, 다]
hSet2.remove("나");
System.out.println(hSet2.toString()); // [가, 다]
// 4. clear()
hSet2.clear();
System.out.println(hSet2.toString()); // []
Set<E> 객체에는 인덱스 번호가 없으므로 데이터를 삭제하려면 remove() 메서드의 매개변수로 실제 삭제할 원솟값을 넣어야 한다.
데이터 정보 추출 - isEmpty(), contains(), size(), iterator()
// hSet2 = []
// 5. isEmpty()
System.out.println(hSet2.isEmpty()); // true
// 6. contains(Object o)
Set<String> hSet3 = new HashSet<String>();
hSet3.add("가"); hSet3.add("나"); hSet3.add("다");
System.out.println(hSet3.contains("나")); // true
System.out.println(hSet3.contains("라")); // false
// 7. size()
System.out.println(hSet3.size()); // 3
// 8. iterator()
Iterator<String> iterator = hSet3.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next() + " "); // 가 나 다
}
iterator()는 Set<E> 객체 내부의 데이터를 1개씩 꺼내 처리하고자 할 때 사용하는 메서드다. 해당 메서드를 호출하면 먼저 제네릭 클래스 타입인 Iterator<E>의 객체가 생성된다. 제네릭 타입은 당연히 HashSet<E>가 저장하고 있는 원소의 타입이다. Iterator의 사전적 의미는 '반복한다'인데, 각각의 데이터의 위치를 번갈아 가며 가리킬 수 있다. 대표적인 메서드 2개만 정리한다.
리턴타입 | 메서드 | 기능 |
boolean | hasNext() | 다음으로 가리킬 원소의 존재 여부를 불리언으로 리턴 |
E | next() | 다음 원소 위치로 가서 읽은 값을 리턴 |
hasNext() 메서드는 리턴값이 false일 때 마지막 데이터까지 읽은 것이다. 주의할 점은 최초 Iterator<E> 객체가 생성되면 이 객체가 가리키고 있는 위치는 첫 원소 위치가 아닌 첫 원소 바로 이전의 위칫값이라는 것이다. 즉, 첫 번째 원솟값을 읽는 것은 iterator.next()를 사용해야 한다.
배열로 변환 - toArray(), toArray(T[] t)
// hSet3 = [가, 나, 다]
// 9. toArray()
Object[] objArr = hSet3.toArray();
System.out.println(Arrays.toString(objArr)); // 가, 나, 다
// 10-1. toArray(T[] t)
String[] strArr1 = hSet3.toArray(new String[0]);
System.out.println(strArr1.toString(objArr)); // 가, 나, 다
// 10-2. toArray(T[] t)
String[] strArr2 = hSet3.toArray(new String[5]);
System.out.println(strArr2.toString(objArr)); // 가, 나, 다, null, null
HastSet<E>의 중복 확인 매커니즘
3과 3은 같은 데이터다. A a1 = new A(3)
과 A a2 = new A(3)
도 생성자에 동일한 값을 넘겨 객체를 생성했으므로 두 객체는 완전히 똑같이 생겼을 것이다. 하지만 클래스 A에 특별한 처리가 있는것이 아니라면 HashSet<E>의 관점에서는 다른 객체다. 즉, 둘 다 저장할 수 있다는 것.
어떤 차이가 있는지 이해하기 위해서는 해시코드(hashcode)의 개념을 알아야한다. 객체의 해시코드는 객체가 저장된 번지와 연관된 값으로, 실제 번짓값은 아니다. 해시코드는 객체가 저장된 번지를 기준으로 생성된 정수형 고윳값이다. hashCode()
는 Object 클래스에 정의되어 있고 객체의 해시코드값을 리턴한다. 하위 클래스에서 toString()
메서드로도 해시코드값을 확인할 수 있다.
HashSet<E>에서의 중복 확인 매커니즘은 다음과 같이 2단계로 처리된다.
1) 두 객체의 해시코드가 동일한가?
2) equals() 메서드의 결과가 true인가?
위의 2단계를 통과시에 두 객체는 동일한 객체로 인식(즉, 중복된 값으로 인식)하고, 이외에는 다른 객체로 인식한다. 하위클래스에서 hashCode()와 equals() 메서드를 오버라이딩을 수행했다면 객체의 동등 여부 결과가 달라질 수 있다.
import java.util.*;
class A {
int data;
public A(int data) {
this.data = data;
}
}
class B {
int data;
public B(int data) {
this.data = data;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof B) {
this.data = ((B)obj).data;
return true;
}
return false;
}
}
class C {
int data;
public C(int data) {
this.data = data;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof B) {
this.data = ((C)obj).data;
return true;
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(data);
}
}
public class HashSetMachanism {
public static void main(String[] args) {
// 1. 오버라이딩 하지 않은경우
Set<A> hashSet1 = new HashSet<>();
A a1 = new A(3);
A a2 = new A(3);
System.out.println(a1 == a2); // false
System.out.println(a1.equals(a2)); // false
hashSet1.add(a1); hashSet1.add(a2);
System.out.println(hashSet1.size()); // 2
// 2. equals() 메서드 오버라이딩
Set<B> hashSet2 = new HashSet<>();
B b1 = new B(3);
B b2 = new B(3);
System.out.println(b1 == b2); // false
System.out.println(b1.equals(b2)); // true
hashSet2.add(b1); hashSet2.add(b2);
System.out.println(hashSet1.size()); // 2
// 3. equals(), hashCode() 메서드 오버라이딩
Set<A> hashSet3 = new HashSet<>();
C c1 = new C(3);
C c2 = new C(3);
System.out.println(c1 == c2); // true
System.out.println(c1.equals(c2)); // true
// 2단계까지의 결과가 모두 true 이므로 같은 값으로 인식됨
hashSet3.add(c1); hashSet3.add(c2);
System.out.println(hashSet1.size()); // 1
}
}
LinkedHashSet<E> 구현 클래스
LinkedHashSet<E>는 HashSet<E>의 자식 클래스로 HashSet<E>의 모든 기능에 데이터 간의 연결 정보를 추가로 갖고 있는 컬렉션이다. 즉, 입력된 순서를 기억하고 있는 것. 고로 LinkedHashSet<E>는 출력순서가 항상 입력 순서와 동일한 특징을 갖고 있다. 입력 순서의 정보를 가지고 있지만 List<E>처럼 중간에 데이터를 추가 하거나 특정 순서의 저장된 값을 가져오는 것은 불가능 하다.
LinkedHashSet<E>의 모든 메서드는 출력 순서가 입력 순서와 동일하다는 점을 제외하고 HashSet<E>와 동일하다. (결과값에서 출력 순서만 차이가 나는것)
import java.util.*;
public class LinkedHashSetMethod {
public static void main(String[] args) {
Set<String> linkedSet1 = new LinkedHashSet<>();
linkedSet1.add("가");
linkedSet1.add("나");
linkedSet1.add("가");
System.out.println(linkedSet1.toString()); // [가, 나]
Set<String> linkedSet2 = new LinkedHashSet<>();
linkedSet2.add("나");
linkedSet2.add("다");
linkedSet2.addAll(linkedSet1);
System.out.println(linkedSet2.toString()); // [나, 다, 가]
}
}
TreeSet<E> 구현 클래스
TreeSet<E>는 공통적인 Set<E>의 기능에 크기에 따른 정렬 및 검색 기능이 추가된 컬렉션이다. TreeSet<E>는 데이터 입력 순서와 상관없이 크기순으로 출력한다. TreeSet<E>는 다른 구현 클래스와 달리 Navigable Set<E>와 SortedSet<E>를 부모 인터페이스로 두고 있다. 따라서 TreeSet<E> 생성자로 객체를 생성해도 Set<E> 타입으로 선언시 추가된 정렬 및 검색 기능을 사용할 수 없다.
Set<String> treeSet = new TreeSet<String>();
// Set<E> 메서드만 사용 가능
TreeSet<String> treeSet = new TreeSet<String>();
// Set<E> 메서드와 정렬/검색 기능의 메서드도 사용 가능
TreeSet<E>의 주요 메서드
TreeSet<E>이 Set<E>의 구현 클래스이므로 Set<E>의 모든 메서드를 사용할 수 있고 활용법 또한 동일하다. 아래 표에서는 TreeSet<E>에서 추가된 정렬과 검색 관련 메서드에 대해 알아보자.
구분 | 리턴 타입 | 메서드명 | 기능 |
데이터 검색 | E | first() | Set의 원소 중 가장 작은 원솟값 리턴 |
E | last() | Set의 원소 중 가장 큰 원솟값 리턴 | |
E | lower(E element) | 매개변수로 입력된 원소보다 작은, 가장 큰 수 | |
E | higher(E element) | 매개변수로 입력된 원소보다 큰, 가장 작은수 | |
E | floor(E element) | 매개변수로 입력된 원소보다 같거나 작은 가장 큰수 | |
E | ceiling(E element) | 매개변수로 입력된 원소보다 같거나 큰 가장 작은 수 | |
데이터 추출 | E | pollFirst() | Set 원소들 중 가장 작은 원솟값을 꺼내 리턴 |
E | pollLast() | Set 원소들 중 가장 큰 원솟값을 꺼내 리턴 | |
데이터 부분 집합 생성 |
SortedSet<E> | headSet(E toElement) | toElement 미만인 모든 원소로 구성된 Set을 리턴 (toElement 미포함) |
NavigableSet<E> | headSet(E toElement, boolean inclusive) | toElement 미만/이하인 모든 원소로 구성된 Set을 리턴 (inclusive=true라면 toElement를 포함하고 false라면 미포함) |
|
SortedSet<E> | tailSet(E fromElement) | fromElement 이상인 모든 원소로 구성된 Set을 리턴 (fromElement 포함) |
|
NavigableSet<E> | tailSet(E fromElement, boolean inclusive) | fromElement 초과/이상인 모든 원소로 구성된 Set을 리턴 (inclusive=true라면 fromElement 를 포함하고 false라면 미포함) |
|
SortedSet<E> | subSet(E fromElement, E toElement) | fromElement 이상 toElement 미만인 원소들로 이뤄진 Set을 리턴(fromElement 포함, toElement 미포함) | |
NavigableSet<E> | subSet(E fromElement, boolean frominclusive, E toElement, boolean toinclusive) | fromElement 이상 toElement 미만/이하인 원소들로 이뤄진 Set을 리턴(frominclusive=true라면 fromElement 를 포함 false라면 미포함, toinclusive=true라면 toElement를 포함 false라면 미포함, ) | |
데이터 정렬 | NavigableSet<E> | descendingSet() | 내림차순의 의미가 아닌 현재 정렬 기준을 반대로 변환 (오름차순-> 내림차순, 내림차순-> 오름차순) |
pollFirst()와 pollLast()는 TreeSet<E>에서 값을 꺼내 리턴하므로 검색 메서드와 달리 수행 후 데이터의 개수가 줄어든다.
데이터 부분 집합을 생성하는 메서드의 종류가 많다. 간단히 메서드의 매개변수에 boolean 타입이 들어가지 않으면 SortedSet<E>를 리턴, 들어가면 NavigableSet<E> 타입을 리턴한다고 생각하면 된다.
TreeSet<E>에서 데이터 크기 비교
정렬을 위해서는 각 데이터들의 크고 작음을 비교할 수 있어야 한다.
TreeSet<Integer> treeSet1 = new TreeSet<Integer>();
Integer intV1 = new Integer(20);
Integer intV2 = new Integer(10);
treeSet1.add(intV1);
treeSet1.add(intV2);
System.out.println(treeSet1.toString()); // [10, 20]
TreeSet<Integer> treeSet2 = new TreeSet<Integer>();
String str1 = "가나";
String str2 = "다라";
treeSet2.add(str1);
treeSet2.add(str2);
System.out.println(treeSet2.toString()); // [가나, 다라]
문자열 일때에는 사전 순서로 비교되어 "다라"가 더 큰 값을 가진다.
이제 직접 클래스를 생성해 크기를 비교하는 예시를 살펴보자. treeSet<E>에 저장되는 모든 객체는 크기 비교의 기준이 제공되어야 하며 기준이 꼭 필드 값일 필요는 없다. 기준을 제공하는 방법은 아래 2가지다.
1) java.lang 패키지의 Comparable<T> 제네릭 인터페이스 구현
2) TreeSet<E> 객체를 생성하며 생성자의 매개변수로 Comparator<T> 객체를 제공
import java.util.*;
class MyClass {
int data1;
int data2;
public MyClass(int data1, int data2) {
this.data1 = data1;
this.data2 = data2;
}
}
class MyComparableClass implements Comparable<MyComparableClass> {
int data1;
int data2;
public MyComparableClass(int data1, int data2) {
this.data1 = data1;
this.data2 = data2;
}
// 크기 비교의 기준 설정(음수, 0, 양수)
@Override
public int compareTo(MyComparableClass o) {
if (this.data1 < o.data1) return -1;
else if (this.data1 == o.data1) return 0;
else return 1;
}
}
public class TreeSetMethod2 {
public static void main(String[] args) {
// 방법1
TreeSet<MyComparableClass> treeSet1 = new TreeSet<MyComparableClass>();
MyComparableClass myComparableClass1 = new MyComparableClass(2, 5);
MyComparableClass myComparableClass2 = new MyComparableClass(3, 3);
treeSet1.add(myComparableClass1);
treeSet1.add(myComparableClass2);
for(MyComparableClass mcc: treeSet1) {
System.out.println(mcc.data1); // 2, 3
}
// 방법2
TreeSet<MyComparableClass> treeSet2 = new TreeSet<MyClass>(new Comparator<MyClass>() {
@Override
public int compare(MyClass o1, MyClass o2) {
if(o1.data1 < o2.data1) return -1;
else if(o1.data1 == o2.data1) return 0;
else return 1;
}
});
MyClass myClass1 = new MyClass(2, 5);
MyClass myClass2 = new MyClass(3, 3);
treeSet2.add(myClass1);
treeSet2.add(myClass2);
for(MyComparableClass mcc: treeSet2) {
System.out.println(mcc.data1); // 2, 3
}
}
}
'Backend > JAVA' 카테고리의 다른 글
[Java] #17.5-6 Stack<E> 컬렉션 클래스, Queue<E> 컬렉션 인터페이스 (0) | 2023.08.09 |
---|---|
[JAVA] #17.4 Map<K, V> 컬렉션 인터페이스 (0) | 2023.07.20 |
[JAVA] #17 컬렉션 프레임워크, List<E> 컬렉션 인터페이스 (1) | 2023.07.18 |
[JAVA] #16 제네릭, 제네릭 클래스와 인터페이스, 제네릭 메서드 (1) | 2023.07.16 |
[JAVA] #15.5 쓰레드의 상태 (0) | 2023.07.15 |