Generics

#Java #Generics

1. 제네릭이 헷갈려!

제네릭은 너무나 당연하고 너무나 자연스러운 공기같은 존재이다.
쓸 때는 거리낌이 없지만, 제네릭을 이용한 클래스나 메서드를 작성할 일이 생기게 되면 그 때부터 혼돈의 카오스가 시작된다. 단순히 오버로딩을 줄이고자 의도한 것 뿐인데 작성할때도 이판사판이였지만 사용할때는 이판사판 공사판이 될 수 있다. 제네릭을 다시 정리하면서 어떤 포인트에서 혼동이 발생하는지를 짚어내보았다.

java_generics_intro.png

내가 헷갈리고 있었던 부분은 1. 제네릭 클래스, 제네릭 메서드의 선언, 2. 클래스 자체 타입의 캐스팅과 구체화된 타입 파라미터의 타입캐스팅에서 오는 혼란, 3. 구체화된 타입 파라미터를 가진 클래스를 타입으로 사용할때와 클래스 내부에서 구체화된 타입 파라미터를 받아들일때의 차이, 4.변성 그리고 PECS 였다. 그리고 여기에 1, 2, 3를 합치게 되면 4. 사용 지점 변성과 선언 지점 변성 또한 헷갈렸다고 보여진다. 이것들을 정리해보고자 한다.


2. 제네릭 클래스, 제네릭 메서드

Garden/Language/Java_Kotlin/generics_images/java_generics_define.png

제네릭 클래스와 제네릭 메서드의 선언은 위와 같다. 여기서 눈여겨봐야 할 것은

제네릭 타입을 사용한 메서드는 제네릭 클래스 안에 정의된 함수 중에서 클래스의 타입파라미터 타입을 매개변수나 반환값으로 혹은 로컬변수로 사용하는 메서드를 말한다. 반면 제네릭 메서드는 새로운 타입파라미터를 메서드 선언부에서 선언해줌으로써 클래스와는 독립적으로 자신만의 타입파라미터를 갖는 메서드를 의미한다.

자바 제네릭(Generics) 개념 & 문법 정복하기, 인파 인파님의 포스팅 중 이 포스트의 '제네릭 객체 만들어보기' 부분을 참고해서 이해했다.


3. 클래스 자체의 타입캐스팅, 타입파라미터의 구체화된 타입들을 가지는 클래스의 타입캐스팅

java_generics_typecasting.png

왼쪽은 맞고 오른쪽은 틀리다.
왼쪽과 오른쪽의 차이라면 왼쪽은 타입파라미터의 구체화된 타입이 같으면서 클래스 타입이 다르고(+ 상속관계에 있는 클래스들), 오른쪽은 클래스의 타입이 같으나 타입파라미터의 구체화된 타입이 다르다는 것이다(+ 타입파라미터들의 구체화된 타입들은 상속관계에 있다.).

3-1. 구체화된 타입을 가지게 된 클래스 (오른쪽 그림)

내가 제네릭 타입의 변수들을 보면서 어렴풋이 느꼈던 느낌은 아래와 같다.
java_generics_imagine.png

'NumberInteger는 상속관계에 있지! List<Number>List<Integer>도 그렇겠지!'
혹은
'<Number>던지 <Integer>던지 List니까 뭐 어떻게 관계가 있겠지!'
혹은
'이 타입들 모두 어떤 타입을 상속 받았겠지! 그러니까 뭐 어떻게 관계가 있겠지!'
였다.

하지만 제네릭 클래스 타입을 이렇게 느끼게되면 안된다. 그것보단 아래의 느낌으로 느끼는게 맞다.
java_generics_realimagine.png

우리가 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. 그래서 첫번째 사진을 바꿔보면

java_generics_casting_final.png
위와 같은 느낌으로 받아들여도 무방하다.


4. 구체화된 타입 파라미터를 가진 클래스를 타입으로 사용할때와 클래스 내부에서 구체화된 타입 파라미터를 받아들일때의 차이

generics_element_class.png

말도 살짝 어렵다. 제목을 설명하자면,
'구체화된 타입 파라미터를 가진 클래스를 타입으로 사용한다'의 의미는 List<String> list = new ...List<String>처럼 String으로 구체회된 타입 파라미터를 가지는 List 클래스가 변수의 타입으로 지정되어서 사용되는 경우를 말하는 것이다.
generics_class_type.png

그리고 '클래스 내부에서 구체화된 타입 파라미터를 받아들인다'의 의미는 list.add(new Char('a'))처럼 구체화된 타입 파라미터를 타입으로 사용하는 내부 변수에 값을 할당하는 경우를 의미한다.
generics_element_type.png

이것을 구분하는 이유는 아래의 코드를 이해할 수 있기 때문이다.
generics_class_type_element_type.png

'클래스 타입'의 차원으로 보면 NumberInteger의 사이는 무엇도 아니다. 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

? super something


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 제네릭 가변성