숑숑이의 개발일기

자바에서 우리가 가장 쉽게 접하고 있는 제너릭의 예시는 List가 있다. 아래는 List 내부 구현 코드이다.

public interface List<E> extends Collection<E> {

    boolean add(E e);
    
    E get(int index);

	/***/
}

List는 제네릭을 활용하므로, 우리는 다양한 타입의 List를 선언할 수 있다. 또한, List에 잘못된 타입의 값을 추가하는 경우 컴파일 타임에 예방을 해준다.

// 다양한 타입의 List 선언가능
List<String> messages = new ArrayList<>();
List<User> users = new ArrayList<>();

// 잘못된 타입의 값을 컴파일 에러를 통해 예방
List<String> codes = new ArrayList<>();
codes.add("1");
codes.add(1);	// 에러발생

 

 

변성이란?

변성이란 제너릭 프로그래밍을 하면서 상속에 관련되어 생기는 이슈가 생길때 접하곤 한다. 변성은 타입의 계층 관계에서 서로 다른 타입간에 어떤 관계가 있는지 나타내는 개념이다. 제네릭을 사용할 때 기저 타입이 같고 타입 인자가 다른 경우, 서로 어떤 관계가 있는지를 나타내는 것이라고 보면 된다. (List<String> 에서 List는 기저 타입, String은 타입인자)

 

타입 관계가 의미하는 것은 "어떤 타입 S가 T의 하위 타입이라면, S를 T로 대체할 수 있는가?"의 문제로 이어지기도 한다. 이는 상위 타입이 사용되는 곳에는 언제나 하위 타입의 인스턴스를 넣어도 이상 없이 동작해야 함을 의미한다.

 

아래 코드를 살펴보자.

Integer는 Number를 상속받아 만들어진 객체다. 그래서 Integer는 Number의 하위 타입이라고 할 수 있다.

public void test() {
    List<Number> list;
    list.add(Integer.valueOf(1));
}

그러나 List<Integer>는 List<Number>의 하위타입이 될 수 없다. 이러한 상황에서 type parameter에 타입 경계를 명시하여 Sub-Type, Super-Type을 가능하게 해주는데 이것을 변성이라고한다.

 

경계 bound  
상위 경계 Upper bound Type<? extends T>
하위 경계 Lower bound Type<? super T>

 

공변(covariant)

A가 B의 상위 타입이고 T<A>가 T<B>의 상위 타입이면 공변이다.extends 키워드를 사용해 공변 처리할 수 있다.

public class Bank {

    public void giveMoney(List<? extends User> users) {
        // List의 요소가 User의 하위 타입이라는 뜻
    }
}

 

공변에서 제네릭 타입을 사용하는 메서드에 값을 전달 할 수 없다.

List<? extends User> users = new ArrayList<>();
users.add(new Player()); // 컴파일 에러

이는 List의 실제 타입을 특정할 수 없기 때문이다. 이 문제는 반공변을 통해 해결할 수 있다.

 

 

반공변(contravariant)

A가 B의 상위 타입이고 T<A>가 T<B>의 상위 타입이면 반공변이다. super 키워드를 사용해 반공변 처리할 수 있다. 다르게 말해 부모가 자식의 상위 타입이라면 List<Parent>는 List<? super Child>의 하위타입이다.

// List<Player>는 List<? super Player>의 하위 타입이다.
List<? super Player> players = new ArrayList<>();
players.add(new Player());

// List<User>는 List<? super Player>의 하위 타입이다.
List<? super Player> players2 = new ArrayList<User>();
players.add(new Player());

 

무공변(invariance) or 불공변

기본적으로 제네릭의 경우 무공변/불공변성을 가지므로 같은 타입만 대입 가능하게 한다.

// User는 Player의 상위 타입이다.
User user = new Player(); // OK

// List<User>는 List<Player>의 상위 타입이 아니다.
List<User> users = new ArrayList<Player>(); // 컴파일 에러

 

언제 어떤걸 사용하는가?

펙스(P.E.C.S) : Producer-Extends, Consumer-Super

위의 공식을 통해 어떤 와일드카드 타입을 써야 하는지 알 수 있다.

  • 생산자(producer) : extends
  • 소비자(consumer) : super

 

요약

  • 제네릭은 기본적으로 무공변/불공변성을 가진다
  • 상한 경계(extends) 타입 변수 지정하여 제네릭은 공변성을 가질 수 있다.
  • 하한 경계(super) 타입 변수 지정하여 제네릭은 반공변성을 가질 수 있다.

공변은 인터페이스 구현이나 상속(부모-자식 관계)을 생각하고, 반공변은 부모-자식 관계가 반대로 된 것으로 생각하자

 

https://kdhyo98.tistory.com/83#%EB%B6%88%EA%B3%B5%EB%B3%80-1
https://dev-ljw1126.tistory.com/m/310
https://xxeol.tistory.com/25
https://sungjk.github.io/2021/02/20/variance.html
profile

숑숑이의 개발일기

@숑숑-

풀스택 개발자 준비중입니다