제네릭 클래스란
제네릭은 데이터의 타입(data type)을 일반화한다(generalize)는 것을 의미한다.
제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법으로, 객체의 타입 안정성을 높이고 형변환과 타입 검사에 들어가는 노력을 줄일 수 있다.
자바 1.5 이전의 코드에선 여러 타입을 사용하는 클래스에선 인수나 반환값으로 Object 타입을 활용했다.
// 자바 1.5 이전
List strings = new ArrayList();
strings.add("문자1");
strings.add(1);
// 형변환이 필요하다
String str1 = (String) strings.get(0);
// 타입이 다르면 런타임 에러가 발생한다.
String str2 = (String) strings.get(0);
Java
복사
1.5 이전
// 자바 1.5 이후
List<String> strings = new ArrayList<>();
strings.add("문자1");
// strings.add(1); // 컴파일 에러
// 형변환이 필요없다
String str1 = strings.get(0);
Java
복사
1.5 이후
클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 제네릭 클래스, 제네릭 인터페이스(통틀어 제네릭 타입) 라고 부른다. (제네릭 메서드도 있다) List<E> 인터페이스가 대표적이다.
// 제네릭 선언. 여러 타입을 사용할 경우 쉼표로 구분
class MyClass<T1,T2,T3> {
private T1 firstField;
private T2 secondField;
private T3 thirdField;
// 반환 타입 앞에 타입을 명시하는 제네릭 메서드 선언. 여기서 선언한 T는, 클래스에서 선언한 타입과 무관하다.
private <T> void genericMethod(T parameter){
}
}
Java
복사
제네릭 타입을 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다.
로 타입은 제네릭 타입을 선언한 후, 타입 매개변수를 설정해주지 않는 경우를 의미한다. (제네릭을 지원한 java 1.5 이전 버전 코드와의 하위호환을 위해서)
예컨데 위에서 예를 든 List<E> 타입의 로 타입은 List다.
제네릭 타입은 컴파일러가 제네릭 타입의 매개변수로 특정한 타입만 들어갈 것이란 걸 감지하게 해주고, 컴파일 시 에러를 잡아준다. 제네릭이 주는 안정성과 표현력을 잃지 않으려면 로 타입 사용을 자제해야 한다.
private final Collections<Stamp> stamps = ...;
Java
복사
Stamp 이외의 타입이 stamps에 들어오면 컴파일러가 경고를 던진다.
임의 객체를 허용하고 싶다면
만약 Collection에서 아무 객체나 받는 것을 의도하고 싶다면, 로 타입이 아닌 Object 제네릭이 낫다.
// 🙅🏻♂️ not recommend
public void processList(List list){
...
}
// 🙆🏻♂️ recommend
public void processList(List<Object> list){
...
}
Java
복사
임의 객체를 받고 싶다면, Object 제네릭을 활용하라
로 타입은 제네릭을 사용하지 않겠다는 의미이고, Object 제네릭은 컴파일러에게 어떤 객체든 들어올 수 있다는 것을 알려주는 것이다. 이로 인해 다음과 같은 차이가 발생한다.
// 🙅🏻♂️ not recommend
// List<Object>, List<String> 등 모든 List타입을 넘길 수 있다.
private static void unsafeAdd(List list, Object o){
list.add(o);
}
// 🙆🏻♂️ recommend
// List<Object> 만 인자로 넘길 수 있고, List<String은 넘길 수 없다. * 제네릭의 하위타입 규칙
private static void unsafeAdd(List<Object> list, Object o){
list.add(o);
}
public static void main(String[] args){
List<String> strings = new ArrayList<>();
// 🙅🏻♂️ not recommend에선 컴파일 오류가 나지 않고, 🙆🏻♂️ recommand에선 컴파일 오류가 발생한다.
unsafeAdd(strings, Integer.valueof(42));
}
Java
복사
만약 파라미터로 어떤 타입이든 받고 싶고, 그 타입을 신경쓰고 싶지 않은 것이라면 비한정적 와일드 카드 타입 (<?>)을 사용하라.
raw 타입을 사용할 때와 달리, 비한정적 와일드 카드를 쓰면 컬렉션의 불변식이 보장된다. (? 과 null만 담을 수 있음).
static void unsafeAdd(List<?> l1, List<?> l2){
// 코드 블록에 들어온 순간 타입이 정해진다. Set<String>이 들어왔다면 s1과 s2에는 String만 담을 수 있다.
// 아래 코드는 컴파일 오류가 발생한다.
l1.add(l2.get(0));
}
static void unsafeAdd(List l1, List l2){
// 아래 코드는 성공한다. 런타임 오류 가능성을 내포한다.
l1.add(l2.get(0));
}
Java
복사
로 타입을 사용할 수 있는 소소한 예외
1.
class 리터럴을 사용할 때.
자바 명세는 class 리터럴에 제네릭 별로 변수화 하는 것을 허용하지 않았습니다. (배열과 기본 타입은 허용)
// 🙆🏻♂️ 다음 세 개는 성공
List.class
String[].class
int.class
// 🙅🏻♂️ 다음 두 개는 불가
List<String>.class
List<Integer>.class
Java
복사
이 때는 로 타입을 사용해야 합니다.
2.
instanceof 연산자를 사용할 때.
런타임에는 제네릭 타입 정보가 지워지므로, 비한정적 와일드 카드(?)를 제외하고는 instanceof 연산자를 사용할 수 없다. 또한 instanceof의 맥락에선 로 타입과 비 한정적 와일드 카드가 동일하게 동작하므로, 코드가 깔끔해진다는 측면에서 차라리 안쓰는 게 낫다.
if (o instanceof Set){
Set<?> s = Set<?> o;
...
}
Java
복사