제네릭에 대해 공부하며 헷갈렸던 부분들에 대해 정리해놓으려고 한다.
해당 포스팅은 이 참조 블로그를 많이 참조했음을 알린다 :)
List<String>
은 List<Object>
의 하위타입이 아니다.당연하지 않은가? 싶다가도 헷갈리기 쉬운 개념이다.
List<String>
은 List
의 하위 타입이지만, List<Object>
의 하위 타입은 아니다!
이 개념은 참조 블로그의 예시를 보면 더 명확히 이해할 수 있다.
class Box<T> {
protected T obj;
public void setObj(T obj) {
this.obj = obj;
}
public T getObj() {
return obj;
}
}
class SteelBox<T> extends Box<T> {
public SteelBox(T obj) {
this.obj = obj;
}
}
Box<Integer> IBox = new SteelBox<>(7959);
Box<String> SBox = new SteelBox<>("simple");
해당 예시에서도 살펴볼 수 있듯 "제네릭 클래스 Steel Box"는 "제네릭 클래스 Box"를 상속받고 있다.
이 경우에 Integer를 매개타입 인자로 받는 SteelBox 제네킬 클래스는 Integer를 매개타입 인자로 받는 Box 제네릭 클래스를 상속 받을 수 있고, String을 매개타입 인자로 받는 SteelBox 제네릭 클래스는 String을 인자로 받는 Box 제네릭 클래스를 상속 받을 수 있다.
Box<String>
과 Box<Integer>
는 서로 다른 타입이기 때문이다!
이러한 논리로 인해
List<String>
은 List의 하위 클래스이지만, List<Object>
의 하위 클래스는 아님을 이해하게 된다.
따라서 아래의 예제도 성립하게 된다.
public void static method1(Box<Object> box) {
...
}
이러한 메소드가 있다고 생각해보자.
그렇다면 해당 메소드에 method1(new Box<Integer>())
을 집어넣으면 올바르게 동작할까 ?
전혀 그렇지 않다.
왜냐하면 Box<Integer>
는 Box<Object>
의 하위 타입이 아니기 때문이다!!
이러한 부분에서 헷갈릴 수 있기 때문에 제네릭 클래스 간의 상속 개념을 잘 이해해 두는 것이 좋다.
다음 예제를 살펴보자
public class MyClass1<T> {
public static void method1(T t) {
..
}
}
이는 제네릭 클래스 내부에 선언된 메소드이고
public class MyClass2 {
pbulic static void <T> method2(T t) {
...
}
}
이는 일반 클래스 내부에 선언된 메소드이다.
이 둘 중 첫 번째, 즉 제네릭 클래스 내부에 선언된 메소드에서 컴파일 에러가 발생한다.
왜냐하면 MyClass1
의 제네릭 타입 T
는 인스턴스가 생성될 때 결정이 되는데, static 메소드는 인스턴스 생성과 별도로 메모리에 올라와 있다.
그렇다면 MyClass1
이 생성되는 시점에 결정되는 제네릭 타입을 메서드가 매개변수로 받아낼 방법이 없다.
하지만 MyClass2
의 메소드는, 호출되는 시점에 타입이 결정되기 때문에 컴파일 에러가 발생하지 않는다.
List<T>
와 List<?>
는 같은 개념이 아니다<?>
는 타입 매개변수가 무엇이든 난 모른다! 알아서 해라!는 뜻으로 이해하면 좋다 (일단 나는 그렇게 이해했다💦)
List<?>
는
-> 원소 꺼내서! 해당 원소에 정의 된 기능만 사용할게
-> List 인터페이스에 정의 된 기능만 사용할게. 근데 나는 타입 매개변수는 몰라서 타입 매개변수랑 관련 있는 작업은 못해~
를 의미한다. 타입 매개변수를 모르기 때문에! 애초에 타입 매개변수 관련 작업을 진행하지 않아서 혹시 모를 타입 무결성 이슈를 예방하고자 관련 작업을 막아놓은 것이다.
List<T>
도 비슷한 의미를 가지지만 한 가지 다른 부분은 나는 타입 매개변수를 곧 알 것이기 때문에 관련된 작업도 가능해라는 것이다.
그렇다면 타입 매개변수랑 관련있는 작업은List
의 add
등이 있다.
상한 제한과 하한 제한이 실무에서 어떻게 사용하는지에 대해 ! 마찬가지로 참조 블로그에서 좋은 예시를 제공해준다. 내 의견을 덧붙힌 설명을 작성하고 싶지만 해당 블로그에 너무 잘 정리가 되어있어서 링크를 걸어두는 것이 최선일 것 같다😂 당장 나도 까먹지 않게 자주 봐야지
결론은 Box<? extends Toy> box
에서는 box
에 넣는 것이 불가능해지고
Box<? super Toy> box
에서는 box
에서 꺼내는 것이 불가능해진다.
아래와 같은 코드가 있다고 해보자.
public static void outBox(Box<? extends Toy> box) {
...
}
public static void ouBox(Box<? extends Robot> box) {
...
}
일단 매개변수 타입은 분명히 다르다. 그래서 오버로딩이 될 것 같긴한데..😶
일단 오버로딩의 조건을 살펴보자.
그렇다면 타입이 같아서 가능할 것 같아보이는데!
결론은 불가능하다
이를 알기 위해선 제네릭의 등장에 대해 알아야한다.
제네릭은 등장 이전 코드와 잘 융합되기 위해 타입 소거라는 개념을 활용한다.
제네릭이 등장한 자바 1.5 버전 이하에서는 <>
라는 다이아몬드 연산자가 존재하지 않는다. 따라서 이를 호환성있게 활용하기 위해서 자바 컴파일러는 다이아몬드 연산자를 지워버리는 선택을 한다.
그래서 결국 컴파일 된 코드는 아래와 같게 된다.
public static void outBox(Box box) {
...
}
public static void outBox(Box box) {
...
}
이리봐도 저리봐도 같은 코드다.
결국 오버로딩이 될 수 없는 것이다.
하지만 당연하게도 방법이 없는 것은 아니다.
와일드 카드와 제네릭을 혼합하면 가능해지기 때문이다!
오버로딩을 구현하고 싶은 이유는 outBox
매개변수에 Robot
도 넣고싶고 Toy
도 넣고 싶어서 일 것이다.
그러면! 타입을 제네릭으로 바꿔주면 된다.
public static <T> void outBox(Box<? extends T> box {
T toy = box.get();
System.out.println(toy.toString());
}
이렇게 되면! set()은 못하기 때문에 논리적인 오류도 막을 수 있고 그 안에 다양한 매개변수 타입을 집어넣을 수 있게 된다.
추가적으로 참조 블로그를 활용하여 복잡한 제네릭 메소드를 해석하는 과정을 따라가보자.
아래의 내용은 공부를 위해 작성하는 글이니, 자세한 내용은 참조 블로그를 확인하는 것이 더 좋을 것 같다!