제네릭(<T>)
제네릭(Generic)이란 결정되지 않은 타입을 파라미터로 처리하고, 실제 사용할 때 파라미터를 구체적인 타입으로 대체시키는 기능이다. 제네릭을 사용할 시, 매 객체를 선언할 때마다 클래스 수정 없이 타입 변경이 가능하다.
<T>로 표현되기도 하는 타입 파라미터는 T가 아닌 어떠한 알파벳을 사용해도 좋다. 그러나, 클래스 및 인터페이스 외에는 사용할 수 없다. 즉, 기본 타입인 int는 타입 파라미터의 대체 타입이 될 수 없다.
제네릭 타입은 결정되지 않은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다. 제네릭 타입은 선언부에 <> 기호가 붙고, 그 사이 타입 파라미터들이 위치한다. 외부에서 제네릭 타입을 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 한다. 만약 지정하지 않으면 Object 타입이 암묵적으로 사용된다.
예시 코드
class Box<T> {
// T는 타입 매개변수 (어떤 타입이든 들어갈 수 있음)
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
// T가 String이 됨
stringBox.setItem("Hello");
System.out.println(stringBox.getItem());
// 출력: Hello
Box<Integer> intBox = new Box<>();
// T가 Integer가 됨
intBox.setItem(100);
System.out.println(intBox.getItem());
// 출력: 100
}
}
Box<T> 클래스는 어떤 타입이든 저장이 가능하며, T가 실행 시점에 결정된다.
제한된 타입 파라미터(<T extends Number>)
경우에 따라서는 타입 파라미터를 대체하는 구체적인 타입을 제한할 필요가 있다. 예를 들어 숫자를 연산하는 제네릭 메소드는 대체 타입으로 Number 또는 자식 클래스 (Byte, Short, Integer, Long, Double)로 제한할 필요가 있다.
이처럼 모든 타입으로 대체할 수 없고, 특정 타입과 자식 또는 구현 관계에 있는 타입만 대체할 수 있는 타입 파라미터를 '제한된 타입 파라미터(Bounded Type Parameter)'라고 한다.
상위 타입은 클래스뿐만 아니라 인터페이스도 가능하다. 인터페이스라고 해서 implements를 사용하지는 않는다. 타입 파라미터가 특정 타입으로 제한되면, Object의 메서드뿐만 아니라 해당 특정타입이 가지고 있는 메서드도 사용할 수 있다. 예를 들면, Number로 제한된 타입 파라미터는 Number의 메서드 또한 사용할 수 있다.
예시 코드
class NumberBox<T extends Number> {
// Number와 그 하위 타입만 허용
private T number;
public void setNumber(T number) {
this.number = number;
}
public double getDoubleValue() {
return number.doubleValue();
// Number의 메서드 사용 가능
}
}
public class Main {
public static void main(String[] args) {
NumberBox<Integer> intBox = new NumberBox<>();
intBox.setNumber(10);
System.out.println(intBox.getDoubleValue());
// 출력: 10.0
NumberBox<Double> doubleBox = new NumberBox<>();
doubleBox.setNumber(5.5);
System.out.println(doubleBox.getDoubleValue());
// 출력: 5.5
// NumberBox<String> stringBox = new NumberBox<>();
// 컴파일 오류! (String은 Number의 하위 타입이 아님)
}
}
어떤 타입이든 허용하는 것이 아니라, 특정 타입과 그 하위 타입만 사용하도록 제한이 가능하다.
와일드카드 타입 파라미터(<? extends T> 또는 <? super T>)
제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 타입 파라미터로 ?(와일드카드)를 사용할 수 있다. ?는 범위에 있는 모든 타입으로 대체할 수 있다는 표시이다. 상속 관계가 있는 클래스들에서, 부모클래스를 와일드카드로 상속(extends)하면 그 자식클래스 중 어떤 타입이든 사용이 가능하며 해당 부모클래스의 자식클래스 외에는 사용이 불가능하다. 또는, 역으로 super 선언으로 자식클래스 지정 시 자식클래스와 부모클래스만 타입으로 사용이 가능하다.
예시 코드
import java.util.Arrays;
import java.util.List;
public class Main {
// ? extends Number → Number 또는 그 하위 타입만 허용 (읽기 전용)
public static double sumOfNumbers(List<? extends Number> list) {
double sum = 0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
System.out.println(sumOfNumbers(intList));
// 출력: 6.0
System.out.println(sumOfNumbers(doubleList));
// 출력: 6.6
}
}
? extends Number의 경우, Number의 하위 타입만 가능하다.
List<? extends Number>는 List<Integer>, List<Double>을 받을 수 있다.
그러나 읽기 전용이므로, list.add(100) 등의 코드를 통한 추가 컴파일은 불가능하며 오직 출력만이 가능하다.
Number의 하위 타입이면 어떤 것이든 올 수 있기 때문에 정확한 타입이 정해지지 않아서 추가가 불가능한 것이다.
즉, Java는 타입 안정성을 보장하기 위해 add()를 제한한다.
public static void addNumbers(List<? super Integer> list) {
list.add(10);
list.add(20);
// list.add(3.14);
// 오류! (Integer보다 상위 타입이므로 Double은 추가 불가)
}
한편 ? super Integer의 경우, Integer을 포함한 그 상위 타입(Integer, Number, Object)만 가능하다.
값을 추가할 수 있지만, 특정 타입으로 읽기는 제한적이다.
super를 사용하는 와일드카드 타입 파라미터의 경우, extends를 사용할 때와 달리 Integer를 추가할 수 있다.
Integer와 Integer의 부모 타입만이 올 수 있기 때문이다.
비고
1. ? extends는 값을 읽을 수만 있고, ? super는 값을 추가할 수 있다.
레퍼런스
1. 이것이 자바다 개정판(신용권&임경균 저, 한빛미디어)