해당 글은 아래 링크를 참고하여 작성되었습니다.
13. 제네릭
1. 제네릭의 개념
자바에서 제네릭(generic)이란 데이터의 타입(data type)을 일반화한다(generalize)는 것을 의미한다.
제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법으로 이렇게 컴파일 시에 미리 타입 검사(type check)를 수행하면 다음과 같은 장점을 가진다.
1. 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있다
2. 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.
JDK 1.5부터 도입된 제네릭을 사용하면 컴파일 시에 미리 타입이 정해지므로, 타입 검사나 타입 변환과 같은 번거로운 작업을 생략할 수 있게 된다.
자바에서 제네릭은 클래스와 메소드에만 다음과 같은 방법으로 선언할 수 있다.
class MyArray<T> {
T element;
void setElement(T element) { this.element = element; }
T getElement() { return element; }
}
위와 같이 선언된 제네릭 클래스(generic class)를 생성할 때에는 타입 변수 자리에 사용할 실제 타입을 명시해야 한다.
MyArray<Integer> myArr = new MyArray<Integer>();
자바에서 타입 변수 자리에 사용할 실제 타입을 명시할 때 기본 타입을 바로 사용할 수는 없고 위 예제의 Integer와 같이 래퍼(wrapper) 클래스를 사용해야만 한다.
자바 코드에서 선언되고 사용된 제네릭 타입은 컴파일 시 컴파일러에 의해 자동으로 검사되어 타입 변환된다.
코드 내의 모든 제네릭 타입은 제거되어 컴파일된 class 파일에는 어떠한 제네릭 타입도 포함되지 않게 된다.
이런 식으로 동작하는 이유는 제네릭을 사용하지 않는 코드와의 호환성을 유지하기 위해서다.
2. 다양한 제네릭 표현
제네릭 메소드란 메소드의 선언부에 타입 변수를 사용한 메소드를 의미한다.
이때 타입 변수의 선언은 메소드 선언부에서 반환 타입 바로 앞에 위치한다.
다음 예제의 제네릭 클래스에서 정의된 타입 변수 T와 제네릭 메소드에서 사용된 타입 변수 T는 전혀 별개의 것임을 주의해야 한다.
class AnimalList<T> {
...
public static <T> void sort(List<T> list, Comparator<? super T> comp) {
...
}
...
}
와일드카드(wild card)란 이름에 제한을 두지 않음을 표현하는 데 사용되는 기호를 의미하는데 자바의 제네릭에서는 물음표(?) 기호를 사용하여 이러한 와일드카드를 사용할 수 있다.
<?> // 타입 변수에 모든 타입을 사용할 수 있음.
<? extends T> // T 타입과 T 타입을 상속받는 자손 클래스 타입만을 사용할 수 있음.
<? super T> // T 타입과 T 타입이 상속받은 조상 클래스 타입만을 사용할 수 있음.
14. 컬렉션 프레임워크
1. 컬렉션 프레임워크의 개념
자바에서 컬렉션 프레임워크(collection framework)란 다수의 데이터를 쉽고 효과적으로 처리할 수 있는 표준화된 방법을 제공하는 클래스의 집합을 의미한다
즉, 데이터를 저장하는 자료 구조와 데이터를 처리하는 알고리즘을 구조화하여 클래스로 구현해 놓은 것이다.
이러한 컬렉션 프레임워크는 자바의 인터페이스(interface)를 사용하여 구현된다.
컬렉션 프레임워크에서는 데이터를 저장하는 자료 구조에 따라 다음과 같은 핵심이 되는 주요 인터페이스를 정의하고 있다.
1) List 인터페이스
2) Set 인터페이스
3) Map 인터페이스
이 중에서 List와 Set 인터페이스는 모두 Collection 인터페이스를 상속받지만 구조상의 차이로 인해 Map 인터페이스는 별도로 정의된다.
List<E> | 순서가 있는 데이터의 집합으로, 데이터의 중복을 허용함. | Vector, ArrayList, LinkedList, Stack, Queue |
Set<E> | 순서가 없는 데이터의 집합으로, 데이터의 중복을 허용하지 않음. | HashSet, TreeSet |
Map<K, V> | 키와 값의 한 쌍으로 이루어지는 데이터의 집합으로, 순서가 없음. 이때 키는 중복을 허용하지 않지만, 값은 중복될 수 있음. |
HashMap, TreeMap, Hashtable, Properties |
컬렉션 프레임워크에 속하는 인터페이스를 구현한 클래스를 컬렉션 클래스(collection class)라고 한다.
컬렉션 프레임워크의 모든 컬렉션 클래스는 List와 Set, Map 인터페이스 중 하나의 인터페이스를 구현하고 있다.
또한, 클래스 이름에도 구현한 인터페이스의 이름이 포함되므로 바로 구분할 수 있다.
Vector나 Hashtable과 같은 컬렉션 클래스는 예전부터 사용해와서 기존 코드와의 호환을 위해 아직도 남아 있다.
하지만 기존에 사용하던 컬렉션 클래스를 사용하는 것보다는 새로 추가된 ArrayList나 HashMap 클래스를 사용하는 것이 성능 면에서도 더 나은 결과를 얻을 수 있다.
Collection 인터페이스에서 제공하는 주요 메소드는 다음과 같다.
boolean add(E e) | 해당 컬렉션(collection)에 전달된 요소를 추가함. (선택적 기능) |
void clear() | 해당 컬렉션의 모든 요소를 제거함. (선택적 기능) |
boolean contains(Object o) | 해당 컬렉션이 전달된 객체를 포함하고 있는지를 확인함. |
boolean equals(Object o) | 해당 컬렉션과 전달된 객체가 같은지를 확인함. |
boolean isEmpty() | 해당 컬렉션이 비어있는지를 확인함. |
Iterator<E> iterator() | 해당 컬렉션의 반복자(iterator)를 반환함. |
boolean remove(Object o) | 해당 컬렉션에서 전달된 객체를 제거함. (선택적 기능) |
int size() | 해당 컬렉션의 요소의 총 개수를 반환함. |
Object[] toArray() | 해당 컬렉션의 모든 요소를 Object 타입의 배열로 반환함. |
2. List 컬렉션 클래스
List 인터페이스를 구현한 모든 List 컬렉션 클래스는 다음과 같은 특징을 가진다.
1) 요소의 저장 순서가 유지된다.
2) 같은 요소의 중복 저장을 허용한다.
대표적인 List 컬렉션 클래스에 속하는 클래스는 다음과 같다.
1) ArrayList<E>
ArrayList 클래스는 가장 많이 사용되는 컬렉션 클래스 중 하나다.
JDK 1.2부터 제공된 ArrayList 클래스는 내부적으로 배열을 이용하여 요소를 저장한다.
ArrayList 클래스는 배열을 이용하기 때문에 인덱스를 이용해 배열 요소에 빠르게 접근할 수 있지만 배열은 크기를 변경할 수 없는 인스턴스이므로 크기를 늘리기 위해서는 새로운 배열을 생성하고 기존의 요소들을 옮겨야 하는 복잡한 과정을 거쳐야 한다.
물론 이 과정은 자동으로 수행되지만 요소의 추가 및 삭제 작업에 걸리는 시간이 매우 길어지는 단점을 가지게 된다.
2) LinkedList<E>
LinkedList 클래스는 ArrayList 클래스가 배열을 이용하여 요소를 저장함으로써 발생하는 단점을 극복하기 위해 고안되었다.
JDK 1.2부터 제공된 LinkedList 클래스는 내부적으로 연결 리스트(linked list)를 이용하여 요소를 저장한다.
배열은 저장된 요소가 순차적으로 저장되는데 연결 리스트는 저장된 요소가 비순차적으로 분포되며, 이러한 요소들 사이를 링크(link)로 연결하여 구성한다.
다음 요소를 가리키는 참조만을 가지는 연결 리스트를 단일 연결 리스트(singly linked list)라고 한다.
이러한 단일 연결 리스트는 요소의 저장과 삭제 작업이 다음 요소를 가리키는 참조만 변경하면 되므로, 아주 빠르게 처리될 수 있지만 단일 연결 리스트는 현재 요소에서 이전 요소로 접근하기가 매우 어렵다.
따라서 이전 요소를 가리키는 참조도 가지는 이중 연결 리스트(doubly linked list)가 좀 더 많이 사용된다.
LinkedList 클래스도 위와 같은 이중 연결 리스트를 내부적으로 구현한 것으로 LinkedList 클래스 역시 List 인터페이스를 구현하므로, ArrayList 클래스와 사용할 수 있는 메소드가 거의 같다.
3) Vector<E>
Vector 클래스는 JDK 1.0부터 사용해 온 ArrayList 클래스와 같은 동작을 수행하는 클래스다.
현재의 Vector 클래스는 ArrayList 클래스와 마찬가지로 List 인터페이스를 상속받아서 Vector 클래스에서 사용할 수 있는 메소드는 ArrayList 클래스에서 사용할 수 있는 메소드와 거의 같다.
하지만 현재에는 기존 코드와의 호환성을 위해서만 남아있으므로, Vector 클래스보다는 ArrayList 클래스를 사용하는 것이 좋다.
3. Stack과 Queue
Stack 클래스는 List 컬렉션 클래스의 Vector 클래스를 상속받아, 전형적인 스택 메모리 구조의 클래스를 제공한다.
스택 메모리 구조는 선형 메모리 공간에 데이터를 저장하면서 후입선출(LIFO)의 시멘틱을 따르는 자료 구조다.
즉, 가장 나중에 저장된(push) 데이터가 가장 먼저 인출(pop)되는 구조다.
더욱 복잡하고 빠른 스택을 구현하고 싶다면 Deque 인터페이스를 구현한 ArrayDeque 클래스를 사용하면 된다.
단, ArrayDeque 클래스는 Stack 클래스와는 달리 search() 메소드는 지원하지 않는다.
클래스로 구현된 스택과는 달리 자바에서 큐 메모리 구조는 별도의 인터페이스 형태로 제공된다.
이러한 Queue 인터페이스를 상속받는 하위 인터페이스는 다음과 같다.
1) Deque<E>
2) BlockingDeque<E>
3) BlockingQueue<E>
4) TransferQueue<E>
Queue 인터페이스를 직간접적으로 구현한 클래스는 많은데 그중에서도 Deque 인터페이스를 구현한 LinkedList 클래스가 큐 메모리 구조를 구현하는 데 가장 많이 사용된다.
1. 스택과 큐의 차이
- 스택(Stack)
- LIFO (Last In, First Out) 구조를 따릅니다.
- 요소의 추가와 제거가 한쪽 끝(스택의 꼭대기)에서 이루어집니다.
- 고정된 동작(푸시, 팝, 피크 등)이 명확하며, 구현 방법이 비교적 단순합니다.
- 자바에서는 Stack 클래스가 Vector 클래스를 상속받아 구현되어 있습니다. 이 클래스는 스택의 고유 동작을 정의하며, 직접적인 사용이 가능하도록 설계되었습니다.
- 큐(Queue)
- FIFO (First In, First Out) 구조를 따릅니다.
- 큐는 여러 변형(예: 우선순위 큐, 덱(Deque))이 존재합니다.
- 동작 방식(삽입, 삭제, 우선순위 등)이 다양한 구현에 따라 달라질 수 있습니다.
- 자바에서는 큐를 유연하게 설계하기 위해 Queue 인터페이스로 정의했으며, 다양한 구현체(LinkedList, PriorityQueue, ArrayDeque 등)가 이를 구현합니다.
2. 스택은 왜 클래스인가?
- 구현이 명확함
스택은 LIFO 동작을 따르며, 주요 연산(푸시, 팝, 피크 등)이 고정적이고 단순합니다. 이를 위해 자바는 별도의 추상화를 제공할 필요 없이 Stack이라는 클래스로 구현했습니다. - Vector 기반 설계
Stack 클래스는 기존의 Vector 클래스를 확장하여 구현되었습니다. 이는 스택의 자료 저장 방식이 Vector의 동작과 잘 맞아떨어졌기 때문입니다. 따라서 스택은 클래스 형태로 정의되어 기존 컬렉션 구조와 연동됩니다.
3. 큐는 왜 인터페이스인가?
- 다양한 구현 가능성
큐는 다양한 형태(우선순위 큐, 순환 큐, 덱 등)로 사용됩니다. 특정 구현에 종속되지 않도록 Queue라는 인터페이스를 만들어 다양한 구현체가 이를 따르도록 설계했습니다. - 유연성 제공
큐의 인터페이스는 최소한의 동작(삽입, 삭제 등)을 정의하며, 구체적인 동작 방식은 구현체(LinkedList, PriorityQueue 등)에 따라 달라집니다. 이를 통해 다양한 요구사항에 맞는 큐를 쉽게 선택하고 사용할 수 있습니다.
4. 클래스와 인터페이스 구현 기준
자바에서 클래스와 인터페이스를 선택하는 기준은 다음과 같습니다:
- 클래스 사용
- 동작이 명확하고 고정적인 경우(구현 방법이 크게 변하지 않을 경우).
- 기본적인 기능을 바로 사용할 수 있도록 제공할 때.
- 상태(필드)를 가지며 이를 활용하는 경우.
- 인터페이스 사용
- 여러 구현 방법이 있을 수 있는 경우.
- 기본적으로 동작의 규약(메서드 시그니처)만 정의하고, 구현은 각기 다르게 해야 할 때.
- 다중 구현(다중 상속의 효과)을 지원해야 하는 경우.
4. Set 컬렉션 클래스
Set 인터페이스를 구현한 모든 Set 컬렉션 클래스는 다음과 같은 특징을 가진다.
1) 요소의 저장 순서를 유지하지 않는다.
2) 같은 요소의 중복 저장을 허용하지 않는다.
대표적인 Set 컬렉션 클래스에 속하는 클래스는 다음과 같다.
1) HashSet<E>
HashSet 클래스는 Set 컬렉션 클래스에서 가장 많이 사용되는 클래스 중 하나다.
JDK 1.2부터 제공된 HashSet 클래스는 해시 알고리즘(hash algorithm)을 사용하여 검색 속도가 매우 빠르고 내부적으로 HashMap 인스턴스를 이용하여 요소를 저장한다.
HashSet 클래스는 Set 인터페이스를 구현하므로, 요소를 순서에 상관없이 저장하고 중복된 값은 저장하지 않는데 만약 요소의 저장 순서를 유지해야 한다면 JDK 1.4부터 제공하는 LinkedHashSet 클래스를 사용하면 된다.
해당 HashSet에 이미 존재하는 요소인지를 파악하기 위해서는 내부적으로 다음과 같은 과정을 거치게 된다.
1. 해당 요소에서 hashCode() 메소드를 호출하여 반환된 해시값으로 검색할 범위를 결정한다.
2. 해당 범위 내의 요소들을 equals() 메소드로 비교한다.
2) TreeSet<E>
TreeSet 클래스는 데이터가 정렬된 상태로 저장되는 이진 검색 트리(binary search tree)의 형태로 요소를 저장한다.
이진 검색 트리는 데이터를 추가하거나 제거하는 등의 기본 동작 시간이 매우 빠르다.
JDK 1.2부터 제공되는 TreeSet 클래스는 NavigableSet 인터페이스를 기존의 이진 검색 트리의 성능을 향상시킨 레드-블랙 트리(Red-Black tree)로 구현한다.
TreeSet 클래스는 Set 인터페이스를 구현하므로 요소를 순서에 상관없이 저장하고 중복된 값은 저장하지 않는다.
TreeSet 인스턴스에 저장되는 요소들은 모두 정렬된 상태로 저장된다.
5. Map 컬렉션 클래스
Map 인터페이스는 Collection 인터페이스와는 다른 저장 방식을 가진다.
Map 인터페이스를 구현한 Map 컬렉션 클래스들은 키와 값을 하나의 쌍으로 저장하는 방식(key-value 방식)을 사용한다.
Map 인터페이스를 구현한 모든 Map 컬렉션 클래스는 다음과 같은 특징을 가진다.
1) 요소의 저장 순서를 유지하지 않는다.
2) 키는 중복을 허용하지 않지만, 값의 중복은 허용한다.
대표적인 Map 컬렉션 클래스에 속하는 클래스는 다음과 같다.
1) HashMap<K, V>
HashMap 클래스는 Map 컬렉션 클래스에서 가장 많이 사용되는 클래스 중 하나다.
JDK 1.2부터 제공된 HashMap 클래스는 해시 알고리즘(hash algorithm)을 사용하여 검색 속도가 매우 빠르다.
HashMap 클래스는 Map 인터페이스를 구현하므로, 중복된 키로는 값을 저장할 수 없고 같은 값을 다른 키로 저장하는 것은 가능하다.
2) Hashtable<K, V>
Hashtable 클래스는 JDK 1.0부터 사용해 온 HashMap 클래스와 같은 동작을 하는 클래스다.
현재의 Hashtable 클래스는 HashMap 클래스와 마찬가지로 Map 인터페이스를 상속받는다.
따라서 Hashtable 클래스에서 사용할 수 있는 메소드는 HashMap 클래스에서 사용할 수 있는 메소드와 거의 같다.
하지만 현재에는 기존 코드와의 호환성을 위해서만 남아있으므로, Hashtable 클래스보다는 HashMap 클래스를 사용하는 게 좋다.
3) TreeMap<K, V>
TreeMap 클래스는 키와 값을 한 쌍으로 하는 데이터를 이진 검색 트리(binary search tree)의 형태로 저장한다.
이진 검색 트리는 데이터를 추가하거나 제거하는 등의 기본 동작 시간이 매우 빠르다.
JDK 1.2부터 제공된 TreeMap 클래스는 NavigableMap 인터페이스를 기존의 이진 검색 트리의 성능을 향상시킨 레드-블랙 트리(Red-Black tree)로 구현한다.
TreeMap 클래스는 Map 인터페이스를 구현하므로, 중복된 키로는 값을 저장할 수 없지만 같은 값을 다른 키로 저장하는 것은 가능하다.
6. Iterator
자바의 컬렉션 프레임워크는 컬렉션에 저장된 요소를 읽어오는 방법을 Iterator 인터페이스로 표준화하고 있다.
Collection 인터페이스에서는 Iterator 인터페이스를 구현한 클래스의 인스턴스를 반환하는 iterator() 메소드를 정의하여 각 요소에 접근하도록 하고 있다.
따라서 Collection 인터페이스를 상속받는 List와 Set 인터페이스에서도 iterator() 메소드를 사용할 수 있다.
boolean hasNext() | 해당 이터레이션(iteration)이 다음 요소를 가지고 있으면 true를 반환하고, 더 이상 다음 요소를 가지고 있지 않으면 false를 반환함. |
E next() | 이터레이션(iteration)의 다음 요소를 반환함. |
default void remove() | 해당 반복자로 반환되는 마지막 요소를 현재 컬렉션에서 제거함. (선택적 기능) |
하지만 현재 자바에서는 될 수 있으면 JDK 1.5부터 추가된 Enhanced for 문을 사용하도록 권장하고 있다.
Enhanced for 문을 사용하면 같은 성능을 유지하면서도 코드의 명확성을 확보하고 발생할 수 있는 버그를 예방해 준다.
하지만 요소의 선택적 제거나 대체 등을 수행하기 위한 경우에는 반복자(iterator)를 사용해야만 한다.
ListIterator 인터페이스는 Iterator 인터페이스를 상속받아 여러 기능을 추가한 인터페이스다.
Iterator 인터페이스는 컬렉션의 요소에 접근할 때 한 방향으로만 이동할 수 있다.
하지만 JDK 1.2부터 제공된 ListIterator 인터페이스는 컬렉션 요소의 대체, 추가 그리고 인덱스 검색 등을 위한 작업에서 양방향으로 이동하는 것을 지원한다.
단, ListIterator 인터페이스는 List 인터페이스를 구현한 List 컬렉션 클래스에서만 listIterator() 메소드를 통해 사용할 수 있다.
7. Comparable
Comparable 인터페이스는 객체를 정렬하는 데 사용되는 메소드인 compareTo() 메소드를 정의하고 있다.
자바에서 같은 타입의 인스턴스를 서로 비교해야만 하는 클래스들은 모두 Comparable 인터페이스를 구현하고 있다.
따라서 Boolean을 제외한 래퍼 클래스나 String, Time, Date와 같은 클래스의 인스턴스는 모두 정렬 가능하다.
이때 기본 정렬 순서는 작은 값에서 큰 값으로 정렬되는 오름차순이다.
'dev > 기본쌓기' 카테고리의 다른 글
[JAVA] 빠르게 정리하는 자바 문법 (9) (5) | 2025.01.02 |
---|---|
[JAVA] 빠르게 정리하는 자바 문법 (8) (3) | 2025.01.02 |
[JAVA] 빠르게 정리하는 자바 문법 (6) (3) | 2024.12.31 |
[JAVA] 빠르게 정리하는 자바 문법 (5) (1) | 2024.12.31 |
[JAVA] 빠르게 정리하는 자바 문법 (4) (0) | 2024.12.31 |