[JAVA] 제네릭 공변, 반공변

2025. 5. 26. 23:40·java

그동안 얕게 알고 쓰던 자바를 정리하기 위한 포스팅 / 개인 기록용

 

자바, 코틀린에는 제네릭이라는 개념이 있다. 제네릭은 타입을 파라미터로 전달할 수 있도록 하기 위해 만들어진 것이다.

흔히 List, Map과 같은 Collections를 사용하기 때문에 자연스럽게 사용해왔을 것이다.

 

단순히 제네릭을 사용하는 건 어렵지 않다. 다만 제네릭과 관련된 학습 문서를 보면 와일드카드와 관련해서 공변, 반공변이란 개념을

접할 수 있는데 이 개념들이 그렇게 쉽게 잘 와닿지 않기 때문에 다시 정리한다.

제네릭은 불공변이다.

공변이란 것은 타입 계층이 그대로 유지되는 것이라고 생각하면 된다.

예를 들어 Animal(부모) -> Cat(자식) 관계라고 하자. 그럼 Animal과 Cat이 사용되는 어떠한 클래스도 똑같이 성립된다면 공변이다.

예를 들어 List를 사용한다면 List<Animal>(부모) -> List<Cat>(자식) 관계가 성립한다.

 

부모 - 자식 관계에서 중요한 것은 자식은 부모를 대체할 수 있다는 것이다.

 

아무튼 제네릭은 기본적으로 공변이 아니다. 따라서 List<Animal>과 List<Cat> 이 둘은 어떠한 관계도 없으며 그냥 별개의 타입이다.

? extends T는 공변이다.

그런데 제네릭이 공변 성질을 가질 수 없다면 매우 불편할 것이다.

T에는 수 많은 타입들이 적용될 수 있는데 해당 타입마다 메서드를 만들어줘야 하기 때문이다.

예를 들어 리스트에 있는 동물들을 전광판에 띄운다고 생각해보자.

public class Display {
    public void displayAnimals(List<Cat> cats) {
        cats.forEach(System.out::println);
    }

    public void displayAnimals(List<Dog> dogs) {
        dogs.forEach(System.out::println);
    }
}

 

위 코드처럼 동물 종류만큼 메서드를 만들어야 할 것이다.

List<T>에서 T는 전부 Animal을 상속하고 있는 자식관계일지라도 제네릭은 공변성이 없기 때문이다.

그래서 제네릭은 ? extends라는 와일드 카드를 통해 특정 상황에서는 공변성을 가질 수 있도록 한다.

public class Display {
    public void displayAnimals(List<? extends Animal> animals) {
        animals.forEach(
            Animal::sound
        );
    }
}

 

extends의 의미는 Animal 혹은 Animal의 하위 클래스를 뜻한다.

따라서 displayAnimals 메서드는 Animal 혹은 그 하위 클래스를 받을 수 있다.

public class Main {
    public static void main(String[] args) {
        List<Cat> catList = new ArrayList<>();
        catList.add(new Cat());

        List<Dog> dogList = new ArrayList<>();
        dogList.add(new Dog());

        Display display = new Display();
        display.displayAnimals(catList);
        display.displayAnimals(dogList);
    }
}

 

이제 displayAnimals 메서드를 사용하는 곳에서 catList, dogList 둘 다 전달할 수 있다.

제네릭은 공변성이 없다면서 갑자기 여기서는 왜 되는건지 의문이 들 수있다. 정답은 일종의 제한을 두었기 때문이다.

public class Display {
    public void displayAnimals(List<? extends Animal> animals) {
        animals.forEach(
            Animal::sound
        );
    }
}

 

? extends Animal의 제한은 전달받은 animals는 생산만 한다는 것이다. 즉 animals는 데이터를 주기만 한다.

실제로 displayAnimals 메서드 body는 단순히 animals를 순회하면서 데이터를 사용하기만 한다.

 

displayAnimals로 전달되는 animals는 Animal 혹은 Animal의 하위 클래스이기 때문에 Animal, Cat, Dog

그 무엇이 오든 Animal로 안전하게 캐스팅이 가능하다. 

 

생산만 가능하기 때문에 animals에 add를 하는 것은 안된다. 즉 animals가 외부로부터 데이터를 받을 수(소비) 없다는 뜻이다.

displayAnimals가 호출되는 시점에서는 전달되는 animals가 Cat이 올 지, Dog가 올 지, Animal이 올 지 모른다.

따라서 add / 데이터를 소비하는 것은 허용되지 않는다.

 

핵심은 전달 된 animals를 생산만 시킨다면 전혀 문제가 없다는 것이다.

animals의 모든 원소들은 Animal로 캐스팅이 가능하기 때문이다.

이렇게 소비를 제한하면 List<Animal> -> List<Cat> / List<Animal> -> List<Dog> 공변성을 적용시킬 수 있다.

 

정리해보자면 ? extends T는 전달되는 제네릭 객체를 생산만 시킬꺼니까 공변성을 적용시키겠다는 뜻이다.

