도도한 개발자

[Java] #23-1. Collection(컬렉션) - Set 본문

Backend/Java

[Java] #23-1. Collection(컬렉션) - Set

Kiara Kim 2022. 4. 8. 20:00

* Set이 왜 필요할까?

 

Collection을 확장한 배열과 비슷한 역할을 하는 3개의 인터페이가 있다고 했다. List, Set, Queue가 그것들인데 List는 순서가 중요한 데이터를 담을 때 사용되는 반면 Set은 어디에 사용되는 것일까?

 

어떤 값이 존재하는지, 없는지 여부만 중요할 때 Set을 사용하면 된다. 더 나아가 HashSet의 예제를 보자. 자바에서 Set 인터페이스를 구현한 주요 클래스는 Hashset, TreeSet, LinkedHashSet이 있다.

 

· HashSet : 순서가 전혀 필요 없는 데이터를 해시 테이블에 저장한다. Set 중에 가장 성능이 좋다.

· TreeSet : 저장된 데이터의 값에 따라 정렬되는 셋이다. red-black이라는 트리 타입으로 값이 저장되며, HashSet보다 성능이 약간 느리다.

· LinkedHashSet : 연결된 목록 타입으로 구현된 해시 테이블에 데이터를 저장한다. 저장된 순서에 따라 값이 정렬되며, 이 셋 중 가장 성능이 나쁘다.

 

 

* HashSet에 대하여

 

HashSet 클래스의 상속 관계를 보자.

java.lang.Object
  ㄴ java.util.AbstractCollection<E>
    ㄴ java.util.AbstractSet<E>
      ㄴ java.util.HashSet<E>

 

AbstractCollection을 확장한 것은 ArrayList와 동일한다. 그러나 HashSet은 AbstractSet을 확장했다. AbstractSet 클래스는 이름 그대로 abstract 클래스이다. 구현되어 있는 메소드는 Object 클래스에 선언되어 있는 equals(), hashCode() 메소드와 이 클래스에서 추가한 removeAll() 뿐이다.

 

equals()와 hashCode() 메소드를 구현하는 부분은 Set에서 매우 중요하다. 추가로 removeAll() 메소드는 컬렉션을 매개 변수로 받아, 매개 변수 컬렉션에 포함된 모든 데이터를 지울 때 사용한다. HashSet은 다음의 인터페이스를 구현했다.

Serializable, Cloneable, Iterable<E>, Collection<E>, Set<E>

 

여기서 Set 인터페이스에 정의되어 있는 메소드는 대부분 List와 비슷하다. 그럼 두개의 차이점은 무엇일까? 

Set은 순서가 없다. 따라서 순서가 매개 변수로 넘어가는 메소드나, 수행 결과가 데이터의 위치와 관련된 메소드는 Set 인터페이스에서는 필요가 없다. 그러므로 get(int index)나 indexOf(Object o)와 같은 메소드들은 Set에 존재하지 않는다.

 

* HashSet의 여러 생성자들

 

생성자 설명
HashSet() 데이터를 저장할 수 있는 16개의 공간과 0.75의 로드 팩터(load factor)를 갖는 객체를 생성한다.
HashSet(Collection<? extends E> c) 매개 변수로 받은 컬렉션 객체의 데이터를 HashSet에 담는다.
HashSet(int initialCapacity) 매개 변수로 받은 개수만큼의 데이터 저장 공간과 0.75의 로드 팩터를 갖는 객체를 생성한다.
HashSet(int initialCapacity, float loadFactor) 첫 매개 변수로 받은 객수만큼의 데이터 저장 공간과 두 번째 매개 변수로 받은 만큼의 로드 팩터를 갖는 객체를 생성한다.

 

여기서의 로드 팩터(load factor)란 무엇일까? 로드 팩터는 (데이터의 개수)/(저장 공간)을 의미한다. 만약 데이터의 개수가 증가해 로드 팩터보다 커지면 저장 공간의 크기는 증가되고, 해시 재정리 작업을 해야 한다. 나같은 초급 개발자는 로드 팩터를 건드릴 필요가 거의 없으므로 매개 변수가 없는 첫 번째 생성자를 사용하거나, 세 번째에 있는 초기 크기만 지정하는 생성자를 사용하면 된다.

 

 

* HashSet의 주요 메소드

 

