티스토리 뷰

변성이란?

변성(Variance)은 서로 다른 타입 간에 어떤 관계가 있는지를 나타내는 개념이다.

변성에는 공변, 반공변, 무공변(불공변)이 있는데 각 변성의 특징은 다음과 같다.

 

Child가 Parent의 하위 타입일 때,

  • 공변
    • Child[]는 Parent[]의 하위 타입이다.
    • List<Child>는 List<Parent>의 하위 타입이다.
  • 반공변
    • Parent[]는 Child[]의 하위 타입이다.
    • List<Parent>는 List<Child>의 하위 타입이다.
  • 무공변(불공변)
    • List<Parent>와 List<Child>는 서로 다른 타입이다.
// 공변성
Number[] covariance = new Integer[5];

// 반공변성
Integer[] contravariance = (Integer[]) covariance;

마치 업캐스팅, 다운캐스팅의 개념과 유사하다고 볼 수 있다.

 

자바의 제네릭 변성

// 공변성
ArrayList<Number> convariance = new ArrayList<Integer>();  // 에러!

// 반공변성
ArrayList<Integer> contravariance = new ArrayList<Number>();  // 에러!

그런데 배열과 다르게 제네릭을 선언한 리스트는 실제로는 에러가 발생한다.

자바에서는 제네릭이 무공변의 성질을 갖기 때문이다.

 

일반 객체의 공변성

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

parent = child;  // 업캐스팅
Object parent = new Integer(5);
Integer child;

child = (Integer) parent;  // 다운캐스팅

자바의 대표적인 특징 중 하나인 다형성의 예로 상속 관계에 있는 객체들끼리는 서로 캐스팅이 가능하다.

제네릭 클래스도 다형성이 적용되는 것은 마찬가지이다.

List<Integer> parent = new ArrayList<>();

ArrayList는 List를 상속받고 있기 때문에 위처럼 업캐스팅이 가능하다.

 

제네릭 타입의 무공변성

그러나 제네릭 타입 (꺽쇠 괄호) 는 무공변이기 때문에 캐스팅이 불가능하고 딱 전달받은 타입만 가질 수 있다.

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

parent = child;  // (X)

Object는 Integer를 포함하지만 제네릭 타입에서는 캐스팅이 되지 않는다.

제네릭 클래스와 제네릭 타입에서의 성질을 헷갈리지 말아야 한다!

 

와일드 카드<?>의 등장 배경

제네릭의 타입 파라미터는 단 하나의 타입만이 허용되는데 무공변의 성질을 갖기 때문에 유연함이 떨어진다는 단점이 있다.

public void productId(List<Object> list) {};

ArrayList<Integer> list = Arrays.asList(1, 2, 3);

productId(list);  // X

여러가지 타입을 담고자 모든 객체의 상위인 Object를 타입으로 지정하였으나 list는 Integer만 받을 수 있기 때문에에러가 발생한다.

public void productId(List<Integer> list) {};

public void productId(List<String> list) {};

만약 무공변인 상태로 구현을 해야한다면 위와 같이 타입별로 메소드를 구현해야 하는 것이다.

이러한 점을 해결하기 위해 와일드카드<?> 가 등장하게 되었고 어떤 타입이든 받을 수 있다는 것을 의미한다.

 

제네릭 와일드카드

 

와일드카드는 보통 타입 한정 연산자와 함께 쓰인다.

 

와일드카드 명칭 설명
<?> 비한정적 와일드카드 타입 제한 없음
<? extends T> 상한 경계 와일드카드 상위 클래스 제한(T와 그 하위 클래스만 가능)
<? super T> 하한 경계 와일카드 하위 클래스 제한(T와 그 상위 클래스만 가능)

 

상한 경계 와일드카드(공변)

상한 경계 와일드카드는 와일드카드 타입에 extends를 사용해서 와일드카드 타입의 최상위 타입을 정의하는 것이다.

class Fruits {};

class Apple extends Fruits {};

class Mango extends Fruits {};

public class Store {
    List<Fruits> fruits = new ArrayList<>();

    public void putItem(Collection<? extends Fruits> list) {
        fruits.addAll(list);
    }
}
Store store = new Store();

List<Mango> mango = new ArrayList<>();
mango.add(new Mango());

store.putItem(mango);

위와 같이 최상위 타입을 Fruits로 제한해서 Fruits 타입을 상속받는 Mango를 생산할 수 있다.

