본글은 'Do it 자바 완전 정복' 책을 통한 공부내용을 정리한 글입니다.
컬렉션 프레임워크의 개념과 구조
컬렉션이란?
컬렉션(collection)은 동일한 타입을 묶어 관리하는 자료구조를 말한다. 한 컬렉션에는 동일한 타입의 데이터만 모아둘 수 있다. 컬렉션이 배열과 구분되는 가장 큰 특징은 바로 데이터의 저장 용량(capacity)을 동적으로 관리 할 수 있다는 것. 컬렉션의 저장 공간은 데이터의 개수에 따라 얼마든지 동적으로 변화할 수 있으며 컬렉션은 메모리 공간이 허용하는 한 저장 데이터 개수에 제약이 없다.
컬렉션 프레임워크란?
먼저 프레임워크(framwork)에 대해 알아보자. 일반적으로 단순히 연관된 클래스와 인터페이스들의 묶을을 라이브러리라고 한다. 프레임워크는 클래스 또는 인터페이스를 생성하는 과정에서 설계의 원칙 또는 구조에 따라 클래스 또는 인터페이스를 설계하고, 이렇게 설계된 클래스와 인터페이스를 묶어 놓은 개념이다. 컬렉션 프레임워크는 컬렉션과 프레임워크가 조합된 개념으로, 리스트, 스택, 큐, 트리 등의 자료구조에 정렬, 탐색 등의 알고리즘을 구조화 해놓은 프레임워크다.
자바에서 제공하는 컬렉션 프레임워크의 주요 클래스와 인터페이스를 컬렉션의 특성에 따라 구분하면 크게 List<E>
, set<E>
, Map<K, V>
로 나눌 수 있고, 메모리의 입출력 특성에 따라 기존의 컬렉션 기능을 확장 또는 조합한 Stack<E>
, Queue<E>
가 있다.
List<E> 컬렉션 인터페이스
List<E>
는 배열과 가장 비슷한 구조를 지니고 있는 자료구조다. 먼저, 배열과 리스트의 차이점을 알아본다.
배열과 리스트의 차이점
가장 큰 차이점은 저장 공간의 크기가 고정적이냐, 동적으로 변화하느냐다.
import java.util.*;
public class ArrVsList {
public static void main(String[] args) {
// 배열의 특징
String[] arr = new String[]{"a", "b", "c", "d", "e", "f"};
// 저장공간의 크기가 6인 배열 참조 객체
// 특정 index의 데이터가 필요없어졌다면?
arr[2] = null; arr[4] = null;
System.out.println(arr.length); // 6
// 리스트의 특징
List<String> list = new ArrayList<>();
list.add("a"); list.add("b"); list.add("c");
list.add("d"); list.add("e"); list.add("f");
System.out.println(list.size()); // 6
// 저장공간의 크기가 6인 리스트 참조 객체
list.remove("a"); list.remove("c");
System.out.println(list.size()); // 4
}
}
List<E> 객체 생성하기
List<E>
는 인터페이스이므로 객체를 스스로 생성할 수 없다. 따라서 객체를 생성하기 위해서는 List<E>를 상속받아 자식 클래스를 생성하고, 생성한 자식 클래스를 이용해 객체를 생성해야 한다.
하지만 컬렉션 프레임워크를 이용할 때에는 인터페이스를 구현하지 않고 List<E> 인터페이스를 구현한 대표적인 클래스 ArrayList<E>
, Vector<E>
, LinkedList<E>
를 이용하면 된다.
List<E> 인터페이스 구현 클래스 생성자로 동적 컬렉션 객체 생성
List<E>자체가 제네릭 인터페이스이므로 이를 상속한 자식클래스들도 제네릭 클래스다. 즉, 객체 생성시에 제네릭의 실제 타입을 지정해야 한다. 일반적으로 기본 생성자를 사용하지만, 초기 저장 용량(capacity)을 매개변수로 포함하고 있는 생성자를 사용할 수도 있다.
저장 용량은 실제 데이터의 개수를 나타내는 저장 공간의 크기 size()
와는 다른 개념으로, 데이터를 저장하기 위해 미리 할당해 놓은 메모리의 크기라고 생각하면 된다. 기본 생성자를 사용해 객체를 생성하면 기본으로 10만큼의 저장 용량을 확보해 놓고, 이후 데이터가 추가되어 저장 용량이 더 필요하면 자바 가상 머신이 자동으로 저장 용량을 늘린다.
// List<E> 인터페이스의 구현 클래스 생성자로 동적 컬렉션 생성
List<Integer> list1 = new ArrayList<Integer>(); // capacity = 10
List<Integer> list2 = new ArrayList<Integer>(30); // capacity = 30
Vector<String> list3 = new Vector<String>(); // capacity = 10
List<MyWork> list4 = new LinkedList<MyWork>(20); // LinkedList는 capacity 지정 불가
Arrays.asList() 메서드를 이용해 정적 컬렉션 객체 생성
List<E>
객체를 생성하는 또 다른 방법은 Arrays 클래스의 asList(T..)
정적 메서드를 사용하는 것이이다. 배열을 먼저 생성하고 이를 List<E>로 래핑(wrapping), 즉 포장만 해 놓은 것이므로 내부 구조가 배열과 동일하기 때문에 컬렉션 객체인데도 불구하고 저장 공간의 크기를 변경할 수 없다.
즉, 구현 클래스와 객체를 생성했을 때와 데이터의 추가 및 삭제가 불가능하며, 저장 공간의 크기를 변경하지 않는 데이터의 변경(set()
)은 가능하다. 따라서 고정된 개수의 데이터를 저장하거나 활용할 때 주료 사용한다.
List<Integer> list = Arrays.asList(1, 2, 3, 4);
list.set(1, 7); // [1 7 3 4]
list.add(5); // 오류(UnsupportedOperationException)
list.remove(0); // 오류(UnsupportedOperationException)
List<E>의 주요 메서드
List<E>에는 데이터 추가, 변경, 삭제, 리스트 데이터 정보 추출 및 리스트의 배열 변환 등의 추상 메서드가 정의되어 있다. 주요 메서드를 살펴본다.
구분 | 리턴 타입 | 메서드명 | 기능 |
데이터 추가 | boolean | add(E element) | 매개변수로 입력된 원소를 리스트 마지막에 추가 |
void | add(int index, E element) | index 위치에 입력된 원소 추가 | |
boolean | addAll(Collection<? Extends E> c) | 매개변수로 입력된 컬렉션 전체를 마지막에 추가 | |
boolean | addAll(int index, Collection<? Extends E> c) | index 위치에 입력된 컬렉션 전체를 추가 | |
데이터 변경 | E | set(int index, E element) | index 위치의 원솟값을 입력된 원소로 변경 |
데이터 삭제 | E | remove(int index) | index 위치의 원솟값 삭제 |
boolean | remove(Object o) | 원소 중 매개변수 입력과 동일한 객체 삭제 | |
void | clear() | 전체 원소 삭제 | |
리스트 데이터 정보 추출 | E | get(int index) | index 위치의 원솟값을 꺼내 리턴 |
int | size() | 리스트 객체 내에 포함된 원소의 개수 | |
boolean | isEmpty() | 리스트의 원소가 하나도 없는지 여부를 리턴 | |
리스트 배열 변환 | Object[] | toArray() | 리스트를 Object 배열로 변환 |
T[] | toArray(T[] t) | 입력매개변수로 전달한 타입의 배열로 변환 |
이 중 리스트를 배열 객체로 변환하는 메서드만 상세하게 알아본다. 첫 toArray()
는 원소의 자료형과 상관없이 Object[]로 반환하므로 특정 타입으로 변환하기 위해서는 다운캐스팅이 필요하다. 두번째의 toArray(T[] t)
의 경우에는 리스트의 데이터 개수가 배열 객체의 크기보다 작을 때는 배열의 크기 그대로로 리턴되며, 리스트 데이터의 개수가 더 많을 때는 배열 크기가 데이터의 개수만큼 확장되어 리턴된다.
ArrayList<E> 구현 클래스
ArrayList<E>는 대표적인 List<E> 구현 클래스로, List<E>가 지니고 있는 대표적인 특징인 데이터를 인덱스로 관리하는 기능, 저장 공간을 동적으로 관리하는 기능 등을 그대로 지니고 있다.
// ArrayList<E> 클래스의 주요 메서드 활용 법
import java.util.*;
public class ArrayListMethod {
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<Integer>();
// 1. add(E element)
list1.add(3); list1.add(4); list1.add(5);
System.out.println(list1); // [3, 4, 5]
// 2. add(int index, E element)
list1.add(1, 6);
System.out.println(list1); // [3, 6, 4, 5]
// 3. addAll(다른 리스트 객체)
List<Integer> list2 = new ArrayList<Integer>();
list2.add(1); list2.add(2);
list2.addAll(list1);
System.out.println(list2); // [1, 2, 3, 6, 4, 5]
// 4. addAll(int index, 다른 리스트 객체)
List<Integer> list3 = new ArrayList<Integer>();
list3.add(1); list3.add(5); list3.add(3);
list3.addAll(1, list3);
System.out.println(list3); // [1, 1, 5, 3, 5, 3]
// 5. set(int index, E element)
list3.set(1,5); list3.set(3,0);
System.out.println(list3); // [1, 5, 5, 0, 5, 3]
// 6. remove(int index)
list3.remove(1);
System.out.println(list3); // [1, 5, 0, 5, 3]
// 7. remove(Object o)
list3.remove(new Integer(1));
System.out.println(list3); // [5, 0, 5, 3]
// 8. clear()
list3.clear();
System.out.println(list3); // []
// 9. isEmpty()
System.out.println(list3.isEmpty()); // true
// 10. size()
System.out.println(list3.size()); // 0
// 11. get()
list3.add(3); list3.add(4); list3.add(3);
System.out.println(list3.get(0)); // 3
System.out.println(list3.get(1)); // 4
System.out.println(list3.get(2)); // 3
for (int i = 0; i < list3.size(); i++) System.out.println(list3.get(i)); // 3, 4, 3
// 12. toArray() List -> Array
Object[] object = list3.toArray();
System.out.println(Arrays.toString(object)); // [3, 4, 3]
// 13-1. toArray(T[] t) -> T[] : 배열의크기 < 리스트의 크기
Integer[] integer1 = list3.toArray(new Integer[0]);
System.out.println(Arrays.toString(integer1)); // [3, 4, 3]
// 13-1. toArray(T[] t) -> T[] : 배열의 크기 > 리스트의 크기
Integer[] integer2 = list3.toArray(new Integer[5]);
System.out.println(Arrays.toString(integer2)); // [3, 4, 3, null, null]
}
}
Vector<E> 구현 클래스
Vector<E>
는 List<E>
를 상속했으므로 동일한 타입의 객체를 수집할 수 있고, 메모리를 동적 할당할 수 있으며, 데이터의 추가, 변경, 삭제 등이 가능하다는 List<E>의 공통적인 특징을 모두 갖고 있다. 또한 앞에서 살펴본 ArrayList<E>와 메서드 기능 및 사용법 또한 동일하다. 그렇다면 ArrayList<E>와의 차이점이 뭘까? 바로 Vector<E>
의 주요 메서드는 동기화 메서드(synchronized method)로 구현되어 있으므로 멀티 쓰레드에 적합하도록 설계되어 있다는 것이다.
정리해 Vector<E>는 ArrayList<E>와 동일한 기능을 수행하지만, 멀티 쓰레드에서 사용할 수 있도록 기능이 추가된 것이다. 하나의 쓰레드로만 구성된 싱글 쓰레드에서도 사용할 수 있지만, 싱글쓰레드에서는 무겁고 많은 리소스를 차지하는 Vector<E> 보다는, ArrayList<E>를 사용하는 것이 훨씬 효율적이다.
LinkedList<E> 구현 클래스
List<E>의 마지막 구현 클래스 LinkedList<E>에 대해 알아보자. 역시나 List<E>의 모든 특징을 지니고, 메서드를 동기화하지 않았으므로 싱글 쓰레드에서 사용하기에 적합하다. 그렇다면 ArrayList<E>와의 차이점이 뭘까?
1) LinkedList<E>는 저장 용량(capacity)을 매개변수로 갖는 생성자가 존재하지 않는다.
고로 객체를 생성할 때 저장 용량을 지정할 수 없다.
2) 내부적으로 데이터를 저장하는 방식이 다르다.
ArrayList<E>가 모든 데이터를 위치 정보(인덱스)와 값으로 저장하는 반면, LinkedList<E>는 앞뒤 객체의 정보를 저장한다. 말 그대로 모든 데이터가 서로 연결된 형태로 관리되는 것.
ArrayList<E>와 LinkedList<E>의 성능 비교
LinkedList<E>와 같은 저장 구조를 지니게 되면 얻을 수 있는 이점을 알아보자. ArrayList<E> 객체에서 2번 인덱스에 데이터를 추가한다고 하면 기존의 모든 데이터의 위치 정보를 수정해야 한다. 반면 LinkedList<E>는 각 원소의 앞뒤 객체 정보만을 가지고 있으므로 값이 추가된 앞뒤 데이터 정보만 수정하면 된다.
그러나 LinkedList는 각 원소가 자신의 인덱스 정보를 갖고 있지 않으므로 특정 인덱스 위치의 값을 가져오기 위해서는 앞에서부터 차례대로 번호를 세어가며 찾아야한다.
구분 | ArrayList<E> | LinkedList<E> |
추가, 삭제(add, remove) | 속도 느림 | 속도 빠름 |
검색(get) | 속도 빠름 | 속도 느림 |
'Backend > JAVA' 카테고리의 다른 글
[JAVA] #17.4 Map<K, V> 컬렉션 인터페이스 (0) | 2023.07.20 |
---|---|
[JAVA] #17.3 Set<E> 컬렉션 인터페이스 (0) | 2023.07.19 |
[JAVA] #16 제네릭, 제네릭 클래스와 인터페이스, 제네릭 메서드 (1) | 2023.07.16 |
[JAVA] #15.5 쓰레드의 상태 (0) | 2023.07.15 |
[Java] #15.4 쓰레드의 동기화 (0) | 2023.07.08 |