컬렉션과 제네릭
- 컬렉션의 개념
컬렉션(collection)은 안드로이드를 비롯한 자바 프로그램을 작성하는데 빼놓을 수 없는 중요한 도구이다. 자바의 JDK는 자료 구조들을 컬렉션으로 만들어 제공한다. 컬렉션은 배열이 가진 고정 크기의 단점을 극복하기 위해 객체들을 쉽게 삽입, 삭제, 검색할 수 있는 가변 크기의 컨테이너(container)이다.
- 컬렉션을 위한 자바 인터페이스와 클래스
java.util 패키지는 다양한 컬렉션 인터페이스와 컬렉션 클래스를 제공한다.
컬렉션 클래스는 개발자가 바로 사용할 수 있는 것들로서, Vector<E>와 ArrayList<E>는 가변 크기의 배열을 구현하며, LinkedList<E>는 노드들이 링크로 연결되는 리스트를 구현한다. Stack<E>는 스택을 구현하며, HashSet<E>은 집합을 구현한다. 이들은 모두 Collection<E>를 상속받고, 단일 클래스의 객체만을 요소로 다루는 공통점이 있다. HashMap<K,V>는 키(K)와 값(V)의 쌍으로 이루어지는 데이터를 저장하고, 키로 쉽게 검색하도록 만든 컬렉션이다.
- 컬렉션의 특징
- 컬렉션은 제네릭(generics)이란느 기법으로 만들어져 있다.
컬렉션 클랫의 이름에는 <E>,<K>,<V> 등이 항상 포함된다. 이들은 타입 매개 변수라고 하며, Vector<E>에서 E 대신 Integer와 같이 구체적인 타입을 저장하면 Vector<Integer>는 정수 값만 저장하는 벡터로, Vector<String>은 문자열만 저장하는 벡터로 사용할 수 있다. 특정 타입만 다루지 않고 여러 종류의 타입으로 변신할 수 있도록, 컬렉션을 일반화시키기 위해 <E>를 사용하는 것이다. 그러므로 E를 일반화시킨 타입 혹은 ㅁ제넥릭 타입(generic type)이라 부른다. 컬렉션은 여러 타입의 값을 다룰 수 있도록 변신이 가능한 자료 구조이지만, 컬렉션을 사용할 때는 지정된 특정 타입의 값만 저장 가능하다.- 컬렉션의 요소는 객체들만 가능하다.
int, char, double 등의 기본 타입의 데이터는 원칙적으로 컬렉션의 요소로 불가능하다. 따라서 기본 타입의 값이 삽입되면 자동 박싱에 의해 Wrapper 클래스 탕비으로 변환되어 객체로 저장한다.
- 제네릭의 기본 개념
제네릭은 JDK 1.5 버전부터 도입되었다. 제네릭은 모든 종류의 타입을 다룰 수 있도록, 클래스나 메소드를 타입 매개변수(generic type)를 이용하여 선언하는 기법이다. 자바의 제네릭은 c++의 템플릿(template)과 동일하다. 자바에서 제네릭은 클래스 코드를 찍어내듯이 생산할 수 있도록 일반화시키는 도구이다. 예를 들어 Stak<E>에서 E에 구체적인 타입을 지정하면 지정된 타입만 다룰 수 있는 구체화(specialized)된 스택이 된다. 따라서 Integer로 구체화 하면 정수만 다루는 스택이 되는 것이다.
- 제네릭 타입 매개변수
컬렉션 클래스에서 타입 매개변수로 사용하는 문자는 다른 변수와 혼동을 피하기 위해 일반적으로 하나의 대문자를 사용한다.
(1) E : Element를 의미하며 컬렉션에서 요소임을 나타낸다.
(2) T : Type을 의미
(3) K : Key를 의미
(4) V : Value를 의미
제네릭 컬렉션 활용
- Vector<E>
Vector<E>는 배열을 가변 크기로 다룰 수 있게 하고, 객체의 삽입, 삭제, 이동이 쉽도록 구성한 컬렉션 클래스이다. 벡터는 삽입되는 요소의 개수에 따라 자동으로 크기를 조절하고, 요소의 삽입과 삭제에 따라 자동으로 요소들의 자리를 이동한다.
♣ 벡터 생성
벡터를 생성할때, Vector<E>의 E에 요소로 사용할 타입을 지정해야 한다. 기본 타입은 E에 사용할 수 없고 객체만이 E요소로 사용할 수 있다. 레퍼런스 변수 선언과 벡터 생성을 분리하여 코드를 만들 수 있다. 만일 Vector<E>에서 E에 구체적인 타입을 지정하지 않고 Vector로만 사용하면, 컴파일러가 경고 메세지를 준다. 또한 벡터의 용량을 초기에 설정하고자 하면 다음과 같이 설정하면 된다.Vector<Integer> v = new Vector<Integer>(5); // 초기 용량이 5인 벡터
♣ 벡터에 요소 삽입
add() 메소드를 이용하면 벡터의 끝이나 중간에 요소를 삽입할 수 있다.v.add(Integer.valueOf(5));// 정수를 Integer 객체를 만들어 삽입한다. v.add(5);// 5 -> new Integer(5)로 자동 박싱된다.
자동 박싱에 의해 int 타입의 정수는 자동으로 Integer 객체로 변환되어 삽입된다. 그러나 Integer 외의 다른 타입의 객체를 삽입할 수 없다.
벡터에는 null도 삽일할 수 있기 때문에, 벡터를 검색할 때 null이 존재할 수 있음을 염두에 두어야 한다.
또한 add()를 이용하여 벡터의 중간에 객체를 삽입할 수 있다.v.add(2,100); // index 2에 100을 삽입
이 코드는 index 2에 100을 삽입하고 그 뒤에 있는 원소들을 모두 한 자리씩 뒤로 이동시킨다. 그러나 벡터에 1개의 요소(인덱스 0의 위치)만 들어 있는경우 인덱스 1이 빈 공간이기 때문에 예외가 발생한다.
♣ 벡터 내의 요소 알아내기
벡터 내에 존재하는 요소를 알아내기 위해서는 get()이나 elementAt() 메소드를 이용한다.Integer obj = v.get(1);//벡터의 1번째 Integer 객체를 얻어낸다. int i = obj.intValue();// obj에 있는 정수를 알아낸다. 이 값으 4 int i = v.get(1);//위의 두 코드를 한줄로 한 것, 자동 언박싱으로 v.get(1)이 리턴하는 Integer 객체의 정수 값을 i에 저장
♣ 벡터의 크기와 용량 알아내기
벡터의 크기란 벡터에 들어 있는 요소의 개수를 의미하며, 벡터의 용량이란 수용할 수 있는 크기를 말한다. 벡터의 크기는 size() 메소드를 호출한다. 벡터의 용량은 capacity() 메소드를 호출한다.int len = v.size();// 벡터의 크기, 벡터에 존재하는 요소 객체의 수 int cap = v.capacity(); // 벡터의 용량
♣ 벡터에서 요소 삭제
벡터 내에 임의의 인덱스에 있는 요소를 삭제할 수 있다. remove() 메소드를 이용한다. 모든 요소를 삭제하려면, removeAllElements()를 호출한다.v.remove(1); // 인덱스 1의 위치에 있는 요소 삭제 v.removeAllElements();
인덱스 1의 위치에 있는 요소를 삭제하고 뒤에 있는 요소들이 한 자리씩 앞으로 이동한다.
- 컬렉션과 자동 박싱/ 언박싱
컬렉션은 객체들만 요소로 다룬다고 설명하였다. 따라서 기본 타입의 값은 Wrapper 클래스로 객체화하여 삽입한다. 그러나 자동 박싱에 의해 int 타입을 사용하면 자동으로 Integer 객체로 변환되어 삽입된다. 또한 컬렉션으로부터 값을 얻어내는 과정에서는 자동 언박싱이 일어나 벡터로부터 값을 바로 얻어낼 수 있다.
- 자바의 타입 추론 기능의 진화, java7 , java 10
Java 7부터 제네릭의 객체 생성부의 <> 내(다이어모드 연산자)에 타입 매개변수를 생략하면 컴파일러가 추론하여 타입 매개변수를 찾아주도록 하였다. Java10은 아예 var 키워드를 도입하여 컴파일러에게 변수 타입을 추론하도록하는 기능을 더하였다.Vector<Integer> v = new Vector<>();//java 7 var v = new Vector<Integer>(); //java 10
- ArrayList<E>
ArrayList<E>는 가변 크기의 배열을 구현한 컬렉션 클래스로서 경로명은 java.util.ArrayList이며, Vector 클래스와 거의 동일하다. 크게 다른 점은 ArrayList는 스레드 간에 동기화를 지원하지 않기 때문에, 다수의 스레드가 동시에 ArrayList에 요소를 삽입하거나 삭제할 때 ArrayList의 데이터가 훼손될 우려가 있다. 그러나 멀티스레드 동기화를 위한 시간 소모가 없기 때문에, ArrayList는 Vector보다 속도가 빨라, 단일 스레드 응용에는 더 효과적이다.
- 컬렉션의 순차 검색을 위한 lterator
Vector, ArrayList, LinkedList, Set과 같이 요소가 순서대로 저장된 컬렉션에서 요소를 순차적으로 검색할 때는 java.util 패키지의 Iteration<E> 인터페이스를 사용하면 편리하다. 여기서 <E>에는 컬렉션의 매개변수와 동일한 타입을 지정해야 한다.Vector<Integer> v = new Vector<>();// 요소가 Integer인 벡터 Iterator<Integer> it = v.iterator();//벡터 v의 요소를 순차 검색할 Iterator 객체 리턴
위와 같이 벡터 v의 iterator()를 호출하여, 벡터 v의 각 요소를 순차적으로 검색할 수 있는 Iterator 객체를 얻어낸다. Iterator 객체를 반복자라고 부른다.
벡터 v의 요소 타입(Integer)에 맞추어 Iterator<E>의 E에 Integer를 지정하였다. it 객체를 이용하면 벡터의 각 요소를 순차 검색할 수 있다.while(it.hasNext()){ int n = it.next(); //it가 가리키는 요소 리턴, it의 요소 타입은 Integer이므로 정수 리턴 }
- HashMap<K, V>
HashMap<K, V> 컬렉션은 경로명이 java.util.HashMap이며, '키(key)'와 '값(value)'의 쌍으로 구성되는 요소를 다룬다. K는키로 사용할 데이터 타입을, V는 값으로 사용할 데이터 타입의 타입매개변수이다. 해시맵 객체의 내부 구성은 키와 값을 저장하는 자료구조를 가지고, put(), get() 메소드를 이용하여 요소를 삽입하거나 검색한다.HashMap<String, String> h = new HashMap<>(); //해시맵 생성 h.put("apple", "사과"); // apple 키와 사과 값의 쌍을 h에 삽입 String kor = h.get("apple"); // apple 키로 값 검색. kor는 검색된 값 '사과
put(key, value) 메소드는 키와 값을 받아, 키를 이용하여 해시 함수를 실행하고 해시 함수가 리턴하는 위치에 키와 값을 저장한다. get(key) 메소드는 키를 이용하여 동일한 해시 함수를 실행하여 값이 저장된 위치를 알아내어 값을 리턴한다. 해시맵은 해시 함수를 통해 키와 값이 저장되는 위치를 결정하므로 사용자는 그 위치를 알 수 없고, 삽입되는 순서와 들어 있는 위치 또한 관계가 없다.
♣ 해시맵의 장단점
해시맵은 List<E> 인터페이스를 상속받은 Vector나 ArrayList와는 다른 점이 있다.
1. 요소와 삽입, 삭제 시간이 매우 빠르다. 요소의 위치를 결정하는 해시 함수가 간단한 코드로 이루어져 있고 Vector나 ArrayList와는 달리 요소의 삽입 삭제 시 다른 요소들의 위치 이동이 필요 없기 때문이다.
2. 요소 검색은 더욱 빠르다. 해시맵의 get() 메소드가 호출되면 해시 함수가 key가 저장된 위치를 단번에 찾아내므로, Vector나 ArrayList에서처럼 모든 요소들을 하나씩 비교하는 시간 낭비가 전혀 없다.
3. 해시맵은 인덱스를 이용하여 요소에 접근할 수 없고 오직 키로만 검색해야 한다. 그러므로 해시맵은 빠른 삽입과 검색이 필요한 응용에 용이하다.
♣해시맵의 전체 검색
해시맵에 들어 있는 요소들을 모두 알아내어 보면, 해시맵의 모든 키를 알아낸 후, 각 키에 대해 하나씩 '값'을 알아내는 방식으로 작성하면 된다. HashMap의 keySet() 메소드는 모든 키를 Set 컬렉션으로 만들어 리턴한다.Set<String> keys = h.keySet(); // h에 있는 모든 키를 Set 컬렉션으로 리턴 Iterator<String> it = keys.iterator(); //Set의 각 문자열을 순차 검색하는 Iterator 리턴 while(it.hasnext()){//키가 존재할 때까지 Stirng key = it.next();//키 String value = h.get(key); //값 }
- LinkedList<E>
LinkedList<E>는 List<E> 구현한 클래스로서 경로명이 java.util.LinkedList이다. LinkedList는 요소들을 양방향으로 연결하여 관리한다는 점을 제외하고 Vector, ArrayList와 거의 같다. add() 메소드와 get() 메소드를 가진 LinkedList는 맨 앞과 맨 뒤를 가리키는 head,tail 레퍼런스를 가지고 있어, 맨 앞이나 맨 뒤, 중간에 요소의 삽입이 가능하며 인덱스를 이용하여 요소에 접근할 수 있다.- Collections 클래스 활용
java.util 패키지에 포함된 Collections 클래스는 다음과 같이 컬렉션을 다루는 유용한 여러 메소드를 지원한다.
※ sort() : 컬렉션에 포함된 요소들의 정렬
※ reverse() : 요소를 반대 순으로 정렬
※ max(), min() : 요소들의 최댓값과 최솟값 찾아내기
※ binarySearch() : 이진 탐색
Collections 클래스의 메소드는 모두 static 타입이므로 Collections 객체를 생성할 필요는 없다. 이 유틸리티 메소드들은 인자로 컬렉션 객체를 전달받아 처리한다.Vector<Integer> v = new Vector<>(); Collection.sort(v);//static 메소드이기 때문에 객체 생성 불필요 Collection.reverse(v); Collection.min(v); int index = Collection.binarysearch(v,t);
제네릭 만들기
- 제네릭 클래스
제네릭 클래스를 작성하는 방법은 기존의 클래스 작성 방법과 유사한데, 클래스 이름 다음에 일반화된 타입의 매개변수를 <와 > 사이에 추가한다는 차이가 있다.public class MyClass<T>{ // 제네릭 클래스 MyClass, 매개변수 T T val;//변수 val의 타입은 T void set(T a){ val = a;// T타입의 값 a를 val에 지정 } void get(){ return val; // T 타입의 값 val 리턴 } }
♣ 제네릭 객체 생성 - 구체화(specialization)
제네릭 클래스에 구체적인 타입을 대입하여 구체적인 객체을 생성하는 과정을 구체화라고 부르며, 이 과정은 자바 컴파일러에 의해 이루어진다. MyClass<T>에서 T에 구체적인 타입을 지정하여 객체를 생성하는 예는 다음과 같다.MyClass<String> s = new MyClass<>(); // T를 String으로 구체화 MyClass<Integer> i = new MyClass<>(); // T를 Integer로 구체화
제네릭의 구체화에는 기본 타입은 사용할 수 없음에 주의해야 한다.
♣타입 매개변수
제네릭 클래스 내에서 제네릭 타입을 가진 객체의 생성은 허용되지 않는다. 자바컴파일러가 제네릭 타입의 객체를 생성하는 코드를 컴파일할 때, 그 객체에 대한 구체적인 타입을 알 수 없어, 호출될 생성자를 결정할 수 없고, 또한 객체 생성시 어떤 크기의 메모리를 할당해야 할 지 전혀 알 수 없기 때문이다. 제네릭 클래스 내에서 제네릭 타입의 객체를 생성할 수 없는 것과 같은 이유로 배열도 생성할 수 없다.
- 제네릭과 배열
제네릭에서는 배열에 대한 제한을 두고 있다. 제네릭 클래스 또는 인터페이스 타입의 배열은 생성할 수 없다. 그러나 제네릭 타입의 배열은 생성할 수 있다.GStack<Integer>[] gs = new GStack<>(); // 제네릭 클래스배열은 생성할 수 없음 public void myArray(T[] a) {...} // 제네릭 타입의 배열 생성
- 제네릭 메소드
클래스의 일부 메소드만 제네릭으로 구현할 수 있다.static <T> void toStack(T[] a, GStack<T> gs){ for(int i = 0; i< a.length; i++){ gs.push(a[i]); } }
타입 매개변수는 메소드의 리턴 타입 앞에 선언된다. 위의 toStack()에서 <T>가 타입 매개변수 선언이다. 제네릭 메소드를 호출할 때는 컴파일러가 메소드의 인자를 통해 타입을 유출할 수 있어 제네릭 클래스나 인터페이스와는 달리 타입을 명시하지 않아도 된다. 만약 메소드의 인자가 String과 Object가 오면 Object는 String의 슈퍼 클래스이기 때문에 컴파일러가 매개변수 T를 Object로 유츄한다.
- 제네릭의 장점
- 동적으로 타입이 결정되지 않고 컴파일 시에 타입이 결정되므로 보다 안전한 프로그래밍이 가능하다.
- 런타임 타입 충돌 문제를 방지한다.
- 개발 시 타입 캐스팅 절차가 불필요하다.
- ClassCastExecption 오류를 방지한다.