Java Collection Framework

Seoyeon Kim·2023년 3월 28일
2

Collection

collection은 동일한 타입을 묶어 관리하는 자료구조를 말한다. 배열도 동일한 타입을 묶어 관리하는 것이지만, 배열을 collection이라 부르지는 않는다. collection이 배열과 구분되는 가장 큰 특징은 바로 데이터의 저장 용량을 동적으로 관리할 수 있다는 것이다. 배열은 생성 시점에 저장 공간의 크기를 확정해야 하고, 나중에 변경할 수 없는 반면, collection의 저장 공간은 데이터의 개수에 따라 얼마든지 동적으로 변화할 수 있다.


Framework

일반적으로 단순히 연관된 클래스와 인터페이스들의 묶음을 Library라고 한다. 반면 Framework는 클래스 또는 인터페이스를 생성하는 과정에서 설계의 원칙 또는 구조에 따라 클래스 또는 인터페이스를 설계하고, 이렇게 설계된 클래스와 인터페이스를 묶어 놓은 개념이다. 즉, 단순한 클래스와 인터페이스의 모임이 아니라 클래스의 정의에 설계 원칙 또는 구조가 존재하는 것을 framework라고 할 수 있다.

Collection Framework는 이 두 가지가 조합된 개념으로 List, Stack, Queue, Tree 등의 자료 구조에 정렬, 탐색 등의 알고리즘을 구조화해 놓은 것이다. 쉽게 말해 여러 개의 데이터 묶음 자료를 효과적으로 처리하기 위해 구조화된 클래스 또는 인터페이스의 모음 정도로 생각하면 된다.


Java Collection Framework

자바에서 제공하는 collection framework의 주요 클래스와 인터페이스는 다음과 같다.

List<E>

List<E> 자체가 제네릭 인터페이스이므로 이를 상속한 자식 클래스들도 제네릭 클래스다. 즉, 객체를 생성할 때 제네릭의 실제 타입을 지정해야 한다. 객체를 생성할 때는 일반적으로 기본 생성자를 사용하지만 초기 저장 용량을 매개변수로 포함하고 있는 생성자를 사용할 수도 있다.

단 List<E>의 대표적인 구현 클래스 중 LinkedList<E>는 기본 생성자만 존재한다.

저장 용량은 실제 데이터의 개수를 나타내는 저장 공간의 크기와는 다른 개념으로, 데이터를 저장하기 위해 미리 할당해 놓은 메모리의 크기라고 생각하면 된다. List<E>는 기본 생성자를 사용해 객체를 생성하면 기본적으로 10만큼의 저장 용량을 내부에 확보해 놓는다.

List<E> 객체를 생성하는 또 다른 방법은 Arrays 클래스의 asList(T...) 정적 메서드를 사용하는 것이다. 내부적으로 배열을 먼저 생성하고, 이를 List<E>로 wrapping 즉 포장만 해 놓은 것이다. 따라서 내부 구조는 배열과 동일하므로 컬렉션 객체인데도 저장 공간의 크기를 변경할 수 없다. 따라서 구현 클래스로 객체를 생성했을 때와 달리 데이터의 추가 및 삭제가 불가능하다. 다만 저장 공간의 크기를 변경하지 않는 데이터의 변경은 가능하다.

ArrayList<E> vs Vector<E>

ArrayList는 대표적인 List 구현 클래스로 List가 지니고 있는 대표적인 특징인 데이터를 인덱스로 관리하는 기능, 저장 공간을 동적으로 관리하는 기능 등을 그대로 지니고 있다.

Vector 또한 List를 상속했으므로 당연히 동일한 타입의 객체를 수집할 수 있고, 메모리를 동적으로 할당할 수 있으며, 데이터의 추가, 변경, 삭제 등이 가능하다는 List의 공통적인 특성을 모두 갖고 있을 것이다. 또한 ArrayList와 메서드 기능 및 사용법 또한 완벽히 동일하다.

다만 Vector는 주요 메서드가 동기화 메서드로 구현돼 멀티 쓰레드 환경에 적합하도록 설계돼 있다는 점에서 ArrayList와 차이점을 갖는다.

동기화 메서드는 하나의 공유 객체를 2개의 쓰레드가 동시에 사용할 수 없도록 만든 메서드다. 메서드를 동기화하지 않으면 하나의 List 객체가 있을 때 하나의 쓰레드는 데이터를 읽고, 또 하나의 쓰레드는 데이터를 삭제하는 작업을 동시에 수행해 작업이 충돌하는 상황이 발생할 수 있다.

정리하면 Vector는 ArrayList와 동일한 기능을 수행하지만, 멀티 쓰레드에서 사용할 수 있도록 기능이 추가된 것이다. 물론 하나의 쓰레드로만 구성된 싱글 쓰레드에서도 사용할 수 있지만, 굳이 무겁고 많은 리소스를 차지하는 Vector를 쓰는 대신 ArrayList를 쓰는 것이 훨씬 효율적이다.

