
우리는 다형성을 통해 부모 클래스 타입이 여러 자식 클래스를 참조할 수 있다는 것을 알고 있다.
부모 클래스 타입의 변수가 자식 클래스를 참조해도
자식 클래스가 오버라이딩한 메서드가 우선 순위를 갖기 때문에 다형성을 활용할 수 있었지만,
부모 클래스에는 여러 자식 클래스를 담을 수 있기 때문에 타입 안정성 부분에서 문제점이 있다.
class Smartphone {
public void sendMessage() {
// 메서드 바디
}
}
class SKTSmartphone extends Smartphone {
@Override
public void sendMessage() {
// 메서드 바디
}
}
class KTSmartphone extends Smartphone {
@Override
public void sendMessage() {
// 메서드 바디
}
}
class Service {
public void messageService(Smartphone smartphone) {
smartphone.sendMessage();
}
}
// ** kt 관련 메서드이므로, KTSmartphone 객체를 다루어야 한다. **
public void ktSmartphoneMethod() {
Service service = new Service();
service.messageService(new SKTSmartphone()); // ** 하지만 SKTSmartphone으로 잘못 입력 **
}
ktSmartphoneMethod() 메서드를 수행하기 위해
Service 객체를 생성해 관련 메서드를 사용하는 과정에서,
messageService() 메서드에는 Smartphone 클래스의 자식 객체를 입력할 수 있기 때문에
KTSmartphone 외에 다른 객체가 들어오지 않을 것이란 보장이 없다.
추가로 타입 안정성이 무시된 경우 데이터를 다룰 때에는 다운 캐스팅이 필요한 경우도 있는데,
다운 캐스팅은 위험하므로 사용에 주의가 필요하다.
이러한 문제들을 해결할 수 있는 방법은 없을까?
제네릭은 범용적이라는 뜻으로 타입을 미리 결정하지 않고,
메서드가 매개변수에 인자를 전달해서 사용하는 것처럼,
타입 매개변수에 타입 인자를 전달해서
인스턴스의 생성 시점에 타입을 결정하는 것이다.

class Clazz<T> {
// 코드
}
class Clazz<K, V> {
// 코드
}
제네릭은 클래스 옆에 다이아몬드(<>)를 사용해서 선언할 수 있다.
class Clazz<T> { // ** <>(다이아 몬드)를 사용해서 선언, T는 타입 매개변수 **
T value; // ** T 타입이 필요한 곳에 선언 **
public void genericMethod(T value) {
this.value += value;
}
}
class Clazz<K, V> { // ** 타입 매개변수는 2개 이상 선언할 수 있다. **
// 코드
}
메서드의 매개 변수에 매개 변수를 정의하는 것처럼,
다이아몬드 내부에 타입 매개변수를 정의한다.
타입 매개변수는 2개 이상 선언 가능하다.
타입 매개변수는 대문자를 사용하고 용도에 맞는 단어의 첫 글자를 사용하는 것이 관례이다.
E : Element
K : Key
N : Number
T : Type
V : Value
...
정의한 타입 매개변수를 클래스 내부에 필요한 곳에 사용할 수 있다.
메서드와 동작 원리가 유사하다고 생각하면 된다!
이렇게 선언된 클래스는 제네릭 클래스라고 한다.

// ** 객체를 생성할 때 전달한 타입 인자로 자동으로 추론 가능 **
// ** new Clazz<Integer>() 생략 **
Clazz<Integer> clazz = new Clazz<>(); // ** 타입 매개변수에 전달한 Integer는 타입 인자 **
Clazz<int> clazz = new Clazz<>(); // ** 기본형 불가, 래퍼 클래스 사용 **
사용자가 제네릭 객체를 생성하면
제네릭 클래스는 T 타입으로 선언한 것들은 사용자가 타입 매개변수에 전달한 타입 인자로 변환된다.
타입 인자에 기본형은 사용할 수 없고 래퍼 클래스를 사용해야 한다.
class Clazz<Integer> { // ** T 타입 매개변수가 사용자가 전달한 타입 인자로 변환 **
Integer value; // ** 타입 변환 **
public void genericMethod(Integer value) { // ** 타입 변환 **
this.value += value;
}
}
제네릭을 사용하여 타입 인자로 변환된 필드나 메서드는
다운 캐스팅 없이 안전하게 사용할 수 있으며,
잘못 다룰 경우 컴파일 오류가 발생하기 때문에 실수를 미연에 방지할 수 있다!

