지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크 (compile-time type check
)를 해주는 기능이다.
컴파일 시에 객체의 타입을 체크한다면 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄일 수 있게된다.
타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 워낼의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.
ArrayList
와 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있긴하지만 보통의 경우 한 종류의 객체를 담는다. 그런데도 꺼낼때마다 타입체크를 하고 형변환을 하는 것은 아무래도 불편하다. 게다가 원하지 않는 종류의 객체가 포함되는 것을 막을 수도 없다. 이러한 문제들을 지네릭스를 통해 해결할 수 있다.
장점
1. 타입 안정성 제공
2. 타입체크와 형변환을 생략가능 -> 코드 간결
지네릭 타입은 클래스와 메서드에 선언할 수 있다. 먼저 클래스에 선언하는 지네릭 타입을 살펴보자.
클래스 Box가 다음과 같이 정의되어있다고 가정하자.
class Box {
Object item;
void setItem(Object item) {
this.item = item;
}
Object getItem(){
return item;
}
이 클래스를 지네릭 클래스로 변경하면 다음과 같다.
class Box<T> {
T item;
void setItem(T item) {
this.item = item;
}
T getItem(){
return item;
}
Box<T>
에서 T를 타입변수(type variables)
이라고 한다. 이는 임의의 참조형 타입을 뜻한다. 지네릭 클래스가 된 Box 클래스의 객체를 생성시에는 참조변수와 생성자에 타입 T
대신에 실제 타입을 지정한다.
Box<String> b = new Box<String>(); // 실제 타입 지정
b.setItem("ABC");
String item = b.getItem(); // 형변환 필요없음
class Box<T> {}
Box<T>
: 지네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽음
T
: 타입변수 or 타입 매개변수. (T는 타입 문자)
Box
: 원시 타입(raw type)
컴파일 후에 Box<String>
, Box<Integer>
는 이들의 원시타입
인 Box
로 바꾸니다. 즉 지네릭 타입이 제거가 된다.
모든 객체에 대해 동이랗게 동작해야하는 static
멤버에 타입 변수 T
를 사용할 수는 없다. T
는 인스턴스 변수로 간주되기 때문이다. static
멤버는 인스턴스 변수를 참조할 수가 없다.
static
멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 된다.
지네릭 타입의 배열은 직접적으로 생성할 수 없다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만 new T[10]
과 같이 배열을 생성하는 것은 안된다.
그 이유는 new
연산자 때문이다. 이 연산자는 컴파일 시점에 타입 T
가 뭔지 정확히 알아야한다.
꼭 지네릭 배열을 생성해야한다면 new
대신 Reflection API
의 newInstance()
와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object
배열을 생성해서 복사한 다음 T[]
로 형변환하는 방법 등을 사용해야 한다.
Box<Apple> appleBox = new Box<Apple>(); // ok
Box<Apple> appleBox = new Box<Grape>(); // error
Box<Fruit> appleBox = new Box<Apple>(); // error - 대입된 타입 다름
Box<Apple> appleBox = new FruitBox<Apple>(); // ok - 다형성
Box<Apple> appleBox = new Box<Apple>(); // ok
Box<Apple> appleBox = new Box<>(); // ok
Box<T>
객체에 void add(T item)
으로 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다.Box<Apple> appleBox = new Box<>();
appleBox.add(new Apple()); // ok
appleBox.add(new Grape()); // error
void add(Fruit item)
이 되므로 자손들은 이 메서드의 매개변수가 될 수 있다. (Apple이 Fruit의 자손이라고 가정하)Box<Fruit> appleBox = new Box<>();
appleBox.add(new Fruit()); // ok
appleBox.add(new Apple()); // ok
지네릭 타입에 extends
를 사용하면 특정 타입의 자손들만 대입할 수 있도록 제한가능하다.
class FruitBox<T extends Fruit> {
ArrayList<T> list = new ArrayList<T>();
}
여전히 한 종류의 타입만 담을 수 있으며, Fruit 클래스의 자손들만 담을 수 있다는 뜻이다.
add()의 매개변수의 타입 T도 Fruit와 그 자손 타입이 될 수 있으므로, 여러 과일을 담을 수 있게 된다.
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple()); // ok
fruitBox.add(new Grape()); // ok
만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면 이때도 extends
를 사용한다. (implements
아님)
interface Eatable {}
class FruitBox<T extends Eatable> {...}
Fruit의 자손이면서 Eatable도 구현해야한다면 &
연산자를 사용한다.
class FruitBox<T extends Fruit & Eatable> {...}
지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다.
지네릭 타입은 컴파일러가 컴파일할 때만 사용하거 제거해버리기 때문이다.
이럴 때 사용하도록 만들어진게 와일드 카드 ?
이다.
<? extends T> 와일드 카드의 상한 제한, T와 그 자손들만 가능
<? super T> 와일드 카드의 하한 제한, T와 그 조상들만 가능
<?> 제한 없음, 모든 타입이 가능 (<? extends Object>)와 동일
다음 예제를 살펴보자
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
이 메서드는 매개변수로 FruitBox<Fruit>
뿐 아니라 FruitBox<Apple>
, FruitBox<Grape>
도 가능하다.
자바의 정석 - 남궁성