제네릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시 타입 체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
제네릭 타입은 클래스와 메서드에 선언할 수 있는데 먼저 클래스에 선언하는 타입부터 알아본다.
class Box {
Object item;
void setItem(Object item) {
this.item = item;
}
Object getItem() {
return item;
}
}
이 Box
클래스는 다음과 같이 제네릭 클래스로 변경할 수 있다.
class Box<T> {
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
변수 T
를 타입 변수라고 한다. Type의 첫 글자에서 따온 것이며 다른 것을 사용해도 된다. 보통 상황에 맞게 의미 있는 문자를 선택하여 사용한다.
위 제네릭 클래스의 객체를 생성할 때에는 타입 T
대신 사용될 실제 타입을 지정해주어야 한다.
Box<String> b = new Box<String>();
또한 JDK1.7부터는 다음과 같이 생략도 가능하다.
Box<String> b = new Box<>();
이렇게 선언하고 나면 제네릭 클래스 Box<T>
는 다음과 같이 정의된 것과 같다.
class Box {
String item;
void setItem(String item) {
this.item = item;
}
String getItem() {
return item;
}
}
제네릭은 객체를 생성할 때 객체별로 다른 타입을 지정할 수 있다.
Box<String> b = new Box<>();
Box<Integer> b = new Box<>();
하지만 타입 변수 T
는 인스턴스 변수로 간주되기 때문에, static 멤버에는 타입 변수 T를 사용할 수 없다. static멤버는 인스턴스 변수를 참조할 수 없기 때문이다.
class Box<T> {
static T item; // 에러
static void print(T t) {...} // 에러
}
또한 제네릭 타입의 배열의 참조변수를 생성하는 건 가능하지만, 배열 인스턴스를 생성하는 것은 허용하지 않는다. 왜냐하면 new 연산자는 컴파일 시점에 타입 T
가 뭔지 정확히 알아야 하지만 Box<T>
클래스를 컴파일하는 시점에서는 T
가 어떤 타입이 될지 알 수 없기 때문이다.
class Box<T> {
T[] arr; // 허용
Box() {
arr = new T[10]; // 허용하지 않음
}
}
꼭 제네릭 배열을 생성해야할 필요가 있을 때에는 Reflection API의 newInstance()
와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object
배열을 생성한 뒤 T[]
로 형변환하는 방법을 사용한다.
제네릭에는 타입 변수를 제한할 수 있는 키워드가 존재한다. extends
와 super
를 사용한다.
extends
는 상한 제한이라고 하며 지정한 클래스를 포함한 자식 클래스들만 올 수 있게 한다.
super
는 하한 제한이라고 하며 지정한 클래스를 포함한 조상 클래스들만 올 수 있게 한다.
제네릭 클래스를 만들때에는 오직 extends
키워드만 사용 가능하며 super
는 사용 불가능하다. 이유는 여기의 글을 참조바란다.
제네릭 클래스에서는 다음과 같이 사용한다.
class Box<T extends Number> {
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
이렇게 사용하면, T
에는 Number
클래스를 포함한 자식들만 올 수 있게 된다. 즉 Number
, Integer
, Double
같은 클래스만 올 수 있다.
만약 Number
의 자식이면서, Serializable
인터페이스도 구현해야 한다면 다음과 같이 & 기호로 연결한다.
class Box<T extends Number & Serializable> {
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
와일드 카드는 메서드의 인자로 제네릭 타입의 클래스 객체를 받을 때 사용할 수 있으며 어떠한 타입도 될 수 있다는 것을 나타낸다. 제네릭 클래스를 만들 때는 사용하지 못한다.
static void print(List<Number> list) {
for(Number n : list) {
System.out.println(n);
}
}
내가 원하는 건 Number
클래스를 상속 받은 Integer
, Double
등의 리스트안의 요소를 출력하는 것이다. 하지만 위와 같이 코드를 작성한다면 print()
메서드의 인자로 오직 List<Number>
밖에 들어오지 못하게 된다. 따라서 이 때 와일드 카드를 사용한다.
static void print(List<? extends Number> list) {
for(Number n : list) {
System.out.println(n);
}
}
이렇게 작성하면 이제 원하는 행위가 가능해진다. Number
클래스를 포함한 자식 클래스들이 모두 올 수 있게 되며 List<Integer>
도 올 수 있고, List<Double>
도 올 수 있게 된다.
static void print(List<? super Number> list) {
for(Number n : list) {
System.out.println(n);
}
}
적절한 예시는 아니지만, super
를 이용하여 Number
를 포함한 조상 클래스들만 올 수 있게 할 수도 있다.
static void print(List<?> list) {
for(Number n : list) {
System.out.println(n);
}
}
static void print(List<? extends Object> list) {
for(Number n : list) {
System.out.println(n);
}
}
또한 위 두 개의 선언은 똑같은 의미다. 모든 타입이 올 수 있게 한다. 다만 조금 다른 부분이 있는데, 여기를 참조바란다.
메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 한다.
public <T> void print() {
T item;
...
}
public <T> void print(List<T> list) {
...
}
제네릭 클래스와 타입 변수가 같아도 전혀 별개의 것으로 취급하니 주의해야한다.
class Box<T> {
// 이 T와
T item;
// 이 T는 다름!!
public <T> void print(List<T> list) {
...
}
}
또한 아까 static에서는 타입 변수가 선언이 불가능했지만, 메서드에서의 타입 선언은 지역 변수를 선언한 것과 같기 때문에 static 메서드에서도 선언이 가능하다. 즉 메서드가 static이건 말건 상관하지 않는다.
// 둘 다 가능
public <T> void print1(List<T> list) {
...
}
public static <T> void print2(List<T> list) {
...
}
앞서 나왔던 와일드 카드를 이용한 메서드를 제네릭 메서드로 바꾸면 다음과 같이 바꿀 수 있다.
static void print(List<? extends Number> list) {
for(Number n : list) {
System.out.println(n);
}
}
static <T extends Number> void print(List<T> list) {
for(Number n : list) {
System.out.println(n);
}
}
얼핏 보면 같은 메서드지만, 전자는 타입을 사용할 수 없고 후자는 타입 T
를 이용해 참조 변수를 생성하는 등의 행위가 가능하다. 그래서 다음과 같이 사용한다.
와일드 카드는 타입에는 관심이 없고, 행위 자체에만 관심 있을 때 사용한다.
제네릭 메서드는 타입에 관심이 있고 타입을 이용해 무언가 행위를 해야할 때 사용한다.
제네릭 메서드에서는 super
키워드도 물론 사용 가능하다.
static <T super Number> void print(List<T> list) {
for(Number n : list) {
System.out.println(n);
}
}