본문 바로가기
Tech/Java

[Java] 컬렉션 프레임워크(Collections Framework)

by 소라소라잉 2019. 8. 22.

* 이 포스팅은 남궁성 선생님의 'Java의 정석'책의 내용을 정리한 내용입니다. 

 

 

 

컬렉션 프레임웍 개념

 

- 개념 : 데이터 군(컬렉션)을 저장하는 클래스들을 표준화(프레임웍)한 설계 

- 컬렉션데이터 그룹은 크게 3가지 -> List, Set, Map. ('데이터'의 종류는 이 세가지로 나눌 수 있다는 의미인듯) 

 

그리고 이들을 다루기 위한 인터페이스를 각각 정의했고(데이터를 분석해보니까 List, Set, Map이렇게 세가지로 나눌 수 있고, 데이터들을 다루는 인터페이스를 각 데이터 이름으로 정의.) 이 중에서 List와 Set의 공통된 부분을 뽑아 Collection이라는 인터페이스를 추가로 정의했다.(List와 Set의 조상인터페이스)

(Collections이라는 인터페이스를 왜 추가로 정의했을까? ->  인터페이스는 서로 관계없는 클래스들에 관계를 갖게 해주는 역할을 하는데 List와 Set은 공통된 부분이 많아(같이 쓸 수 있는 부분이 많아?) Collection으로 정의하여 이들간의 관계를 맺어줌으로써 다형성을 활용하려는 것 같다.)

Map의 경우는 List와 Set과는 전혀 다른 형태로 데이터를 다루기 때문에 같이 묶진 못했음.

 

 

각 인터페이스(List, Set, Map)의 특징

 

인터페이스 특징 구현 클래스 Example
List 순서O, 중복허용 ArrayList, LinkedList, Stack, Vector 대기자 명단
Set 순서X, 중복비허용 HashSet, TreeSet 양의 정수집합, 소수집합
Map Key + Value
순서X 
Key : 중복 비허용
Value : 중복 허용
HashMap, TreeMap 지역번호(전화번호)
전화번호부

 

데이터 군(컬렉션)을 다룰때 해당 데이터의 특징을 살피고 어떤 인터페이스를 이용하여 구현하면 될지 고민하고 인터페이스의 특징에 알맞게 잘 이용하는 것이 중요하다. (ex. 그룹별로 전화번호부를 만들어야 하는데 어떤 인터페이스를 사용하는 것이 좋을까?) 

 

각 인터페이스를 구현한 클래스들은 이름만 봐도 '아 이 인터페이스를 구현했구나'하고 파악할 수 있는데, 예를들면 ArrayList는 이름에 List가 들어가니까 List인터페이스를 구현했음을 알 수 있다. 

 

그러나 컬렉션 프레임워크가 만들어지기 이전부터 존재했던 Vector, Stack, Hshtable, Properties와 같은 것들은 이름만 봐서는 예상할 수가 없다. 어쩔 수 없음. 

 

Collection 인터페이스

 

앞서 말했듯이 Collection인터페이스는 List와 Set의 공통된 부분을 뽑아낸 그들의 조상 인터페이스이며 공통된 부분이라 함은 데이터를 다루는데 필수적인 추가하고(add), 삭제하고(remove), 정렬(sort)하는 등의 기본적인 기능이라 할 수 있겠다. 

 

자손1) List 인터페이스 

List인터페이스는 중복을 허용하면서 저장순서가 유지되는 컬렉션을 구현하는데 사용된다. 

 

자손2) Set 인터페이스

Set인터페이스는 중복을 허용하지 않고 저장순서또한 유지되지 않는 컬렉션 클래스를 구현하는데 사용된다. 

 

Map 인터페이스

 

키(Key)와 값(Value)을 하나의 쌍으로 묶어서 저장하는 컬렉션 클래스를 구현하는 데 사용된다. 

Key는 중복될 수 없으나 Value는 중복 가능하다. 만약 기존에 저장된 Key와 같은 Key를 저장하면 기존 값은 사라지고 마지막에 저장된 값이 남게 된다. 

인터페이스내 메서드들 중 주목해야 할 것은 keySet()과 values()의 반환타입이 다르다는 점인데, values()에서는 반환타입이 Collection이고 keySet()은 Set타입이다. 이는 Value는 중복을 허용하지만 Key는 허용하지 않는 Map의 특성때문이다. 

 

Map의 내부 인터페이스 - Map.Entry 인터페이스

Map에 저장되는 key-value쌍을 다루기 위한 인터페이스이다. 사전적 정의는 '항목'.

Map인터페이스를 구현하는 클래스에서는 그 내부 인터페이스인 Map.Entry 인터페이스도 함께 구현해야 한다. 