리턴 타입 메소드 이름 및 매개 변수 설명
boolean add(E e) 데이터를 추가한다.  
void clear() 모든 데이터를 삭제한다.
Object clone() HashSet 객체를 본제한다. 하지만 담겨 있는 데이터들은 복제하지 않는다.
boolean contains(Object o) 지정한 객체가 존재하는지를 확인한다.
boolean isEmpty() 데이터가 있는지 확인한다. 
Iterator<E> iterator() 데이터를 꺼내기 위한 Iterator 객체를 리턴한다.
boolean remove(Object o) 매개 변수로 넘어온 객체를 삭제한다.
int size() 데이터의 개수를 리턴한다.

 

HashSet에 선언되어 있는 메소드는 그리 많지 않다. 부모 클래스인 AbstractSet과 AbstractCollection에 선언 및 구현되어 있는 메소드를 그대로 사용하는 경우가 많다. 

 

코드를 통해 HashSet을 자세히 알아보자. 

어느 회사에 직원들이 보유하고 있는 차의 종류가 몇 개나 되는지 확인해 보려고 한다. 먼저 클래스의 main() 메소드에 cars라는 배열에 직원들의 차 목록을 추가하자. 다음에 getCarKinds() 메소드를 이용해 몇 가지 종류의 차가 있는지 확인해보자.

 

public class SetSample {

	public static void main(String[] args) {
		SetSample sample = new SetSample();
		String[] cars = new String[] {
			"Tico", "Sonata", "BMW", "Benz",
			"Lexus", "Mustang", "Grandeure",
			"The Beetle", "Mini Cooper", "i30",
			"BMW", "Lexus", "Carnibal", "SM5",
			"SM7", "SM3", "Tico"
		};
		System.out.println(sample.getCarKinds(cars));
	}
	public int getCarKinds(String[] cars) {
		return 0;
	}
}

 

직원들의 차 종류를 어떻게 확인해볼 수 있을까? getCarKinds() 메소드에 직접 개수를 확인하는 코드를 구현해보자.

 

public int getCarKinds(String[] cars) {
    if(cars == null) return 0;
    if(cars.length == 1) return 1;
    Set<String> carSet = new HashSet<String>();
    for(String car : cars)
        carSet.add(car);
    return carSet.size();
}

 

가장 먼저 cars라는 배열이 null인지를 확인했다. 이 작업을 수행하지 않으면 null인 배열을 받을 경우 NullPointerException이 발생한다. 다음 cars 배열의 크기가 1인지를 확인했다. 크기가 하나라면 당연히 1이기 때문이다. carSet이라는 HashSet 객체를 생성했고 여기에 cars 배열의 값들을 하나씩 담았다. 이 과정을 통해 중복된 값이 없어진다. 마지막으로 carSet의 크기를 리턴한다.

 

HashSet과 같은 Set을 사용하면 여러 중복되는 값들을 걸러내는 데 유용하다. 그럼 HashSet에 저장되어 있는 값을 어떻게 꺼낼까? for 루프를 사용해보자.

 

public void printCarSet(Set<String> carSet) {
    for(String temp : carSet) {
        System.out.println(temp + " ");
    }
}

 

getCarKinds() 메소드의 for 루프 이후, return 문 이전에 이 메소드를 호출하도록 한줄 추가하자.

 

public int getCarKinds(String[] cars) {
	// 생략
    for(String car : cars)
        carSet.add(car);
    printCarSet(carSet);
    return carSet.size();
}

 

메소드를 다시 호출해보자.

 

Mustang Lexus Tico i30 Grandeure Carnibal Sonata BMW Benz SM3 The Beetle SM5 Mini Cooper SM7 
14

 

데이터를 출력하는 방법은 한 가지만 존재하는 것이 아니다. Iterator 객체를 얻어도 된다.

 

public void printCarSet2(Set<String> carSet) {
    Iterator<String> iterator = carSet.iterator();
    while(iterator.hasNext()) {
        System.out.print(iterator.next() + " ");
    }
    System.out.println();
}

 

iterator()라는 메소드를 사용하여 Iterator 객체를 생성했다. while문을 사용하여 다음 데이터가 존재하는지를 hashNext()라는 메소드를 사용해 지속적으로 확인하고, next() 메소드를 사용하여 다음 값을 얻어냈다. 

 

지금까지 간단하게 Set 인터페이스를 구현한 클래스 중 하나인 HashSet에 대해 살펴보았다. 다음은 Queue에 대해 다룰 예정이다. 

 

 

그럼 이만.