자바의 변성 알아보기

목차

변성이란 다형성을 구현하기 위해 나온 개념이다. 다형성을 구현하는 건 항상 좋은 걸까?

변성에 대해 자세히 알아보자. 그리고 자바는 변성을 가지고 있을 까❔

변성은 왜 사용할까?

  • 변성이란 서로 다른 타입 간에 어떤 관계가 있는지 나타내는 개념이다.

  • 초기화 시, 혹은 매개변수가 T로 선언되어있다면

    • 공변 : T와 T의 자식타입만 받을 수 있다.

    • 반공변 : T와 T의 부모타입만 받을 수 있다.

    • 불공변 : T만 가능하다(선언한 타입만 들어갈 수 있음).

이러한 변성은 왜 사용할까? 다형성을 구현할 수 있기에 쓴다. 자기 자신외의 타입들도 받을 수 있기 때문이다. 하지만 다형성이 있다면 타입 안정성이 떨어져 런타임 에러가 발생할 수 있다.

타입 안정성이란 ❔

의도하지 않는 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄어주는 것. ⇒ 아래 예시가 나온다.

자바는 변성을 가질까?

그렇다면 자바는 변성을 가질까? 배열/제네릭/한정적 와일드 카드의 케이스마다 다르다. 각각의 경우마다 변성과 타입 안정성을 알아보자.

우선 배열은 공변성을 가진다. 단, 업캐스팅 할 경우 잘못된 타입을 삽입할 수 있다. 아래 예시에서는 선언부를 Object로 하고, 구현체는 Long으로 하였다. 이때, 실제 구현체(Long)와 다른 자식(String)을 삽입할 경우, 런타임 에러가 발생한다. 즉, 배열을 업캐스팅하면 다형성을 구현할 수 있다. 하지만 타입 안정성이 떨어져 런타임 에러가 발생할 수 있다.

Object[] arr = new Long[10];  // 컴파일 가능
arr[0] = "A";  // 런타임 에러, Long 타입만 삽입 가능하다!

반면 제네릭은 변성을 가지지 않는다(불공변성). 오직 자기자신과 같은 타입만 가능하다. 따라서 제네릭은 배열처럼 잘못된 형 삽입으로 인한 런타임 에러가 발생하지는 않는다. 그러나 다형성을 구현할 수는 없다.

List<Object> obj = new ArrayList<Long>();  // 컴파일 에러
List<Object> obj = new ArrayList<Object>();  // 같은 타입만 가능!

자바의 변성을 정리해보면, 배열은 공변성을 가지면서 다형성을 구현한다. 그러나 잘못된 형 삽입으로 인해 런타임 에러가 발생할 수 있다(타입 안정성이 없다). 제네릭은 공변성을 가지지 않는다(자기 타입만 가능). 타입 안정성을 가져 인한 런타임 에러가 발생할 일은 없으나, 다형성을 구현하지 못한다. 그렇다면 제너릭의 한정된 와일드 카드의 경우는 어떨까❔

🚩 배열과 제네릭을 보면, 다형성과 타입 안정성을 함께 가지지는 못하는 듯 보인다.

제네릭이 한정적 와일드 카드를 사용하면 공변성 혹은 반공변성을 가진다.

이때도 다형성과 타입 안정성을 둘 다 가지지 못할까?

공변성(Co-variant)

  • <? extend T> : T와 T의 자식 객체만 가능

  • 자기 자신과 자식 객체만 가능하다.

타입 매개변수에 <? extends Type>를 선언하면 타입 매개변수가 공변성을 가진다. Type 자신과 자식 클래스만 가능하다. 제네릭으로 업캐스팅할 경우(공변성), 읽기는 가능하지만 쓰기는 불가능하다. 쓰기(삽입하기)를 하면 컴파일 에러가 발생한다. 따라서 타입 안정성을 가지고, 런타임 에러를 막는다.

// 선언 시
List<? extends Animal> covariantList = new ArrayList<Dog>(); 

// 읽기 가능
Animal x = covariantList.get(0); // OK

// 쓰기 불가능
covariantList.add(new Animal()); // 컴파일 에러
covariantList.add(new Dog()); // 컴파일 에러
covariantList.add(new Cat()); // 컴파일 에러, 이럴 수 있어서 아예 쓰기를 막는다.

반대로 쓰기가 필요하고, 읽을 필요는 없을 수 있다. 이때는 <? super T> (반공변성)을 사용한다.

반공변성(Contra-variance)

  • <? super T> : T와 T의 부모 타입만 가능

  • 자기 자신과 부모 타입만 가능하다.

타입 매개변수에 <? super Type>를 선언하면 타입 매개변수에 반공변성을 가진다. 즉, Type 의 부모들을 받을 수 있다. 이때는 쓰기를 해도 런타임 에러가 발생하지 않는다. 부모 객체들을 받는 데, 부모는 자식보다 한정된 기능을 가지고 있기 때문이다. 반면 읽기는 안된다. 읽기(꺼내오기)를 하면 컴파일 에러가 발생한다.

  • 부모 타입 선언 = 자식 타입 구현체 ⇒ 업캐스팅, 가능하다

  • 자식 타입 선언 = 부모 타입 구현체 ⇒ 불가능하다. 자식(부모+a)이기 때문이다. 부모 구현체로 자식의 메소드를 사용할 수 없기 때문이다.

    • 읽으면(꺼내면) 이 경우에 해당된다.

List<? super Pet> contravariantList = new ArrayList<Pet>();

// 읽기 불가능
Pet pet = contravariantList.get(0); // 컴파일 에러
// Pet보다 부모 객체가 담겨 있을 수 있으므로 꺼낼 수 없다.

// 쓰기 가능
contravariantList.add(new Pet()); // OK

참고) 한정적 와일드카드 타입 사용시, 변성을 정하는 지점이 2가지 있다.

  • 선언 지점 변성 : 클래스 선언 시 변성을 정함

    • List<? extends T> x = new ArrayList<Dog>();

  • 사용 지점 변성 : 타입 파라미터에 변성을 정함

    • public void func(List<? extends T> x)

정리하면

  • 공변은 다형성을 구현할 수 있으나, 타입 안정성이 없어 런타임 에러가 날 수 있다.

  • 자바의 배열은 공변성이다. 다형성을 구현할 수 있지만, 타입 안정성을 가지지 못한다.

  • 자바의 제너릭은 불공변성이다. 타입 안정성을 가지나, 다형성을 구현하지 못한다.

  • 제너릭 한정적 와일드 카드의 경우

    • <? extend T> : 공변성을 가지고, 읽기O, 쓰기X(컴파일 에러)

    • <? super T> : 반공변성을 가지고, 쓰기O, 읽기X(컴파일 에러)

    • 케이스마다 공변성/반공변성을 선택하여 사용한다. 이로 인해 다형성과 타입 안정성을 둘 다 가지면서 구현할 수 있다.

Reference

위로가기⬆

Last updated