JDK 1.5에서 도입된 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법이다.
public interface List<T> extends Collection<E> { ... }
List<String> strList = new ArrayList<String>();
저 꺽쇠 괄호< > 가 바로 제네릭이다.
제네릭을 이용하여 데이터 타입을 일반화할 수 있다.
여러 타입이 들어갈 수 있는 클래스인 MyClass 가 있다고 가정해보자
class MyClass {
Object item;
void setItem(Object item) { this.item = item; }
Object getItem() { return item; }
}
클래스의 필드인 item으로 어떤 타입이 들어올지 모르기 때문에 최상위 클래스인 Object 타입으로 선언되어 있는 상태이다.
이걸 제네릭 클래스로 변경하면 클래스 옆에 <T>를 붙이고 Object 대신 T를 쓰면 된다.
class MyClass<T> {
T item;
void setItem(T item) { this.item = item; }
T getItem() { return item; }
}
MyClass<T> 에서 T를 타입 변수(type variable)라고 한다.
'Type'의 첫 글자에서 따온 것이고 타입 변수를 굳이 T로 쓰지 않아도 된다.
ArrayList<E>의 경우 타입 변수 E는 'Element(요소)'의 의미로 사용하고
Map<K, V>의 경우 Key 와 Value의 앞 글자를 가져와 타입 변수로 사용한다.
위의 MyClass 클래스를 사용해보자.
제네릭 클래스의 객체를 생성할 때는 참조변수와 생성자에
타입 T 대신 실제 타입을 지정해주어야 한다.
MyClass<String> mc = new MyClass<String>();
mc.setItem(3); // 에러. String 이외의 타입 지정불가
mc.seItem("Hello"); // String 타입이므로 가능
위의 코드에서 T 타입 대신 String 타입을 정의해줬으므로
MyClass 클래스는 다음과 같이 정의된 것과 같아진다.
class MyClass<String> {
String item;
void setItem(String item) { this.item = item; }
String getItem() { return item; }
}
mc.setItem(3);은 String 타입으로 인자를 받지 않기 때문에 에러가 발생한다.
MyClass에 애초에 String 타입만 담을 거라면 처음부터 MyClass<String> 으로 만들어도 됐다.
반면 MyClass<T> 클래스는 어떤 타입이든 한 가지 타입을 정해서 담을 수 있다.
제네릭의 타입을 지정하지 않아도 사용이 가능하다. 다만 타입을 지정하지 않아서 안전하지 않다는 경고가 뜬다.
제네릭이 없을 때 만들어진 코드와의 호환성을 위한 허용일 뿐, 이렇게 쓰는 건 지양해야 된다.
MyClass mc = new MyClass();
mc.setItem(new Object()); // 경고.
mc.setItem("Hello"); // 경고.
JDK 1.7 이후부터는 생성자 부분의 제네릭 타입을 생략할 수 있다.
MyClass<String> mc = new MyClass<String>();
// 생성자 부분의 제네릭 타입 생략 가능
MyClass<String> mc = new MyClass<>();
참조 변수의 타입으로부터 객체의 타입을 알 수 있기 때문에, 굳이 생성자에서 반복해서 타입을 지정해주지 않아도 된다.
그렇다면 제네릭을 왜 쓸까? 그냥 Object로 멤버를 선언해도 같은 거 아닌가?
예를 들어, List의 경우 다양한 종류의 객체를 담을 수 있지만 보통 한 종류의 객체를 담는 경우가 더 많다.
이런 경우에는, 원하는 타입의 객체가 들어있다는 보장이 없기 때문에 요소를 꺼낼 때마다 요소의 타입을 체크하고 형변환을 하는 것은 불편하다.
List list = new ArrayList();
list.add("hello");
...
String s = (String)list.get(0);
제네릭을 사용하면 컴파일 시에 타입을 체크해주기 때문에
의도하지 않은 타입의 객체가 저장되는 것을 막아주고, 저장된 객체를 꺼내올 때 잘못된 타입으로 형변환되는 것을 방지해준다.
List<String> list = new ArrayList<String>();
list.add("Hello");
...
String s = list.get(0);
- 타입 안정성을 제공
- 타입체크와 형변환을 생략할 수 있기에 코드가 간결해짐
class Box<T> {
List<T> list = new ArrayList<>();
void add(T item) { list.add(item); }
...
}
제네릭 클래스 Box<T> 가 정의되어 있다.
이 Box<T> 의 객체는 한 가지 종류(T 타입)만 담을 수 있다.
Box<T> 의 객체를 생성할 때, 참조변수와 생성자의
매개변수화된 타입이 일치해야 한다.
Box<Apple> appleBox = new Box<Apple>(); // OK
Box<Apple> appleBox = new Box<Grape>(); // 에러
두 타입이 상속관계에 있어도 마찬가지이다.
Apple이 Fruit의 자손이라고 가정하자
Box<Fruit> fruitBox = new Box<Apple>(); // 에러. 대입된 타입 다름
제네릭 클래스 타입이 상속관계에 있고, 매개변수화된 타입이 같은 것은 괜찮다.
FruitBox가 Box의 자손이라고 가정하다
Box<Apple> appleBox = new FruitBox<Apple>(); // OK. 다형성
Box<T> 객체에 void add(T item)으로 객체를 추가할 때, 매개변수화된 타입과 다른 타입은 메소드의 인자로 넘길 수 없다.
Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple()); // OK.
appleBox.add(new Grape()); // 에러. Apple 객체만 가능
그러나 상속 관계에 있다면 다형성 덕분에 메소드의 인자로 넘길 수 있다.
Apple이 Fruit의 자손이라 가정하자.
Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit()); // OK.
fruitBox.add(new Apple()); // OK. 다형성 void add(Fruit item)
제네릭을 사용하면 한 종류의 타입만 사용하도록 제한을 둘 수 있지만, 여전히 모든 종류의 타입을 지정한다는 것에는 변함이 없다.
그렇게 되면 이런 문제가 생길 수 있다.
FruitBox<Toy> fruitBox = new FruitBox<Toy>();
fruitBox.add(new Toy());
문법적으로 문제가 없다. 그러나 의도된 대로라면 과일 박스에는 과일만 담겨야 하는데 장난감이 담겨버린다.
이런 문제를 해결하는 방법은 제네릭 타입에 제한을 두는 것이다.
extends를 사용하면 그 타입의 자손들만 제네릭 타입으로 매개변수화될 수 있다.
class FruitBox<T extends Fruit> {
List<T> list = new ArrayList<T>();
...
이렇게 하면 Fruit의 자손 클래스들만 FruitBox 에 담을 수 있다.
FruitBox<Apple> appleBox = new FruitBox<Apple>(); // OK
FruitBox<Toy> toyBox = new FruitBox<Toy>(); // 에러. Toy는 Fruit의 자손 X
만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면,
이 때에도 extends 키워드를 사용해야 한다. implements 아님!
interface Eatable { }
class Box<T extends Eatable> { }
Fruit 클래스의 자손이면서 Eatable 인터페이스를 구현한 객체를 받고 싶다면
& 기호로 연결한다.
class Box<T extends Fruit & Eatable> { }
제네릭을 사용하는 이유는 객체별로 다른 타입을 지정하기 위해서이다.
Box<Apple> appleBox = new Box<Apple>();
Box<Grape> grapeBox = new Box<Grape>();
그러나 모든 객체에서 동일하게 작동되어야 하는 static 멤버에 타입 변수 T를 사용할 수 없다. 제네릭 타입 변수는 인스턴스변수로 간주되기 때문이다.
class Box<T> {
static T item; // 에러.
static int compare(T t1, T t2) { ... } // 에러.
static 멤버는 매개변수화된 타입과는 관계 없이 동일한 것이어야 하기 때문에 제네릭 사용이 불가능하다.
과일이 담긴 박스를 담아 주스를 만들려는 코드가 있다고 생각해보자.
class Juicer {
// static Juice makeJuice(FruitBox<T> box) { ... } // 에러. 제네릭 사용 불가
static Juice makeJuice(FruitBox<Fruit> box) { ... }
방금 전에 static 멤버에서는 제네릭 타입 변수를 사용할 수 없다고 하였다.
저렇게 되면 어쩔 수 없이 특정 타입(Fruit)을 지정해주어야 한다.
그러나 제네릭 타입을 FruitBox<Fruit>로 고정해놓으면 다른 과일 박스,
FruitBox<Apple>이나 FruitBox<Grape>는 매개변수로 받을 수 없다.
이럴 때 사용하기 위해 고안된 것이 바로 와일드 카드이다.
와일드 카드는 어떠한 타입도 올 수 있다는 의미이다.
제네릭 코드에서 <?>로 되어있는 물음표 기호가 와일드 카드이다.
<?> 만으로는 모든 타입이 올 수 있기에 Object 타입과 다를 게 없으므로,
extends 와 super 키워드로 상한과 하한을 제한할 수 있다.
T와 그 자손들만 올 수 있음T와 그 조상들만 올 수 있음와일드 카드를 사용해 위의 코드를 적절히 바꿀 수 있다.
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box) { ... }
Fruit의 자손들로 제한을 걸어두었기 때문에 이제는 FruitBox<Fruit>, FruitBox<Apple>, FruitBox<Grape> 모두 인자로 받을 수 있다.
Collections.sort()를 이용해서 정렬하는 방식을 많이 사용한다.
이 메소드의 시그니처를 보면 이렇게 정의되어 있다.

첫 번째 매개변수로 정렬할 리스트를 받고, 두 번째 매개변수로 정렬 방법이 정의된 Comparator를 받는다. 이 때, Comparator의 제네릭 타입에 하한 제한이 걸려있는 걸 볼 수 있다.
만약 와일드카드가 사용되지 않았다고 생각해보자.
static <T> void sort(List<T> list, Comparator<T> c)
이 상태에서는 Apple을 요소로 갖는 list를 정렬하려면 Comparator<Apple>을 구현할 클래스가 인자로 넘어간다.
이 때, 만약 Apple이 아닌 Grape가 담긴 list를 정렬하려면 타입만 Comparator<Grape>로 바뀐 똑같이 구현된 클래스를 새로 만들어야 한다.
이런 문제를 해결하기 위해 와일드카드를 사용할 수 있다.
static void sort(List<Apple> list, Comparator<? super Apple> c)
<? super Apple>의 의미는 Apple과 그 조상들이 올 수 있다는 의미이다.
이 때에는, Comparator<Apple>, Comparator<Fruit>, Comparator<Object> 가 올 수 있다.
즉, Comparator<Fruit> 클래스 하나만 구현해놓아도 Apple 리스트이든
Grape 리스트이든 관계 없이 저 하나로 정렬을 할 수 있다.
메소드의 선언부에 제네릭 타입이 선언된 메소드를 제네릭 메소드라고 한다.
앞서 살펴본 Collections.sort()가 바로 제네릭 메소드이다.
제네릭 타입의 선언 위치는 반환 타입의 바로 앞에 온다.
class Box<T> {
public static <T> T addStatic(T t) { ... }
public T add(T t) { ... }
add 메소드를 제네릭 메소드라고 착각할 수도 있는데, 저것은 그냥 클래스에 선언된 타입 변수로 타입을 지정한 일반 메소드이다.
addStatic 같이 반환타입 앞에 제네릭을 사용하여야 제네릭 메소드라고 부른다.
위의 코드에서 Box 클래스에 선언된 T와 addStatic 메소드에 선언된 T는 별개의 타입 매개변수이다.
즉, 제네릭 메소드의 타입 변수는 독립적으로 사용된다는 의미이다.
앞서 설명한 것처럼 static 메소드에는 타입 변수를 사용할 수 없지만,
제네릭 메소드는 메소드 내에서만 지역적으로 제네릭 타입을 사용하는 것이기 때문에 가능하다.
좀 전에 예시를 든 Collections.sort()와 이름은 같지만 매개변수가 하나인 메소드가 있다.

이 역시 제네릭 메소드이지만 제네릭 타입이 좀 복잡하다.
List<T> 타입의 리스트를 정렬한다는 것은 이해되지만
정작 T가 어떤 타입을 허용한다는 것인지 어렵다.
단계 별로 이해해보자.
정리하자면, T 타입 혹은 그 조상의 타입으로 Comparable 인터페이스를 구현한 클래스 만이 List의 요소로 올 수 있다는 의미이다.