🙋 들어가며
제네릭에 대해서 자세하게 공부를 하다가 가변성(variance)
에 대해서 알게 되었습니다.
어떤 종류가 존재하는지 알아보고, 왜 이런 개념이 있는지 알아보겠습니다.
제네릭을 위해서 다루는 내용이므로 이해가 어렵다면 해당 글에서 예시를 통해 다뤄보도록 하겠습니다.
🔄 가변성이란?
이름에서 예상할 수 있듯이 가변성이란, 특정 타입의 객체를 다른 타입의 객체로 변환할 수 있음을 말합니다.
반대의 개념은 불변성으로, 무공변(Invariance)
으로도 부릅니다.
여기서 가변성은 공변과 반공변 두가지가 존재합니다.
어떤 객체 a와 b에 대해서, a가 b로 변환할 수 있다고 가정하겠습니다.
그러면 다음과 같이 총 3가지로 가변성과 불변성을 구분할 수 있습니다.
- 공변(covariant) : Box<a>를 Box<b>로 변환할 수 있다.
- 반공변(contravariant) : Box<b>가 Box<a>로 변환할 수 있다.
- 무공변 : Box<a>와 Box<b>는 서로 변환할 수 없다.
1️⃣ 공변(covariant)
a가 b로 변환할 수 있다면, Box<a>를 Box<b>로 변환할 수 있다.
구체적인 방향으로 타입 변환을 허용해주는 것을 의미합니다. (자기 자신과 자식 객체 허용)
자바를 사용하신 분들이라면 무언가 떠오르는 것이 있을거라고 생각합니다.
Object[] array = new Long[SIZE]; // ok..!
List<Object> list = new ArrayList<String>(); // error..!
위의 예시처럼 배열은 공변이지만, 자바에서 제네릭은 무공변입니다.
이는 배열은 구체화 타입이지만 제네릭은 비구체화 타입이기 때문입니다.
여기서 소거(erasure)에 대한 개념이 나오는데, 간단하게 정리하고 다음 글에서 자세하게 다루겠습니다. 😋
- 구체화 타입 : 런타임 시에도 자신의 타입 정보를 알고 지킨다.
- 비구체화 타입 : 타입 소거자에 의해서 컴파일 시점에서 타입 정보가 사라진다.
2️⃣ 반공변(contravariant)
a가 b로 변환할 수 있다면, Box<b>를 Box<a>로 변환할 수 있다.
추상적인 방향으로 타입 변환을 허용해주는 것을 의미합니다. (자기 자신과 부모 객체 허용)
반공변에 대한 자바에서 가장 대표적인 예시는 Comparator
인터페이스가 있습니다.
public class Item {
private final String name;
private final int price;
// 생성자, getter...
}
public class Laptop extends Item {
// 생성자, getter...
}
public class Phone extends Item {
// 생성자, getter...
}
물건의 가격을 기준으로 비교하기 위해서 compare()
를 구현하고, 바로 비교해보겠습니다.
// 예시를 위해 익명 인터페이스로 구현
Comparator<Item> comparator = (o1, o2) -> o1.getPrice() - o2.getPrice();
Laptop macBook = new Laptop("MacBook", 3_250_000);
Phone iPhone13 = new Phone("iPhone13", 850_000);
int compared = comparator.compare(macBook, iPhone13);
// MacBook is expensive than iPhone13 : true!!
System.out.println(MessageFormat.format("{0} is expensive than {1} : {2}", macBook.getName(), iPhone13.getName(), compared > 0));
물건을 기준으로 구현했는데 서브 클래스인 노트북과 핸드폰 타입에도 잘 적용되고 있습니다.
이게 반공변에 대한 대표적인 예시입니다 👍
3️⃣ 무공변(Invariance)
a가 b로 변환할 수 있지만, Box<b>와 Box<a>는 서로 변환할 수 없다.
오로지 자기 타입만을 허용하는 것을 의미합니다.
앞서 언급했듯이 자바 제네릭은 무공변입니다. 이해를 위해서 다이어그램을 보겠습니다.
Object, Number, Integer는 하나의 상속관계지만 List는 전혀 그렇지 않습니다.
List<Number>는 오직 Number만을, List<Integer>는 오직 Integer만을 지원하며 어떤 연관관계도 없습니다.
만약 관계를 형성하고 싶으면 다음과 같이 사용할 수 있습니다.
class ItemList<E, V> implements List<E> {
public void addItem(E e, V v);
}
🤔 왜 이런 개념이 존재할까?
우선 자바는 왜 제네릭은 무공변성을 선택했을까요? 그리고 왜 가변성이라는 개념이 존재할까요?
다양한 이유가 있지만 가장 중요한 이유는 바로 타입 안정성 때문입니다.
우선 제네릭을 무공변성으로 선택함으로 다음과 같은 이점이 존재합니다.
- 일관성 : 타입이 명확해진다면 동작하는데 일관성을 보장합니다.
- 신뢰성 : 의도치 않은 타입 변환을 방지해 신뢰성을 보장합니다.
- 안정성 : 타입과 관련된 문제를 컴파일 시점에 잡아서 보다 안정된 프로그램을 보장할 수 있습니다.
이런 것들이 보장된다면 코드를 통해서 쉽게 행동을 예측할 수 있고 이해할 수 있습니다.
처음 제네릭을 접했을 때, 이것은 오히려 다형성을 제한하는 것이 아닐까? 라는 생각을 한 적이 있습니다.
무공변성을 통해서 의도와 다르게 사용할 수 있는 여지를 많이 제약했기 때문입니다.
지금은 공변과 반공변이라는 개념을 통해서 확장성까지 챙길 수 있다고 생각합니다.
-reference
https://wjdtn7823.tistory.com/88
😋 지극히 개인적인 블로그지만 댓글과 조언은 제 성장에 도움이 됩니다 😋
'언어 공부 > Java' 카테고리의 다른 글
[Java] Hash란? (feat. Hash Collection) (0) | 2023.07.21 |
---|---|
[Java] equals()와 hashCode()를 같이 재정의하자! (2) | 2023.07.20 |
[Java] Optional 바르게 사용하기 (0) | 2023.06.22 |
[Java] 빌드툴 (feat. Gradle) (0) | 2023.06.05 |
[Java] Reflection (0) | 2023.01.11 |
댓글