Key와 Value를 쌍으로 다루는 인터페이스기 때문에 내부 메서드에는 getKey(), getValue()등이 있다. 

 

 

List인터페이스를 구현한 ArrayList 클래스(구 Vector)

 

ArrayList클래스는 List인터페이스를 구현하기 때문에 데이터의 저장순서가 유지되고 중복을 허용한다는 특징이 있다. 

기존의 Vector를 개선한 것으로 그 구현원리와 기능은 동일하다. 

ArrayList의 단점은 배열에 더 이상 저장할 공간이 없으면(기본 초기화는 10) 그보다 큰 배열을 생성하여 기존의 배열에 저장된 내용을 새로운 배열로 복사한 다음에 저장된다는 점인데 이 과정에서 처리 시간이 많이 소요된다. 

데이터를 읽어오고 저장하는 데는 효율이 좋지만(비순차적 구조에서는 맨 처음노드부터 접근하여 꼬리를 물고 따라가야 하기 때문에 읽는데 오래 걸린다. 그에 반면 ArrayList는 배열이라 원하는 위치에 한번에 접근 가능함.), 용량을 변경해야 할 때는 위와 같이 배열 복사가 이루어지기 때문에 상당히 효율이 떨어진다. 

 

		for(int i = list2.size()-1; i>=0; i--) {
			   if(list1.contains(list2.get(i))) 
				   list2.remove(i); } 

 

위 코드는 list2에서 list과 공통되는 요소들을 찾아서 삭제하는 코드이다.

여기서 주목해야 할 점은 for문의 변수 i를 0부터 증가시킨 것이 아니라 끝에서부터 감소시키면서 반복한다는 점인데, 이는 만약 0부터 i를 증가시켜가며 삭제하면, 한 요소가 삭제될 때마다 빈 공간을 채우기 위해 나머지 요소들이 자리이동을 하기 때문에 올바른 결과를 얻을 수 없기 때문에 거꾸로 반복하는 것이다. 

 

그동안 알고리즘을 공부하면서 각 클래스들을 언제,왜,어떻게 사용하는지 알지 못한채 사용했었고, 위의 문제처럼 공통된 부분을 삭제해야하는 코드에서는 저렇게 거꾸로 반복할 생각을 못하고 이것저것 코드를 조합해가며 조잡스럽게 구현했었는데 그랬던 나에게 상당히 유용한 내용이라 책의 예제를 붙여봤다. 이렇게 간단한걸 왜 그동안....ㅠㅠ 

 

어쨌든 ArrayList는 용량초과시 복사하는 과정에서의 비효율성 때문에 처음부터 크기를 넉넉히 잡아주는 것이 좋단다. 

 

 

List인터페이스를 구현한 LinkedList 클래스

 

크기 변경이 불가하고, 비순차적 데이터의 추가/삭제에 오랜 시간이 소요되는 배열의 단점을 보완하기 위해 고안된 자료구조이다. 배열은 모든 데이터가 연속적으로 존재하나 LinkedList는 불연속적으로 존재하는 데이터를 서로 연결(Link)한 형태로 구성되어 있다. 하여 각 요소(Node)들은 자신과 연결된 다음 요소에 대한 참조(주소값)와 데이터로 구성되어 있다. 

따라서 배열처럼 데이터 삽입/삭제시 데이터를 이동하기 위한 복사과정이 없어 처리속도가 매우 빠르다.

하지만 이동방향이 단방향이라 다음 요소에 대한 접근은 쉬워도 이전 요소에 대한 접근은 어려운데 이를 보완한 것이 doubley-linked list(이중 연결리스트)이다. 이중연결리스트는 단순히 LinkedList에 참조변수(이전 요소에 대한 참조)를 하나 더 추가했을 뿐 그 외는 모두 LinkedList와 같다.  

실제로 LinkedList클래스는 이름과 달리 '링크드 리스트'가 아닌 '더블 링크드 리스트'로 구현되어 있다. 

 

LinkedList는 List인터페이스를 구현했기 때문에 ArrayList와 내부구현방법만 다를 뿐 제공하는 메서드의 종류와 기능은 거의 같다. 

따라서 각 클래스의 장단점을 이해하고 필요한 상황에 적절한 것을 선택해서 사용해야 한다. 

컬렉션 읽기
(접근시간-access time)
추가/삭제 비고
ArrayList 빠르다 느리다 순차적인 추가 삭제는 더 빠름. 
비효율적인 메모리 사용.
LinkedList 느리다 빠르다 데이터가 많을수록 접근성 떨어짐.

위의 특징처럼 다루고자 하는 데이터의 개수가 변하지 않는 경우라면 ArrayList가 좋고, 데이터 개수의 변경이 잦다면 LinkedList를 사용하는 것이 좋다. 

 