class Smartphone {
public void sendMessage() {
// 메서드 바디
}
}
class SKTSmartphone extends Smartphone {
@Override
public void sendMessage() {
// 메서드 바디
}
}
class KTSmartphone extends Smartphone {
@Override
public void sendMessage() {
// 메서드 바디
}
}
// ** 제네릭 클래스 **
class Service<T> {
public void messageService(T smartphone) {
smartphone.sendMessage();
}
}
// ** kt 관련 메서드이므로, KTSmartphone 객체를 다루어야 한다. **
public void ktSmartphoneMethod() {
Service<KTSmartphone> service = new Service<>();
service.messageService(new SKTSmartphone()); // ** 컴파일 오류 발생 **
}
타입 안전성을 확보하고자 코드를 도입했다.
이제 ktSmartphoneMethod() 메서드에는
KTSmartphone 관련 객체를 명시적으로 사용하지 않으면 컴파일 오류가 발생한다.

// ** 제네릭 클래스 **
class Service<T> {
public void messageService(T smartphone) {
smartphone.sendMessage(); // ** 컴파일 오류 발생 **
}
}
// ** kt 관련 메서드이므로, KTSmartphone 객체를 다루어야 한다. **
public void ktSmartphoneMethod() {
Service<KTSmartphone> service = new Service<>();
service.messageService(new KTSmartphone());
}
하지만 수정한 코드 역시 실행되지 않는다.
왜일까?
우리는 제네릭 클래스가 인스턴스 생성 시점에 타입 인자로 변환된다는 것에 주목해야 한다.
타입 매개변수로 T를 가지는 smartphone은
컴파일 이전에는 Object를 타입 인자로 전달받을지,
Smartphone을 타입 인자로 전달받을지 알 수 없다.
타입 안전성을 확보했더니, 타입 매개변수에 대한 안전성의 문제가 생겨버렸다.
.. 그렇다면 제네릭 클래스는 모든 객체의 상위 부모인
Object 클래스 관련 메서드만 사용 가능하다는 것인가?

제네릭 클래스를 선언하면서, 타입 매개변수를 제한하여
변환될 타입 인자를 한정지어 타입 매개변수의 메서드를 사용하도록 할 수 있다.
class Service<T extends Smartphone> {
public void messageService(T smartphone) {
smartphone.sendMessage(); // ** 실행 가능 **
}
}
타입 매개변수를 특정 클래스로 제한하면서,
타입 매개변수에 대한 안전성을 확보하고
Object에서 제공하는 메서드가 아닌 제한한 클래스의 메서드도 사용할 수 있게 됐다.

제네릭은 클래스가 아닌 메서드에도 사용이 가능하다.
class Clazz {
// 제네릭 메서드 선언
public <T extends Parent> void clazzMethod(T t) {
// 메서드 바디
};
}
// 제네릭 메서드 사용
// ** parameter 타입을 통해서 타입 인자 자동 추론 가능 **
// ** Clazz.<Parameter>genericMethod 생략 **
Clazz.genericMethod(parameter);
제네릭 메서드도 제네릭 클래스와 마찬가지로
다이아몬드(<>)를 사용하고, 타입 매개변수를 입력하여 반환 타입 왼쪽에 명시한다.
타입 매개변수 역시 제한할 수 있다.
static <V> V staticMethod2(V t) {} // static 제네릭 메서드, 가능
static T staticMethod1(T t) {} // static 메서드, 불가
static 제네릭 메서드는 메서드 호출 시점에 타입 인자로 변환한다.
메서드 영역에서 관리되어 정상 동작이 가능하다.
static 메서드도 마찬가지로 메서드 영역에서 관리되지만,
static 메서드에서 사용하는 타입 매개변수 T는 인스턴스와 함께 힙 영역에서 관리된다.
생명 주기에 차이가 발생하여 static 메서드는 타입 매개변수를 사용할 수 없다.
static 메서드에 타입 매개변수를 사용하기 위해서는 제네릭 타입으로 선언해야 한다.

