mm Home

Java - Generics 본문

개발/Java

Java - Generics

jess_m 2019. 12. 4. 22:52

Generic

JDK 1.5에서 타입 안정성을 위해 generic 추가 됨.

//아래와 같이 raw type을 쓰게되면 ClassCastException을 발생하기 쉬웠다
List list = new ArrayList();
list.add(1);
list.add("jess");

Generic을 만들었는데, JDK 1.5 미만의 버전의 호환을 선택.

그래서…


 

Type Erasure

하위 호환을 위해 Type Parameter 정보를 지운다.
JDK 1.5 미만에서 컴파일이 되어도 Generic을 사용했을때와 동일하게 하기 위함이다.

List<String> list = new ArrayList<>();
//위의 코드는 Type Erasure에 의해 실제 Runtime 시에 아래와 같다.
List list = new ArrayList();

반면에, C#의 경우 구체화(Reification)로 하위 호환을 포기했다.


 

Wildcard

그런데.. generic하게 쓰기 위해 아래와 같으면 컴파일 되지 않는다.

public class A{}
public class B extends A {}

List<A> aList = new ArrayList<>();
List<B> bList = aList;      //compile error

//List<A> 의 subType이 List<B>가 아니기 때문이다.
//ArrayList<A> <- List<A> <- Collection<A>
//ArrayList<A>의 subType은 List<A>이고, List<A>의 subType은 Collection<A> 이기 때문이다.

그래서 wildcard를 통해 타입 파라미터가 Unknown type이라고 표시해주면, 어떠한 타입이든 컴파일러가 신경쓰지 않는다.

//아래는 가능하다.
List<A> aList = new ArrayList<>();
List<?> bList = aList;

//그런데 또 아래와 같은 코드가 불가능하다.
bList.add(new Object());
// 컴파일러는 bList에 insert 해야될 타입을 몰라서 에러를 발생시킨다. (Generic 에서 타입 추론을 하기 위해 타입을 capture 하는 프로세스가 있는데, capture에 실패했기 때문에 capture of ~~ 에러가 발생)
// 컴파일러가 잘못된 타입을 넣었다고 판단하는 것.
// 아래에서 더 자세히 살펴보자.

 

가변성

그래서 가변성.. 가변성이란?

변할 수 있는 성질! 타입 파라미터가 변할 수 있음을 알려준다. (타입 파라미터간의 관계를 정하는 것)

기저 타입은 같지만, 타입 파라미터가 다른 경우의 두 Generic 타입을 살펴보자
C, C<A'> 가 있다. 그리고 A -> A' 의 의존성을 가지고 있다.

가변성을 주지 않으면 타입 파라미터는 기본적으로 무공변(Invariant)성을 가짐.

A -> A' 이더라도 C -> C<A'> 불가능.


 

공변 (Covariant)

A -> A' 일때, C -> C<A'>

*공변은 수학에서 사용하는 용어. 같은 방향으로 변한다는 뜻.

반공변 (Contravariant)

A -> A' 일때, C <- C<A'>

*불공변은 공변의 반대어. 다른 방향으로 변한다는 뜻. 위의 공변과 화살표가 반대임을 볼 수 있다.

리스코프 치환 원칙에 의하면

A = A' 가능

A' = A 불가능

그런데

공변일 때 C = C<A'>

반공변일 때 C<A'> = C

Wildcard를 통해 컴파일러가 통과하게 함. 컴파일 단계를 지나면 type erasure에 의해 raw type과 똑같으니 런타임에 문제가 없음.

왜냐면, 우리는 List<?> 를 List 처럼도 쓰고 싶고, List 처럼도 쓰고 싶을 때가 있으니까..

List<? extends A> aList = new ArrayList<>();
List<? super A> bList = new ArrayList<>();

A a = aList.get(0);
aList.add(new B()); //  안됨. B b = new A() 가 안되니까.


bList.add(new B()); // 됨. B b = new A() 를 컴파일러가 반공변으로 가능하게 했으니까. 물론 실제로 저게 가능한건 아님. 컴파일러가 타입 파라미터 추론 할때 가능하게 한 것. 
Object b = bList.get(0);

먼저 알아두어야 할 것은

리스코프 치환원칙에 의해 subType으로 치환이 가능하다. (subType은 읽기 가능)

A a = b; //b 객체안에 C가 있든, D가 있든 A를 상위타입으로 가지는 어떤 것도 읽을 수 있다.

하지만 superType으로 쓰는것은 불가능하다. (superType은 쓰기 불가능)

B b = new A(); // 불가능

자기 자신의 타입은 subType이자, superType 이다.

? extends A = A

? super A = A

가 가능하다는 것.

다시 타입 관계를 알아보자.

List<?> -> List<A>
List<?> -> List<B>

List<?> -> List<? super B> -> List<? super A>, List<? extends A> -> List<A>
List<?> -> List<? extends A> -> List<? extends B>, List<? super B> -> List<B>

Hierarchy

Type Hierarchy

앞서 이야기한듯이 파라미터 타입 A와 B사이에는 관련이 없지만, wildcard를 사용한 List의 subType이 된다.
그렇기 때문에 wildcard를 썼을때 치환이 가능했던 것.

 

 


근데 왜 공변성을 주었을 때 add 가 안될까?

List 에서 해당 없는 subType = List<? extends B>

List 에서 해당 없는 subType = List<? super A>

그러니까!!
공변으로 받으면(List <? extends A>), A extends이지만 A의 super는 아니다. -> 말이 어렵지만 집중하고 생각해보면.. A의 subType인데, A의 superType이 아니다.. A의 Super가 아니기 때문에 가장 최상위인 Object도 >아니라고 생각한다. 그래서 object를 쓸수가 없다. 대신 null은 쓸 수 있음.

반공변으로 받으면(List <? super A>), A extends 이자만 A의 super는 아니다. -> A 타입이 superType이지만, A가 subType이 아니다.. A의 subType이 아니기 때문에 읽기가 불가능하다. 하지만 super 타입이기 때문에 쓰기가 가능.

in = extends (Upper bound)

out = super (Lower bound)

in variable = 사용하기 위해 코드 안으로 가져옴 (코드에 데이터 제공). consuming.

out variable = 밖에서 사용하는 데이터(다른 곳에서 사용할 데이터 보유). producing.

'개발 > Java' 카테고리의 다른 글

Java 9 features  (0) 2017.11.10
Java - static  (0) 2017.09.06
Java 8 - Stream  (0) 2017.08.11
Java 8 - Lambda Translation  (0) 2017.08.10
Java 8 - 람다 표현식  (2) 2017.08.10
Comments