Mango가 Fruits의 하위 타입이므로 List<Mango> 는 List<Fruits> 의 하위 타입이다. 라는 공변의 성질이 적용되는 것이다.

 

하한 경계 와일드카드(반공변)

하한 연계 와일드카드는 super를 사용해 와일드카드의 최하위 타입을 정의한다.

class Fruits {};

class Apple extends Fruits {};

class Mango extends Fruits {};

public class Store {
    List<Fruits> fruits = new ArrayList<>();

    public void putItem(Collection<? extends Fruits> list) {
        fruits.addAll(list);
    }

    public void selling(List<Mango> items) {
        items.addAll(getMango());
    }

    private List<Mango> getMango() {
        return fruits.stream()
                    .filter(item -> item instanceof Mango)
                    .map(Mango.class::cast)
                    .collect(Collectors.toList());
    }
}

과일상점에서 판매한 과일의 리스트를 이동시키는 로직이다.

Store store = new Store();

List<Fruits> sellingList = new ArrayList<>();
store.selling(sellingList);  // X

자바에서 매개변수에는 동일한 타입 혹은 하위 타입의 변수만을 넣을 수 있는데 List<Fruits> 는 List<Mango> 의 하위 타입이 아니기 때문에 에러가 발생한다.

selling 메소드는 판매한 과일을 받을 수 있는 상위 타입이 리스트로 들어와야 한다.

super 키워드가 T와 그 상위 클래스를 허용해주는 것이므로 하한 경계 와일드카드를 이용해서 반공변을 통해 해결해보자!

class Fruits {};

class Apple extends Fruits {};

class Mango extends Fruits {};

public class Store {
    List<Fruits> fruits = new ArrayList<>();

    public void putItem(Collection<? extends Fruits> list) {
        fruits.addAll(list);
    }

    public void selling(Collection<? super Mango> items) {
        items.addAll(getMango());
    }

    private List<Mango> getMango() {
        return fruits.stream()
                    .filter(item -> item instanceof Mango)
                    .map(Mango.class::cast)
                    .collect(Collectors.toList());
    }
}
Store store = new Store();

List<Fruits> sellingList = new ArrayList<>();
store.selling(sellingList);

 

PECS 공식

Effective Java 라는 책에서 소개되는 개념인 PECS라는 공식은 와일드카드의 extends와 super 키워드의 사용 시기를 표현해주는 방식이다.

PECS란, Producer-Extends, Consumer-Super의 약자이다.

 

외부에서 온 데이터를 생산(Producer)한다면 <? extends T>를 사용

외부에서 온 데이터를 소비(Consumer)한다면 <? super>를 사용

 

Producers-Extends

extends 키워드는 생산하는 역할을 한다.
위 예제에서는 상점에 과일을 들여와서 진열하는 것이라고 볼 수 있겠다.

class Fruits {};

class Apple extends Fruits {};

class Mango extends Fruits {};

public class Store {
    List<Fruits> fruits = new ArrayList<>();

    public void putItem(Collection<? extends Fruits> list) {
        fruits.addAll(list);
    }
}

 

Consumer-Super

반면 super 키워드가 쓰인 selling 메소드는 상점에 있던 과일을 소비해서 판매 리스트에 올리는 행위라고 볼 수 있다.

class Fruits {};

class Apple extends Fruits {};

class Mango extends Fruits {};

public class Store {
    List<Fruits> fruits = new ArrayList<>();

    public void selling(Collection<? super Mango> items) {
        items.addAll(getMango());
    }

    private List<Mango> getMango() {
        return fruits.stream()
                    .filter(item -> item instanceof Mango)
                    .map(Mango.class::cast)
                    .collect(Collectors.toList());
    }
}

 

in / out 공식

오라클 공식 문서에서는 in과 out의 개념으로 와일드카드의 사용처를 설명한다.

 

in 은 코드에 복사할 데이터를 제공하는 목적 -> extends

out은 다른 곳에서 사용할 데이터를 보유 -> super

예시를 들어보면,
extends 키워드는 제네릭 타입 매개변수의 데이터를 가져오는 역할로, 외부에서 과일을 상점으로 들여오는 것이고
super 키워드는 제네릭 타입 매개변수에 데이터를 적재하는 역할로, 판매한 과일 리스트를 적재하는 것이라고 생각하면 될 듯 하다!


참고 자료

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함