숑숑이의 개발일기

본글은 '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
        }
        
    }
}
profile

숑숑이의 개발일기

@숑숑-

풀스택 개발자 준비중입니다