Generics
1. 제네릭이 헷갈려!
제네릭은 너무나 당연하고 너무나 자연스러운 공기같은 존재이다.
쓸 때는 거리낌이 없지만, 제네릭을 이용한 클래스나 메서드를 작성할 일이 생기게 되면 그 때부터 혼돈의 카오스가 시작된다. 단순히 오버로딩을 줄이고자 의도한 것 뿐인데 작성할때도 이판사판이였지만 사용할때는 이판사판 공사판이 될 수 있다. 제네릭을 다시 정리하면서 어떤 포인트에서 혼동이 발생하는지를 짚어내보았다.
내가 헷갈리고 있었던 부분은 1. 제네릭 클래스, 제네릭 메서드의 선언, 2. 클래스 자체 타입의 캐스팅과 구체화된 타입 파라미터의 타입캐스팅에서 오는 혼란, 3. 구체화된 타입 파라미터를 가진 클래스를 타입으로 사용할때와 클래스 내부에서 구체화된 타입 파라미터를 받아들일때의 차이, 4.변성 그리고 PECS 였다. 그리고 여기에 1, 2, 3를 합치게 되면 4. 사용 지점 변성과 선언 지점 변성 또한 헷갈렸다고 보여진다. 이것들을 정리해보고자 한다.
2. 제네릭 클래스, 제네릭 메서드
제네릭 클래스와 제네릭 메서드의 선언은 위와 같다. 여기서 눈여겨봐야 할 것은
-
제네릭 클래스의 타입파라미터는 클래스명 뒤에 위치한다.
-
제네릭 메서드의 타입파라미터는 리턴타입 앞에 위치한다.
-
제네릭 타입을 사용한 메서드와 제네릭 메서드를 구분하자!
이다. -
[!] 여기서 잠깐!
제네릭(타입파라미터)은 "어떤 타입이 될지는 모르겠지만, 어떤 타입을 클래스 내에서(제네릭 클래스) 혹은 메서드 내에서(제네릭 메서드) 사용할게요." 라는 뜻이다. 일례로,T texture;
와 같은 맴버 변수가 존재한다는 것이다.)
제네릭 타입을 사용한 메서드는 제네릭 클래스 안에 정의된 함수 중에서 클래스의 타입파라미터 타입을 매개변수나 반환값으로 혹은 로컬변수로 사용하는 메서드를 말한다. 반면 제네릭 메서드는 새로운 타입파라미터를 메서드 선언부에서 선언해줌으로써 클래스와는 독립적으로 자신만의 타입파라미터를 갖는 메서드를 의미한다.
자바 제네릭(Generics) 개념 & 문법 정복하기, 인파 인파님의 포스팅 중 이 포스트의 '제네릭 객체 만들어보기' 부분을 참고해서 이해했다.
3. 클래스 자체의 타입캐스팅, 타입파라미터의 구체화된 타입들을 가지는 클래스의 타입캐스팅
왼쪽은 맞고 오른쪽은 틀리다.
왼쪽과 오른쪽의 차이라면 왼쪽은 타입파라미터의 구체화된 타입이 같으면서 클래스 타입이 다르고(+ 상속관계에 있는 클래스들), 오른쪽은 클래스의 타입이 같으나 타입파라미터의 구체화된 타입이 다르다는 것이다(+ 타입파라미터들의 구체화된 타입들은 상속관계에 있다.).
3-1. 구체화된 타입을 가지게 된 클래스 (오른쪽 그림)
내가 제네릭 타입의 변수들을 보면서 어렴풋이 느꼈던 느낌은 아래와 같다.
'Number
와 Integer
는 상속관계에 있지! List<Number>
와 List<Integer>
도 그렇겠지!'
혹은
'<Number>
던지 <Integer>
던지 List니까 뭐 어떻게 관계가 있겠지!'
혹은
'이 타입들 모두 어떤 타입을 상속 받았겠지! 그러니까 뭐 어떻게 관계가 있겠지!'
였다.
하지만 제네릭 클래스 타입을 이렇게 느끼게되면 안된다. 그것보단 아래의 느낌으로 느끼는게 맞다.
우리가 Object를 상속받은 Integer 클래스나, LocalDateTime 클래스, swing의 JFrame 클래스를 서로 아예 상관이 없는 클래스들로 생각하듯이 List<Number>, List<Double>, List<Integer>
를 그렇게 생각하는게 맞다는 것이다. (이것이 추후에 변성을 제안하게 되는 이유가된다.)
3-2. 클래스 자체의 타입캐스팅
다시 3.의 첫번째 그림으로 돌아가서 왼쪽 그림은 정상적이다. 이것은 ArrayList의 구현을 보면 된다.
openjdk - ArrayList.java, github
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
<E>
의 타입이 같기 때문에 이는 상속관계가 형성된다.
3-3. 그래서 첫번째 사진을 바꿔보면
위와 같은 느낌으로 받아들여도 무방하다.
4. 구체화된 타입 파라미터를 가진 클래스를 타입으로 사용할때와 클래스 내부에서 구체화된 타입 파라미터를 받아들일때의 차이
말도 살짝 어렵다. 제목을 설명하자면,
'구체화된 타입 파라미터를 가진 클래스를 타입으로 사용한다'의 의미는 List<String> list = new ...
의 List<String>
처럼 String으로 구체회된 타입 파라미터를 가지는 List 클래스가 변수의 타입으로 지정되어서 사용되는 경우를 말하는 것이다.
그리고 '클래스 내부에서 구체화된 타입 파라미터를 받아들인다'의 의미는 list.add(new Char('a'))
처럼 구체화된 타입 파라미터를 타입으로 사용하는 내부 변수에 값을 할당하는 경우를 의미한다.
이것을 구분하는 이유는 아래의 코드를 이해할 수 있기 때문이다.
'클래스 타입'의 차원으로 보면 Number
와 Integer
의 사이는 무엇도 아니다. ArrayList<Number>
와 ArrayList<Integer>
는 서로 상관없는 클래스 타입인 것이다.
하지만 구체화된 타입파라미터 즉, '내부 변수 타입'의 차원으로 봤을때 Number는 여타의 자바 클래스들처럼 상속관계에 따라 캐스팅이 가능해진다.
이것을 이해하면 변성과 PECS를 자연스럽게 이해하게 된다.
5. 변성과 PECS(Producer Extends Consumer Super)
변성과 PECS에 대한 설명은 자바 제네릭의 공변성 & 와일드카드 완벽 이해, 인파 님의 글을 참고하면 잘 이해할 수 있다.
클래스 타입으로 사용할 때에도 '클래스'가 같을 때 구체화 타입 파라미터들 간의 상속관계에 따라 혹은 그 반대로 상속관계를 사용하게 하고 싶다. (ArrayList<Number> list = new ArrayList<Integer>();
를 가능하게 하고싶다.)할 때 '변성'을 사용한다. 변성은 제너릭 클래스를 클래스 타입으로 사용할 때의 상속관계를 정의한다. 괄호의 코드를 가능하게 하려면 ArrayList<? extends Number> = ArrayList<Integer>();
로 사용하면 된다.
그런데 이렇게 되면 타입파라미터를 타입으로 사용하는 내부 변수에 제약이 걸리게 된다. 변성을 사용하지 않을때는 ArrayList<Number> list
로 선언하면 list.add(new Integer(10));
이 가능했다. 하지만 ArrayList<? extends Number> = ArrayList<Integer>();
에서는 add가 불가능해진다. 이것은 PECS를 이해해야 한다.
5-1. Extends와 Super 생각하기
<? extends something>
과 <? super something>
을 어떻게 생각해야 할까?
? extends something
-
클래스 타입 측면에서
- 물음표(?)는 something 클래스를 상속한 무언가이다. 물음표는 something이거나 something의 후손이다.
ArrayList<? extends Number> = ArrayList<Integer>();
이 가능하다.
-
클래스 내부의 타입파라미터를 타입으로 사용하는 변수 측면에서
- 그 변수의 타입은 something이거나 something의 자식들이다. 그런데 어떤 자식일지는 모른다. 자식의 자식. 일수도 있고, 자식의 자식의 자식 일수도 있다.
- 그러니까 구체타입이 얼마나 자식일지 모르니까 어떤 객체를 consume할지 정할 수 없다. 그래서 produce만 할 수 있는 것이다.
? super something
-
클래스 타입 측면에서
- 물음표는 something 클래스를 자식 클래스로 가지는 무언가이다. 물음표는 something 이거나 something의 조상이다.
ArrayList<? super Integer> = ArrayList<Number>();
이 가능하다.
-
클래스 내부의 타입파라미터를 타입으로 사용하는 변수 측면에서
- 그 변수의 타입은 something이거나 something의 부모들이다. 그런데 어떤 부모일지는 모른다. 부모의 부모일 수도 있고, 부모의 부모의 부모 일수도 있다. (하지만 조상의 최후 조상은 있다. 그것은 Object이다.)
- 구체타입이 얼마나 부모일지 모르니까 그 변수 안에 실제로 대입된 인스턴스 객체의 타입도 얼마나 부모일지 모른다. 인스턴스 객체의 타입 아래로는 다운캐스팅 할 수 없기 때문에 produce는 불가능하고, 구체타입은 최소한 something 객체보다 부모의 타입이기 때문에 업캐스팅이 필요한 consume은 가능하다.
6. 사용 지점 변성, 선언 지점 변성
변성을 정의하는 것은 클래스나 메서드를 선언하는 시점에도 가능하고, 제네릭 클래스나 메서드를 사용하는 시점에도 가능하다.
선언 지점 변성
class Box<T extends Texture> {
//...
public void <T extends Shape> cutting() {}
}
사용 지점 변성
class Box {
//...
public void addItems(List<? super Integer> list, Integer item) {
list.add(item);
}
}
ArrayList<? extends Number> = ArrayList<Integer>();
ArrayList<? super Integer> = ArrayList<Number>();
7. 참고
자바 제네릭(Generics) 개념 & 문법 정복하기, 인파
자바 제네릭의 공변성 & 와일드카드 완벽 이해, 인파
Generic Variance 제네릭 가변성