지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다.
지네릭스의 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.
지네릭 타입은 클래스와 메서드에 선언할 수 있는데, 먼저 클래스에 선언하는 지네릭 타입에 대해서 알아보자.
class Box<T> { // 지네릭 타입 T를 선언. T는 타입변수
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
T는 타입 변수라고 하며 T가 아닌 다른 것을 사용해도 된다. ArrayList<E">의 경우 Element의 첫 글자를 사용, Map<K,V>와 같이 콤마를 구분자로 여러 개를 사용가능 K는 key V는 Value를 의미한다.
이들은 기호의 종류만 다를 뿐 임의의 참조형 타입을 의미한다는 것은 모두 같다.
Box<String> b = new Box<String>(); // 타입 T대신 실제 타입 지정
b.setItem(new Object()); // 에러. String 외의 타입은 지정 불가
b.setItem("ABC"); // OK. String 타입이므로 가능
class Box<T>
Box<T"> 지네릭 클래스, T의 Box 또는 T Box라고 읽는다
T 타입 변수 또는 타입 매개변수
Box 원시 타입(raw type)
모든 객체에 대해 동일하게 동작해야하는 static멤버에 타입변수 T를 사용할 수 없다.
T는 인스턴스 변수로 간주되기 때문이다. (static 멤버는 인스턴스 변수를 참조할 수 없다.)
지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만,
new T[10] 과 같이 배열을 생성하는 것은 안된다.
new 연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 한다.
instanceof 연산자도 같은 이유로 T를 피연산자로 사용할 수 없다.
지네릭 클래스 Box< T>가 아래와 같이 정의되어 있다고 가정한다.
이 Box< T>의 객체에는 한 가지 종류, 즉 T타입의 객체만 저장할 수 있다.
이전과 달리 ArrayList를 이용해서 여러 객체를 저장할 수 있도록 했다.
class Box<T> {
ArrayList<T> List = new ArrayList<T>();
void add(T item) {
list.add(item);
}
T get(int i) {
return list.get(i)
}
ArrayList<T> getList() {
return list;
}
int size() {
return list.size();
}
public String toString() {
return list.toString();
}
}
Box< T>의 객체를 생성할 때는 다음과 같이 한다.
참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다.
일치하지 않으면 에러가 발생한다.
Box<Apple> appleBox = new Box<Apple>(); // OK
Box<Apple> appleBox = new Box<Grape>(); // 에러
Box<Fruit> appleBox = new Box<Apple>(); // 에러. 대입된 타입이 다르다.
단, 두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다.
FruitBox는 Box의 자손이라고 가정하자.
Box<Apple> appleBox = new FruitBox<Apple>(); // Ok. 다형성
JDK1.7부터는 추정이 가능한 경우 타입을 생략할 수 있게 됐다.
참조변수의 타입으로부터 Box가 Apple타입의 객체만 저장한다는 것을 알 수 있기 때문에,
생성자에 반복해서 타입을 지정해주지 않아도 되는 것이다.
Box<apple> appleBox = new Box<Apple>();
Box<apple> appleBox = new Box<>(); // Ok. JDK1.7부터 생략 가능
타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만,
그래도 여전히 모든 종류의 타입을 저장할 수 있다는 것에는 변함이 없다.
그렇다면, 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법은 없는걸까
FruitBox<Toy> fruit Box = new FruitBox<Toy>();
fruitBox.add(new Toy()); // OK. 과일상자에 장난감을 담을 수 있다.
다음과 같이 지네릭 타입에 'extends'를 사용하면, 특정 타입의 자손만 대입할 수 있게 제한할 수 있다.
class FruitBox<T extends Fruit> { // Fruit의 자손만 타입으로 지정가능
ArrayList<T> list = new ArrayList<T>();
...
}
여전히 한 종류의 타입만 담을 수 있지만, Fruit클래스의 자손들만 담을 수 있다는 제한이 더 추가된 것이다.
FruitBox<Apple> appleBox = nuew FruitBox<Apple>(); // Ok
FruitBox<Toy> toyBox = nuew FruitBox<Toy>(); // 에러. Toy는 Fruit의 자손이 아니다.
게다가 add()의 매개변수의 타입 T도 Fruit와 그 자손 타입이 될 수 있으므로,
아래와 같이 여러 과일을 담을 수 있는 상자가 가능하게 된다.
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple()); // Ok. Apple이 Fruit의 자손
fruitBox.add(new Grape()); // Ok. Grape가 Fruit의 자손
다형성에서 조상타입의 참조변수로 자손타입의 객체를 가리킬 수 있는 것처럼,
매개변수화된 타입의 자손 타입도 가능한 것이다.
타입 매개변수 T에 Object를 대입하면, 모든 종류의 객체를 저장할 수 있게 된다.
만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면,
이때도 'extend'를 사용한다.
'implements'를 사용하지 않는다는 점에 주의하자.
interface Eatable {}
class FruitBox<T extends Eatable> {...}
클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 아래와 같이 '&'기호로 연결한다.
class FruitBox<T extends Fruit & Eatable> {...}
이제 FruitBox에는 Fruit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수 T에 대입될 수 있다.