본문 바로가기
백엔드

[Java] 왜 배열은 Covariant(공변)이고 제네릭은 Invariant(불공변)일까?

by BeforB 2022. 7. 22.
728x90

 

 

 

 

자바에는 Variance(변성)이라는 개념이 있다. 변성은 타입의 계층 관계에서 서로 다른 타입 간에 어떠한 관계가 있는지 나타낸다.

변성의 개념은 자바의 Generic에서도 사용된다. 오늘의 주제인 Covariant(공변), Contravariant(반공변), Invariant(불공변)는 모두 변성의 한 종류이다.

각각의 개념을 간단하게 설명하면 아래와 같다.

 

 

Covariant - SubType[]은 SuperType[]이다. (ex - String[]은 Object[]이다.)

Contravariant - SuperType[]은 SubType[]이다. (ex - Object[]은 String[]이다.)

Invariant - SubType[]은 SuperType[]이 아니고, SuperType[]은 SubType[]이 아니다. (ex - String[]은 Object[]이 아니고, Object[]은 String[]이 아니다.)

 

 

제목에 쓰여 있듯이 자바에서 배열은 Covariant하고, 제네릭은 Invariant하다. 즉, 객체 타입의 관계에서 A가 B의 하위타입일 때, 배열 A[]는 B[]의 하위 타입이지만 List<A>는 List<B>의 하위 타입이 아니다.

 

 

 

"그렇다면 왜 배열은 Covariant하고, 제네릭은 Invariant할까??"

결론부터 말하자면, 제네릭이 존재하지 않을 당시 배열은 Covariant할 필요가 있었고, 이후에 나온 Generic은 Covariant로 발생하는 문제를 방지하기 위해 Invariant하게 설정되었다.

 

 

 

"배열은 Covariant 할 필요가 있었다."

초기 자바에는 제네릭이라는 개념이 존재하지 않았다. 이 상황에서 배열을 invariant(불변)하게 만드는 것은 다형성 프로그래밍을 방지한다. 예를 들어 Object[] 배열을 비교하는 Object.equals() 함수를 작성할 때 invariant할 경우 배열을 비교할 때 Object가 아닌 정확한 타입(Integer, String, …) 에 대한 equals() 메소드를 각각 작성되어야 할 것이다. 이러한 부분을 해결하기 위해 자바에서는 배열을 Covariant(공변)로 처리한다.

 

하지만 Covariant함으로써 몇 가지 문제가 발생한다. 자바에서는 배열이 생성될 때 각 타입을 마킹하여 처리한다. 즉, 값이 배열에 저장될 때마다 JVM은 런타임 시 배열의 타입과 런타임 시 값의 타입이 동일한지 확인한다. 만약 array의 타입과 value의 타입이 맞지 않을 경우 ArrayStoreException이 발생한다.

// Covariant - SubType[]은 SuperType[]의 하위타입이다.
Object[] a = new String[1];

// 실제로 a의 타입은 String[] 이기 때문에 런타임 시 ArrayStoreException이 발생한다.
a[0] = new Object();

위와 같이 공변의 경우 ‘쓰기' 시점에 문제가 발생하게 된다. 이 경우 컴파일 시에는 오류를 발견할 수 없고 런타임 시에만 오류를 발견할 수 있으며, 매번 배열에 값을 넣을 때마다 타입 체크가 이루어지기 때문에 성능 저하도 일으키게 된다.

 

 

 

"제네릭은 Invariant하다."

위와 같은 이유들로 인해 JDK 1.5 이후 등장한 제네릭은 공변이 불가능하게 되었고, 대신 와일드카드를 적용하여 공변과 반공변의 표현이 가능하도록 했다.

 

 

 

728x90

댓글