generic은 1) 타입 제한
2) 타입 보류
라는 크게 두가지 목적으로 사용된다.
타입 제한을 위해 제네릭이 사용되고 있는 예시는 리스트이다. 제네릭이 도입되기 이전까지 자바에서는 리스트에 담기는 object들의 타입을 명시적으로 지정해주지 않았고, 이로 인해 컴파일 시점에 타입으로 인한 오류를 잡기 힘들어졌다.
List strings = new LinkedList();
strings.add(300);
Integer.parseInt(strings.get(0)); // runtime error
가장 좋은 오류는 컴파일 오류라고 하지 않나, 그래서 위와 같이 런타임 오류가 발생하기 이전에 컴파일 오류가 발생할 수 있도록 제네릭이 도입되었다.
위의 예시를 제외하고는 대부분 제네릭은 타입 보류를 목적으로 사용되는 것 같다. (제네릭 클래스, 제네릭 메서드 등...)
그러나 제네릭의 사용이 제한되는 경우
가 있는데 이를 먼저 알아보자.
일단 new 연산자를 사용하는 경우엔 안된다. 즉 제네릭 클래스가 아닌 클래스의 멤버 변수는 제네릭 타입일 수 없다. 또한 제네릭을 사용하는 ArrayList의 내부 구현을 살펴보면
this.elementData = new Object[initialCapacity];
와 같이 처음 array의 생성은 제네릭 타입을 이용해서 new T[];와 같이 하는게 아니라, Object로 한다는 것을 알 수 있다. 그 이유는 new 연산자의 내부 동작 때문이다.
new 연산자는 heap 영역에 충분한 공간이 있는지 확인한 후 메모리를 확보하는 역할을 한다. 그리고 이때 충분한 공간이 있는지 확인하려면 타입을 알아야한다. 그런데 new 연산과 제네릭을 함께 쓰면 컴파일 시점에 타입이 무엇인지 알 수 없기 때문에 new 연산을 하면서 타입을 밀어넣어주는 제네릭 클래스의 경우가 아니라면 new 연산과 제네릭은 함께 쓸 수 없다.
(위에서 언급한 ArrayList도 generic 클래스이다.)
public class ArrayList<E> { // implements 등은 생략
...
}
위와 같이 작성하면 이 클래스 내에서 모든 제네릭 타입 E는 ArrayList 클래스 내부에서 전역 변수처럼 사용된다. (이와 반대로 지역 변수처럼 사용되는 제네릭 타입도 있는데 이는 다음 절에서 소개한다.)
public class GenericExample<M, I> {
...
}
위와 같이 제네릭 타입을 복수 개 사용하는 것도 가능하다.
지금까지 내용을 정리하면, 제네릭 클래스가 아닌 경우 변수는 제네릭 타입을 가질 수 없고(인스턴스화할 때 해당 변수의 타입이 결정되지 않기 떄문에)
제네릭 클래스의 제네릭 타입은 해당 클래스 내에서 전역 변수와 같이 사용된다.
그럼 제네릭 메서드는 뭘까?
말 그대로 메서드 레벨에서 사용되는 제네릭이다. 이때 제네릭 타입은 해당 메서드 내에서 지역 변수와 같이 사용된다.
public class CoffeeMachine {
public <T> Coffee makeCoffee(T capsule) {
return new Coffee(capsule);
}
}
메서드는 제네릭 클래스 내부의 메서드이든 아니든 제네릭 메서드가 될 수 있다.
이때 제네릭 메서드 사용은 다음과 같이 한다.
CoffeeMachine coffeeMachine = new CoffeeMachine();
Colombian capsule = new Colombian();
coffeeMachine.<Colombian>makeCoffee(capsule);
coffeeMachine.makeCoffee(capsule); // 타입 추정 가능하므로 생략 가능
제네릭에 들어갈 타입을 제한해주고 싶다면 extends, super 키워드를 사용해준다.
extends
: 해당 타입의 자손 타입만 올 수 있도록 하므로 상한(upper bound)를 제한
super
: 하한(lower bound)를 제한. 즉 T super BoxMaterial은 BoxMaterial의 조상 타입만 올 수 있도록 제한한다
자바의 type eraser란, 자바에서 제네릭은 컴파일 시점에 체크하는데 사용되고, 실제 실행시에는 타입이 삭제된 코드가 실행된다는 의미이다. (오잉.....?!_)
따라서 코드 상으로 제네릭 타입 제한을 벗어나는 코드를 작성해주었다면 이를 컴파일 타임에 잡아낼 수 있지만, 코드 상으로는 어떤 타입이 담기는지 알 수 없는 경우(예를 들어 캐시에서 값을 꺼내온다거나 하는 경우...)에는 컴파일 에러가 발생하지 않고, 결국 이는 런타임에 타입 캐스팅 에러로 이어지게 된다. (예시)