제네릭은 컴파일 이후에는 타입 매개변수라는 개념이 존재하지 않는다.
컴파일 이후에는 타입 매개변수의 상한으로 타입을 변환해버린다.
class Clazz<T> {
T value;
public Clazz(T value) {
this.value = value;
}
public T getGeneric() {
return value;
}
}
============================== 컴파일 이후 ================================
class Clazz { // ** 제네릭 정보 삭제 **
Object value; // ** 타입 매개변수 상한으로 변환 **
public void genericMethod(Object value) { ** 타입 매개변수 상한으로 변환 **
this.value += value;
}
}
반환 타입의 경우 컴파일러가 자체적으로 다운 캐스팅하는 코드를 생성해버린다.
Clazz<Integer> clazz = new Clazz<>(10);
Integer value = (Integer) clazz.getGeneric(); // ** 컴파일러 자체 다운캐스팅 **
컴파일 이후에 매개변수의 상한으로 클래스가 만들어져 타입 매개변수의 개념이 없어지기 때문에,
런타임에서 타입 매개변수를 활용하는 코드는 사용할 수 없다.
public T returnGeneric() {
return new T();
}
public void checkGeneric(Object target) {
return target instanceof T;
new로 인스턴스를 생성하고 참조 객체를 확인하는 instanceof 키워드 모두
T는 전부 타입 매개변수의 상한으로 타입 이레이저가 수행되기 때문에,
개발자의 의도와 다른 결과값이 출력될 수 있다.
런타임에 타입 매개변수를 활용하는 코드는 불가능하다!

class Clazz<T> { // ** <>(다이아 몬드)를 사용해서 선언, T는 타입 매개변수 **
T value; // ** T 타입이 필요한 곳에 선언 **
public void genericMethod(T value) {
this.value += value;
}
}
제네릭은 인스턴스 생성 시에 전달한 타입 인자로 변환되는데,
컴파일 과정에서 매우 복잡하게 동작한다.
class Clazz<Integer> { // ** T 타입 매개변수가 사용자가 전달한 타입 인자로 변환 **
Integer value; // ** 타입 변환 **
public void genericMethod(Integer value) { // ** 타입 변환 **
this.value += value;
}
}
복잡하게 작용한다는건, 그만큼 더 일을 한다는 소리이다.
그냥 간단하게 사용하는 방법은 없을까?
그래서 등장한게 바로 와일드 카드이다.

// 제네릭 메서드
public <T> void genericMethod(Clazz<T> clazz) {
// 메서드 바디
}
// ** 와일드카드 **
public void wildcardMethod(Clazz<?> clazz) {
// 메서드 바디
}
와일드카드는 '?'를 사용하여 선언할 수 있다.
와일드카드는 보통 반환 타입이나 매개변수에서 사용되는데,
이미 만들어진 제네릭 타입을 활용할 때 사용할 수 있다.
public void wildcardMethod(Clazz<? extends Parent> clazz) // ** 상한 **
public void wildcardMethod(Clazz<? super Child> clazz) // ** 하한 **
와일드 카드도 제네릭 메서드와 같이 매개 변수를 제한할 수 있다.
다른 점은 와일드카드는 상한과 달리 하한도 적용할 수 있다는 점이다.
각각 extends, super 키워드를 사용한다.
wirdcardMethod(Clazz<Integer> clazz) // ** 메서드 호출 **
와일드카드는 컴파일 과정에서 별도로 타입의 변환이 일어나지 않는다.
그냥 제네릭 자체를 활용할 뿐이다.
..뭐야, 그러면 제네릭 메서드는 왜 존재하는건데?
더 가벼워보이는 와일드카드가 무조건 좋은거 아니야?

public <T extends Shoes> T createGeneric(Clothes<T> shoes) {
T result = shoes.get();
return result; // ** 타입 인자로 변환 **
}
public Shoes createWildcard(Clothes<? extends Shoes> shoes) {
Shoes result = shoes.get();
return result; // ** 타입 인자로 변환되지 않아 최상위 클래스(Shoes) 반환 **
}
Shoes<Nike> shoes = new Shoes<>();
Nike shoes = createGeneric(nikeShoes); // ** Nike 반환 가능 **
Shoes shoes = createWildcard(nikeShoes); // ** Nike 반환 불가 **
제네릭 메서드는 컴파일 과정에서 타입 인자로 변환되기 때문에,
반환 타입 역시 전달한 타입 인자로 받을 수 있다.
하지만 와일드카드는 컴파일 과정에서 변환이 일어나지 않기 때문에,
매개변수의 상한(최상위 부모 클래스)으로 반환 받을 수 있다.
특정한 타입이 필요한 경우 제네릭 메서드를 사용하고,
그렇지 않은 경우에는 와일드카드를 사용하면 되겠다.

제네릭은 이해는 되지만 계속 보아도
직접 사용하려고만 하면 손이 잘 나가질 않는다(당황스러움).
특히 제네릭 메서드에서 와일드카드가 등장하면서부터는 이해하는데 하루 종일 걸렸던 것 같다.
계속되는 코딩과 다양한 사례를 찾아보면서
제네릭을 능숙하게 다룰 줄 안다면 아주 큰 도움이 될 것 같다.
제네릭은 메서드 설계와 유사하다!고 최면을 걸어보면 어떻게든 되지 않을까?
메서드 핵심 중 하나가 매개변수인 만큼,
제네릭의 핵심은 타입 매개변수이고 그것에 배경에는 타입 안정성이 있다는 것을 잊지 말자.