따라서 앞으로 ? extends T가 사용 된 메서드를 본다면 생산을 하는 제네릭이라고 생각하자.

? super T는 반공변이다.

반공변은 말 그대로 공변의 반대이다.

예를 들어 Animal(부모) -> Cat(자식) 관계면 오히려 Type<Cat> <- Type<Animal> 관계가 된다는 것이다.

 

단순히 예시를 먼저 설명해보자면 동물과 고양이는 서로 부모 - 자식 관계이다. 그리고 동물 의사와 고양이 의사가 있다.

 

고양이 의사는 고양이를 치료할 수 있다.

동물 의사는 강아지도 치료할 수 있고 고양이도 치료할 수 있다.

따라서 동물 의사는 고양이 의사를 대체할 수 있다.

 

위 상황은 반공변성을 표현할 수 있는 예시이다. 동물 - 고양이 타입을 사용하는 의사들 간의 관계는 동물 의사가 고양의 의사를

대체할 수 있게 되었다.

public class A {
}

public class B extends A {
}

public class C extends B {
}

 

만약 A -> B -> C 관계가 있다고 가정하자.

public static void wildcardSuper(List<? super C> list) {
}

 

? super C는 C 혹은 그 상위 클래스만 전달이 가능하다는 의미이다. 따라서 wildcardSuper는 C, B, A, Object List를 받을 수 있다.

public class Main {
    public static void main(String[] args) {
        wildcardSuper(new ArrayList<Object>());
        wildcardSuper(new ArrayList<A>());
        wildcardSuper(new ArrayList<B>());
        wildcardSuper(new ArrayList<C>());
    }

    public static void wildcardSuper(List<? super C> list) {
    }
}

 

따라서 위와 같이 Object, A, B, C를 전달할 수 있다. 근데 여기서 이제 뭐 어쩌라고가 나올 수 있다.

? super T는 T 혹은 그 상위 클래스 타입만 올 수 있게 제한한다는 것은 알았다. 근데 이걸 뭐 어떻게 활용하는 건지는 이해하기 쉽지 않다.

class LegoBlock {
    @Override
    public String toString() {
        return "레고블록";
    }
}

class RedLegoBlock extends LegoBlock {
    @Override
    public String toString() {
        return "빨간색 레고블록";
    }
}

 

레고 블럭과 레고 블럭을 상속하는 빨간 레고 블럭이 있다고 가정하자.

public static void putRedBlocks(List<? super RedLegoBlock> destination) {
    destination.add(new RedLegoBlock());
}

 

그리고 ? super RedLegoBlock을 통해 RedLegoBlock, LegoBlock, Object List를 받을 수 있도록 범위를 지정하였다.

public class Main {
    public static void main(String[] args) {
        List<LegoBlock> allLegoBox = new ArrayList<>();
        putRedBlocks(allLegoBox);
    }

    public static void putRedBlocks(List<? super RedLegoBlock> destination) {
        destination.add(new RedLegoBlock());
    }
}

 

근데 생각해보면 LegoBlock 박스에는 빨간 블록도 들어갈 수 있다.

putRedBlocks는 단지 RedLegoBlock을 처리해줄 수 있는 박스를 원할 뿐이다. 즉 소비자를 찾을 뿐이다.

 

즉 super wildcard는 단지 자신을 처리해줄 수 있는, 소비해 줄 수 있는 대상을 찾는다.

 

또한 LegoBlock -> RedLegoBlock 관계이지만 LegoBlock을 소비하는 Box 입장에서는 LegoBlock이 RedLegoBlock을

담을 수 있는 관계 즉 반공변성 관계가 된다.

정리

정리하면서 느낀 것은 제네릭의 extends와 super를 너무 공변성 / 반공변성으로 연관지으려고 하면 더 어려워지는 것 같다.

 

extends의 경우는 전달되는 제네릭 타입을 안정적으로 읽기 위해 범위를 제한 두는 것

super의 경우는 전달되는 제네릭 타입이 내가 원하는 것을 안정적으로 처리해줄 수 있게 범위를 제한 두는 것

 

위와 같이 이해하는 것이 더 쉽게 받아들여질 것 같다.

'java' 카테고리의 다른 글

[JAVA] GC, G1GC  (0) 2025.06.24
[JAVA] Reflection - 리플렉션  (0) 2025.05.24
'java' 카테고리의 다른 글
  • [JAVA] GC, G1GC
  • [JAVA] Reflection - 리플렉션
e4g3r
e4g3r
e4g3r 님의 블로그 입니다.
  • e4g3r
    e4g3r 님의 블로그
    e4g3r
  • 전체
    오늘
    어제
    • 분류 전체보기 (39)
      • spring (22)
      • kotlin (6)
      • java (3)
      • database (3)
      • cs 공부 기록용 (5)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
e4g3r
[JAVA] 제네릭 공변, 반공변
상단으로

티스토리툴바