두 클래스를 조합해서 사용할 수도 있는데, 처음 작업하기 전에 데이터를 저장할때는 ArrayList를 사용하고, 작업시에는 LinkedList로 작업하면 좋은 효율을 얻을 수 있다.

컬렉션 프레임웍에 속한 대부분의 컬렉션 클래스들은 서로 변환이 가능한 생성자를 제공하므로 그를 이용하면 간단히 데이터를 옮길 수 있다. 

 

알고리즘에 자주 등장하는 Stack과 Queue를 구현하기 위해서는 어떤 컬렉션 클래스를 사용하는 것이 좋을까?

순차적으로 데이터를 추가하고 삭제하는 스택은 ArrayList와 같은 배열기반의 컬렉션 클래스가 적합하지만, 큐는 데이터를 꺼낼 때 항상 첫 번째 저장된 데이터를 삭제하므로, ArrayList를 사용한다면 데이터를 꺼낼 때마다 빈 공간을 채우기 위해 데이터의 복사가 발생하여 비효율적이다. 

그동안 Queue를 사용할때 Queue q = new LinkedList();로 사용했던 이유가 여기있었다! (유레카!) 

 

자바에서는 스택을 Stack클래스로 구현하여 제공하고 있지만 큐는 인터페이스로만 정의하고 별도로 클래스가 없다. 대신 Queue인터페이스를 구현한 클래스들이 여러개 있는데 그냥 지금껏 그래왔던 것처럼 LinkedList를 사용하자!

 

Iterator(구 Enumeration), ListIterator

 

Iterator

컬렉션에 저장된 요소를 접근하는데 사용되는 인터페이스이다. 

왜 접근하는데 사용하는 인터페이스를 따로 정의해두었을까? -> 컬렉션의 요소를 읽어오는 방법을 표준화하여 코드의 재사용성을 높일 수 있다.

예를 들면,

 

List list = new ArrayList() ;

// 이후 Iterator 사용하는 코드 blah blah 

... 

 

위의 코드의 경우 ArrayList대신 다른 컬렉션으로 변경하고 싶을때는 저 객체 생성 부분만 고치면(List인터페이스를 구현한 다른 컬렉션 클래스의 객체)된다. 다른 컬렉션 클래스들도 Iterator를 갖고 있으니까 이후의 코드부분을 고쳐줄 필요가 없다. (코드의 재사용성 ↑)

여기서 참조변수타입을 List로 한 이유는 이후 코드의 변경이 있을때 저 한 부분만 고치면 된다는 점에서 위의 이유와 동일하다. 

 

Map인터페이스를 구현한 컬렉션 클래스는 key와 value를 쌍으로 저장하고 있기 때문에 iterator()를 직접 호출할 수 없다. 그 대신 keySet()이나 entrySet()과 같은 메서드를 통해서(set타입으로 반환됨) 키와 값을 각각 따로 Set의 형태로 얻어 온 후에 다시 iterator()를 호출해야 Iterator를 얻을 수 있다. 

 

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

class nothing {

	public static void main(String[] args) {

		Object obj = "";

		// List 연습
		List list = new ArrayList();
		for (int i = 1; i <= 4; i++) {
			list.add(i);
		}
		Iterator it = list.iterator();

		while (it.hasNext()) {
			obj = it.next();
			System.out.println(obj);
		}

		// Set 연습
		Set set = new HashSet();
		for (int i = 1; i <= 4; i++) {
			set.add(i);
		}
		it = set.iterator();

		while (it.hasNext()) {
			obj = it.next();
			System.out.println(obj);
		}

		// Map 연습
		String[] lastName = { "김", "이", "박", "최" };

		Map map = new HashMap();
		for (int i = 0; i < lastName.length; i++) {
			map.put(i + 1, lastName[i] + "자바");
		}
		it = map.entrySet().iterator();

		while (it.hasNext()) {
			obj = it.next();
			System.out.println(obj);
		}
	}

}

각 인터페이스 별로 iterator사용 연습코드를 작성해봤다.

Map의 경우는 바로 iterator을 사용할 수 없기때문에 entrySet()메서드를 이용해 Map.Entry형태의 Set타입으로 반환받아 iterator 메서드를 호출했다. 

 

iterator()는 Collection인터페이스에 정의된 메서드이므로 Collection인터페이스의 자손인 List와 Set에도 포함되어 있다. 그래서 List나 Set인터페이스를 구현하는 컬렉션 클래스는 iterator()가 각 컬렉션의 특징에 맞게 알맞게 작성되어있다. 

주로 반복문을 사용해서 컬렉션 클래스의 요소를 읽어올때 iterator()를 호출해 사용한다.(주로 while문)

 

Iterator인터페이스의 메서드

