그동안 얕게 알고 쓰던 자바를 정리하기 위한 포스팅 / 개인 기록용
자바, 코틀린에는 제네릭이라는 개념이 있다. 제네릭은 타입을 파라미터로 전달할 수 있도록 하기 위해 만들어진 것이다.
흔히 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 |