LinkedList<E>

LinkedList 역시 List의 모든 공통적인 특징을 지니고 있다. ArrayList처럼 메서드를 동기화하지 않았기 때문에 싱글 쓰레드에서 사용하기에 적합하다.

다만 LinkedList는 저장 용량을 매개변수로 갖는 생성자가 없기 때문에 객체를 생성할 때 저장 용량을 지정할 수 없다. 그리고 ArrayList가 모든 데이터를 인덱스와 값으로 저장하는 반면, LinkedList는 앞뒤 객체의 정보를 저장한다. 말 그대로 모든 데이터가 서로 연결된 형태로 관리되는 것이다.

내부적으로 데이터를 저장하는 방식만 다를 뿐 메서드의 종류와 활용 방법은 ArrayList와 동일하다. 즉 ArrayList의 예제를 LinkedList로 수정해 실행하면 동일하게 동작한다.

데이터를 저장하는 방식이 다르기 때문에 각 구조별로 얻을 수 있는 이점이 다르다. 데이터를 추가 또는 삭제할 때는 LinkedList의 속도가 빠르며, 데이터를 검색할 때는 ArrayList의 속도가 빠르다.


Set<E>

Set<E>은 동일한 타입의 묶음이라는 특징은 그대로 갖고 있지만, 인덱스 정보를 포함하고 있지 않은 집합의 개념과 같은 collection이다. 인덱스 정보가 없으므로 데이터를 중복해서 저장하면 중복된 데이터 중 특정 데이터를 지칭해 꺼낼 방법이 없다. 즉, Set<E>은 데이터를 구분할 수 있는 유일한 방법이 데이터 그 자체인 것이다. 따라서 동일한 데이터의 중복 저장을 허용하지 않는다.

HashSet<E>

HashSet<E>은 Set<E> 인터페이스의 대표적인 구현 클래스다. Set<E>의 특성상 저장 데이터를 꺼낼 때는 입력 순서와 다를 수 있다. 인덱스 번호가 없으므로 말 그대로 주머니에 손을 넣어서 1개씩 꺼내는 셈인 것이다. HashSet<E> collection도 저장 용량을 동적으로 관리하며, 기본 생성자로 생성할 때 기본값은 16이다.

Set<E>의 모든 구현 클래스는 모든 항목을 출력하도록 toString()을 오버라이딩해 놓았기 때문에 System.out.println() 메서드로 항목을 쉽게 확인할 수 있다.

중복을 허용하지 않기 위해서는 '같음' 또는 '다름'을 비교할 수 있어야 한다. 예를 들어 3과 3은 같은 데이터다. 즉 HashSet에 2개 이상 저장할 수 없다. 그렇다면 A a1 = new A(3)A a2 = new A(3) 은 어떨까? 생성자에 동일한 값을 넘겨 객체를 생성했으므로 두 객체는 완벽히 똑같이 생겼을 것이다. 하지만 클래스 A에서 특별한 처리를 해주지 않았다면 HashSet의 관점에서는 다른 객체다. 즉, 둘 다 저장할 수 있다는 것이다.

이런 결과가 나오는 것이 a1a2 가 객체이기 때문에 그런 것은 아니다. 실제로 모든 collection은 객체만 저장할 수 있기 때문에 3의 데이터를 넣으면 내부적으로 new Integer(3) 과 같이 객체로 변환돼 저장된다. 이와 같은 예로 String s1 = new String("hello")String s2 = new String("hello") 는 HashSet의 관점에서 동일한 객체다.

이 모든 것을 이해하기 위해서는 먼저 hashcode의 개념과 등가 연산자 == , equal() 메서드의 차이점을 알아야 한다.

먼저 객체의 hashcode는 객체가 저장된 번지와 연관된 값으로, 실제 주솟값은 아니다. 즉, 객체가 저장된 번지를 기준으로 생성된 정수형 고윳값이 바로 hashcode다. 객체의 hashcode 값을 반환하는 hashCode() 메서드는 Object 클래스에 정의돼 있다. 또한 하위 클래스에서 Object의 toString() 메서드를 오버라이딩 하지 않았다면 toString() 메서드로 hashcode 값을 확인할 수 있다.

Object의 toString() 메서드는 '패키지명.클래스명@hashcode'를 출력한다.

등가 연산자 ==스택 메모리 값을 동등 비교한다. 기본 자료형은 스택 메모리에 실제 값참조 자료형은 스택 메모리에 위치값을 저장하고 있다. 따라서 기본 자료형의 등가 연산은 실제 값을, 참조 자료형의 등가 연산은 위치값을 비교한다. Object equals() 메서드는 등가 연산과 완벽하게 동일하다. 즉, Object의 equals() 메서드 내부에서는 자신의 객체와 매개변수로 넘어온 객체의 등가 연산을 수행한 결과를 반환한다. 따라서 equals() 메서드를 오버라이딩하지 않았다면 등가 연산과 equals() 메서드의 결과는 항상 동일한 값을 가질 것이다.

