본문 바로가기
Java

지네릭 - 공변성과 불공변성

by runlearn 2023. 6. 28.

지네릭 - 공변성과 불공변성

변성 이란 상속 계층 관계에서 서로 다른 타입 간에 어떤 관계가 있는지 나타내는 지표다. 공변성은 서로 다른 타입간에 함께 변할 수 있다는 특징을 말한다. 관련된 객체 지향 개념으로는 리스코프 치환원칙이 있다.

S가 T의 하위타입인 경우를 기준으로  공변, 반공변, 불공변이 어떤 차이가 있는지 알아보자.

  • 공변성 : S 는 T 의 하위 타입이다.
    ex) List<S>List<T> 의 하위 타입이다.
    String 이 Object의 서브타입이면, List<String>List<? extend Object> 의 서브타입이다.
  • 반공변 : T 는 S 의 하위 타입이다. (공변의 반대)
    ex) List<T>List<S> 의 하위 타입이다.
    String 이 Object의 서브타입이면, List<Object>List<? super String> 의 서브타입이다.
  • 불공변 : S 와 T 는 서로 관계가 없다.
    ex) List<S>List<T> 는 서로 다른 타입이다. (관계가 없음)
    List<String>List<Object>의 하위타입이 아니다.

예시 코드

// 공변성
Object[] Covariance = new Integer[10];

// 반공변성
Integer[] Contravariance = (Integer[]) Covariance;


// 공변성
ArrayList<Object> Covariance = new ArrayList<Integer>();

// 반공변성
ArrayList<Integer> Contravariance = new ArrayList<Object>();

배열과 달리 자바의 제네릭에서는 공변성/반공변성을 지원하지 않는다.
따라서 ArrayList 코드는 컴파일 에러가 발생한다.

Object parent = new Object();
Integer child = new Integer(1);

parent = child;

제네릭은 공변성이 없다!

다형성

Java는 객체지향 언어로 객체 타입은 상하 관계가 있다.
Object 타입으로 선언한 변수에는 String 타입으로 선안한 변수를 대입할 수 있다. (업캐스팅)

Object parent = new Object();
Integer child = "child";

parent = child; 

다음 예제를 보면 제네릭 타입이 같으면 객체 간에 상하관계가 유효한 것을 확인할 수 있다.

Collection<String> parent = new ArrayList<>();
ArrayList<String> child = new ArrayList<>();

parent = child; // 다형성 (업캐스팅)

제네릭 타입은 상하관계 없음

ArrayList<Object> parent = new ArrayList<>();
ArrayList<String> child = new ArrayList<>();

parent = child; // ! 업캐스팅 불가능
child = parent; // ! 다운캐스팅 불가능

공변성이 없어 발생하는 문제점

메서드의 매개변수으로 지네릭을 사용할 경우 외부에서 넘겨주는 파라미타의 캐스팅 문제가 발생할 수 있다.
매개변수가 배열과 List인 상황의 예시로 비교해 보자.

배열 예시

public static void print(Object[] arr) {
    for (Object e : arr) {
        System.out.println(e);
    }
}

public static void main(String[] args) {
    Integer[] integers = {1, 2, 3};
    print(integers); // [1, 2, 3]
}

List 예시

public static void print(List<Object> arr) {
    for (Object e : arr) {
        System.out.println(e);
    }
}

public static void main(String[] args) {
    List<Integer> integers = Arrays.asList(1, 2, 3);
    print(integers); // ! Error
}

와일드카드

지네릭의 공변성 관련 문제는 와일드카드로 해결할 수 있다.
와일드카드는 ?로 사용하며, <?>로 사영하면 Object와 동일한 의미를 가진다. 일반적으로 와일드 카드의 타입 범위를 제한하기 위해 extends,super 키워드와 함께 사용한다.

  • <?> : 제한 없음 (모든 타입 가능)
  • <? extends U> : 상위 클래스 제한 (상한 경계, U와 자손들만 가능) -> 공변성
    • EX ) ArrayList<? extenes Number> list = new ArrayList<>(); : 지네릭 타입으로 Number 자손타입 대입 가능
  • <? super U> : 하위 클래스 제한 (하한 경계, U와 조상들만 가능) -> 반공변성
    • EX ) ArrayList<? super Number> list = new ArrayList<>(); : 지네릭 타입으로 Number 부모타입 대입 가능
    • 상위타입 데이터를 대입하는 경우 사용한다.

와일드카드가 필요한 예

class MyArrayList<T> {
    Object[] element = new Object[5];
    int index = 0;

    // 외부로부터 리스트를 받아와 매개변수의 모든 요소를 내부 배열에 추가하여 인스턴스화 하는 생성자
    public MyArrayList(Collection<T> in) {
        for (T elem : in) {
            element[index++] = elem;
        }
    }

    // 외부로부터 리스트를 받아와 내부 배열의 요소를 모두 매개변수에 추가해주는 메서드
    public void clone(Collection<T> out) {
        for (Object elem : element) {
            out.add((T) elem);
        }
    }

    @Override
    public String toString() {
        return Arrays.toString(element); // 배열 요소들 출력
    }
}
public static void main(String[] args) {
    // MyArrayList의 제네릭 T 타입은 Number
    MyArrayList<Number> list;

    // MyArrayList 생성하기
    Collection<Integer> col = Arrays.asList(1, 2, 3, 4, 5);
    list = new MyArrayList<>(col); // ! ERROR

    // MyArrayList 출력
    System.out.println(list);
}
  • Integer는 Number를 상속하여 둘은 상속관계를 가진다. 하지만 제네릭의 타입 파라미터는 기본적으로 불공변성이라 컴파일 에러가 발생한다.
  • 해당 문제를 해결하기 위해서는 파라미터 Collection<T> 부분을 Collection<? extends T> 로 수정해야 한다.

와일드카드 주의사항!

super
  • 데이터를 꺼낼 때 어떤 타입이 나올지 모름 (super 사용시 Object 타입으로 꺼내는 이유)
  • 데이터를 넣을 때 T 의 자손 데이터만 넣어야 함. 어떤 타입이 오더라도 꺼낼 수 있도록 하기 위함

PECS 공식

지네릭 메서드

https://ecsimsw.tistory.com/entry/%EC%9E%90%EB%B0%94-%EC%A0%9C%EB%84%A4%EB%A6%AD-%EB%A9%94%EC%86%8C%EB%93%9C
https://nozeroslope.tistory.com/285

참고
https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%A0%9C%EB%84%A4%EB%A6%AD-%EC%99%80%EC%9D%BC%EB%93%9C-%EC%B9%B4%EB%93%9C-extends-super-T-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4#thankYou

https://www.youtube.com/watch?v=w5AKXDBW1gQ

댓글