1. boolean hasNext() : 읽어 올 요소가 남아있는지 확인. 

2. Object next() : 다음 요소를 읽어 옴. next()호출 전 hasNext()를 호출해서 읽어올 요소가 있는지 먼저 확인하는 것이 안전함.

3. void remove() : next()로 읽어 온 요소를 삭제. next)를 호출한 다음 remove()를 호출해야 한다.(선택적 기능)

 

'다음 요소'라는 말이 살짝 헷갈리는게 그럼 '지금','현재' 요소는 뭘까??? 배열로 표현한다면 next()의 기준점은 어디일까? 고민해봤는데 그냥 저 '다음'요소를 '현재'요소로 생각하면 이해가 쉬울 것 같다. ㅡ,.ㅡ;;

마지막 void remove()는 선택적 기능으로 반드시 구현하지 않아도 되는 메서드인데, 그렇다 하더라도 인터페이스로부터 상속받은 메서드는 추상메서드라 메서드의 body를 반드시 만들어 주어야 하므로 아래와 같이 예외를 던져 구현한다.

	public void remove() {
		throw new UnsupportedOperationException();
	}

단순히 public void remove() {};와 같이 구현하는 것 보다 위와같이 구현해서 구현되지 않은 기능이라는 것을 호출한 쪽에 알려주는 것이 좋다. 

그렇지 않으면 소스를 까보기 전까지는 기능이 바르게 동작하지 않는 이유를 알길이 없기 때문이다. 

 

ListIterator 

Iterator에 양방향 조회기능을 추가한 것이다. Iterator에 이전방향으로의 접근 기능을 추가 한 인터페이스!

이름에서부터 알 수 있듯이 List를 구현한 경우만 사용 할 수 있다. 순서가 유지되지 않는 Set의 경우는 애초에 '방향'이라는 것이 존재하지 않기 때문이다(?).(그래서 Set은 Iterator를 이용해 저장된 요소들을 읽어 와도 처음에 저장된 순서와 같지 않을 수 있다.) 

어찌됐든 이전방향으로의 접근 기능을 추가하였고 hasNext()와 next()에 각각 대응하는 Previous메서드는

boolean hasPrevious(), Object previous()가 있고

List인터페이스를 구현한 클래스에서만 사용할 수 있기 때문에 index를 얻어올 수 있는데 그 메서드는 

int previousIndex(), int nextIndex()가 있다.  

 

Arrays

 

(왜 갑자기 Arrays인가 싶지만 책 목차가 그러해서 넣었다. 뒤에 나오는 Comparator와 Comparable을 이해하는데 도움이 됨.)

Arrays클래스는 배열을 다루는 메서드들의 집합이다. 자바를 1주일 이상 만져본 사람이라면 누구나 기존에 알고있을 sort()와 copyOf(), copyOfRagne(), toString()외에 유용한 몇가지 메서드들을 정리해보자.

 

- binarySerch() : 이진검색 메서드. 검색속도가 상당히 빠르지만 배열이 정렬되어있는 경우에만 사용할 수 있다.

- equals() : 두 배열에 저장된 모든 요소를 비교하여 같으면 true 다르면 false를 반환한다. 

- asList(Object... a) : 배열을 List에 담아서 반환한다. 매개변수의 타입이 가변인수라 배열 생성없이 저장할 요소들만 나열하는 것도 가능하다. (ex. List list = Arrays.asList(1,2,3,4,5); )

그러나 asList()가 반환한 List는 크기를 변경할 수 없다는 단점이 있다. 크기를 변경할 수 있는 List가 필요하다면

List list = new ArrayList(Arrays.asList(1,2,3,4,5)); 이런식으로 써주면 됨.

 

이 외에도 parallel로 시작하는 메서드(빠른 결과를 얻기 위해 여러 쓰레드가 작업을 나누어 처리)가 있다.

뭔 내용인지는 나중에 알아보자... 

 

Comparator와 Comparable

 

위의 Arrays.sort()를 호출만 하면 컴퓨터가 알아서 배열을 정렬한다 생각하겠지만 사실은 Character클래스의 Comparable의 구현에 의해 정렬되었던 것이다! 

Comparable이 뭘까? 말 그대로 '비교가능한' something 인데 Comparator와 Comparable은 모두 컬렉션을 정렬하는데 필요한 메서드를 정의해놓은 인터페이스이다. 

'Tech > Java' 카테고리의 다른 글

[Java] AWT를 이용한 GUI빙고게임  (0) 2019.08.27
[Java] 컬렉션 프레임웍(Collections Framework) 연습문제  (0) 2019.08.23
[Java] Vector 구현  (0) 2019.08.21
[Java] Object-Oriented Programming  (0) 2019.08.08
[Java] 별찍기문제  (0) 2019.08.05

댓글