중복 확인은 다음과 같이 2단계로 처리된다.

첫 번째 단계에서는 두 객체의 hashcode가 동일한지를 비교하고, 두 번째 단계에서는 equals() 메서드를 이용해 두 객체를 비교한다. hashcode가 동일하고 equals() 메서드가 true를 반환하면 두 객체는 동일한 객체로 인식하고 이외에는 다른 객체로 인식한다. 따라서 하위 클래스에서 Object의 hashcode()와 equals()를 오버라이딩했는지, 했다면 어떻게 했는지에 따라 객체의 동등 여부 결과는 달라질 것이다. 참고로 첫 번째 단계에서 hashcode 값이 다르면 두 번째 단계로 넘어가지도 않는다.

Java HashSet 알아보기


Map<K, V>

Map<K, V>은 collection 상속 구조상 List와 Set과 분리돼 있다. 즉 List와 Set이 Collection 인터페이스를 상속받는 반면, Map<K, V>은 별도의 인터페이스로 존재한다. 따라서 저장의 형태와 방식이 두 collection과 다르다.

Map<K, V> collection은 KeyValue의 한 쌍으로 데이터를 저장한다. 이때 한 쌍의 데이터를 Entry라고 하며 Map.Entry 타입으로 정의된다. 즉, Map<K, V>은 데이터를 Entry 단위로 입력받는 것이다. Key로 데이터를 구분하기 때문에 Key는 중복 저장이 불가하며 Value는 Key로 구분해 가져올 수 있으므로 중복이 허용된다.

HashMap<K, V>

HashMap<K, V>은 Map<K, V>의 대표적인 구현 클래스다. Key 값의 중복 여부를 확인하는 메커니즘은 HashSet<E> 때와 완벽히 동일하다. 즉, 두 Key 객체의 hashcode() 값이 같고, equals() 메서드가 true를 반환하면 같은 객체로 인식한다. 이외에는 서로 다른 객체로 간주해 각각의 Key 값으로 등록할 수 있다. 초기 저장 용량의 기본 값은 16이고, 원소의 개수가 16을 넘어가면 자동으로 늘어난다.

Hashtable<K, V> & LinkedHashMap<K, V>

Hashtable<K, V>은 HashMap<K, V>과 비교해 멀티 쓰레드에도 안전하다는 특징 말고는 완벽히 와 동일한 특징을 가진다.

LinkedHashMap<K, V>은 HashMap<K, V>의 기본적인 특성에 입력 데이터의 순서 정보를 추가로 갖고 있는 collection이다. 따라서 저장 데이터를 출력하면 항상 입력된 순서대로 출력된다. HashMap<K, V>에서는 Key를 HashSet<E>로 관리하는 반면, LinkedHashMap<K, V>은 Key를 LinkedHashSet<E>으로 관리한다.

TreeMap<K, V>

TreeMap<K, V>은 Map<K, V>의 기본 기능에 정렬 및 검색 기능이 추가된 collection으로 입력 순서와 관계없이 데이터를 Key 값의 크기 순으로 저장한다. 따라서 반드시 Key 객체는 크기 비교의 기준을 갖고 있어야 한다.


Stack<E>

Stack<E> collection은 이 글에서 다루는 collection 중 유일하게 클래스다. 즉, 자체적으로 객체를 생성할 수 있다. 상속 구조를 살펴보면 List collection의 구현 클래스인 Vector 클래스의 자식 클래스로 후입선출 자료구조를 구현한 collection이다.

당연히 Vector의 모든 기능을 포함하고 있으며, 여기에 추가로 후입선출 구조를 위한 5개의 메서드가 추가됐다. 이들 추가 메서드는 Stack<E> 클래스에서 추가됐기 때문에 해당 기능을 사용하려면 변수를 Stack<E> 타입으로 선언해야 한다.


Queue<E>

Queue<E>는 List와 Set처럼 Collection<E>에게서 상속된 인터페이스로 LinkedList가 Queue<E> 인터페이스를 구현하고 있다. Queue<E>의 가장 큰 특징은 Stack의 후입선출과 반대되는 개념인 선입선출 구조를 가진다는 것이다. 대표적인 예로는 이벤트를 처리하는 이벤트 큐가 있다. 키보드 이벤트가 발생하면 각 이벤트는 발생한 순서대로 이벤트 큐에 저장되고, CPU는 이벤트 큐에서 이벤트를 1개씩 꺼내 처리한다.

여러 collection 중에서 'Linked'가 붙은 collection은 입력 순서 정보를 저장하기 때문에 입력 순서와 출력 순서가 동일하다고 했는데 이것이 바로 Queue<E>의 특징이라고 생각하면 된다.


참고 자바 완전 정복 624 page~

0개의 댓글