지네릭 - 공변성과 불공변성
변성
이란 상속 계층 관계에서 서로 다른 타입 간에 어떤 관계가 있는지 나타내는 지표다. 공변성
은 서로 다른 타입간에 함께 변할 수 있다는 특징을 말한다. 관련된 객체 지향 개념으로는 리스코프 치환원칙이 있다.
S가 T의 하위타입
인 경우를 기준으로 공변, 반공변, 불공변이 어떤 차이가 있는지 알아보자.
- 공변성 : S 는 T 의 하위 타입이다.
ex)List<S>
는List<T>
의 하위 타입이다.
String 이 Object의 서브타입이면,List<String>
은List<? extend Object>
의 서브타입이다. - 반공변 : T 는 S 의 하위 타입이다. (공변의 반대)
ex)List<T>
는List<S>
의 하위 타입이다.
String 이 Object의 서브타입이면,List<Object>
은List<? super String>
의 서브타입이다.
- 불공변 : S 와 T 는 서로 관계가 없다.
ex)List<S>
와List<T>
는 서로 다른 타입이다. (관계가 없음)List<String>
은List<Object>
의 하위타입이 아니다.
예시 코드
// 공변성
Object[] Covariance = new Integer[10];
// 반공변성
Integer[] Contravariance = (Integer[]) Covariance;
// 공변성
ArrayList<Object> Covariance = new ArrayList<Integer>();
// 반공변성
ArrayList<Integer> Contravariance = new ArrayList<Object>();
배열과 달리 자바의 제네릭에서는 공변성/반공변성을 지원하지 않는다.
따라서 ArrayList 코드는 컴파일 에러가 발생한다.
Object parent = new Object();
Integer child = new Integer(1);
parent = child;
제네릭은 공변성이 없다!
다형성
Java는 객체지향 언어로 객체 타입은 상하 관계가 있다.
Object 타입으로 선언한 변수에는 String 타입으로 선안한 변수를 대입할 수 있다. (업캐스팅)
Object parent = new Object();
Integer child = "child";
parent = child;
다음 예제를 보면 제네릭 타입이 같으면 객체 간에 상하관계가 유효한 것을 확인할 수 있다.
Collection<String> parent = new ArrayList<>();
ArrayList<String> child = new ArrayList<>();
parent = child; // 다형성 (업캐스팅)
제네릭 타입은 상하관계 없음
ArrayList<Object> parent = new ArrayList<>();
ArrayList<String> child = new ArrayList<>();
parent = child; // ! 업캐스팅 불가능
child = parent; // ! 다운캐스팅 불가능
공변성이 없어 발생하는 문제점
메서드의 매개변수으로 지네릭을 사용할 경우 외부에서 넘겨주는 파라미타의 캐스팅 문제가 발생할 수 있다.
매개변수가 배열과 List인 상황의 예시로 비교해 보자.
배열 예시
public static void print(Object[] arr) {
for (Object e : arr) {
System.out.println(e);
}
}
public static void main(String[] args) {
Integer[] integers = {1, 2, 3};
print(integers); // [1, 2, 3]
}
List 예시
public static void print(List<Object> arr) {
for (Object e : arr) {
System.out.println(e);
}
}
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3);
print(integers); // ! Error
}
와일드카드
지네릭의 공변성 관련 문제는 와일드카드로 해결할 수 있다.
와일드카드는 ?
로 사용하며, <?>
로 사영하면 Object와 동일한 의미를 가진다. 일반적으로 와일드 카드의 타입 범위를 제한하기 위해 extends
,super
키워드와 함께 사용한다.
<?>
: 제한 없음 (모든 타입 가능)<? extends U>
: 상위 클래스 제한 (상한 경계, U와 자손들만 가능) -> 공변성- EX )
ArrayList<? extenes Number> list = new ArrayList<>();
: 지네릭 타입으로 Number 자손타입 대입 가능
- EX )
<? super U>
: 하위 클래스 제한 (하한 경계, U와 조상들만 가능) -> 반공변성- EX )
ArrayList<? super Number> list = new ArrayList<>();
: 지네릭 타입으로 Number 부모타입 대입 가능 - 상위타입 데이터를 대입하는 경우 사용한다.
- EX )
와일드카드가 필요한 예
class MyArrayList<T> {
Object[] element = new Object[5];
int index = 0;
// 외부로부터 리스트를 받아와 매개변수의 모든 요소를 내부 배열에 추가하여 인스턴스화 하는 생성자
public MyArrayList(Collection<T> in) {
for (T elem : in) {
element[index++] = elem;
}
}
// 외부로부터 리스트를 받아와 내부 배열의 요소를 모두 매개변수에 추가해주는 메서드
public void clone(Collection<T> out) {
for (Object elem : element) {
out.add((T) elem);
}
}
@Override
public String toString() {
return Arrays.toString(element); // 배열 요소들 출력
}
}
public static void main(String[] args) {
// MyArrayList의 제네릭 T 타입은 Number
MyArrayList<Number> list;
// MyArrayList 생성하기
Collection<Integer> col = Arrays.asList(1, 2, 3, 4, 5);
list = new MyArrayList<>(col); // ! ERROR
// MyArrayList 출력
System.out.println(list);
}
- Integer는 Number를 상속하여 둘은 상속관계를 가진다. 하지만 제네릭의 타입 파라미터는 기본적으로 불공변성이라 컴파일 에러가 발생한다.
- 해당 문제를 해결하기 위해서는 파라미터
Collection<T>
부분을Collection<? extends T>
로 수정해야 한다.
와일드카드 주의사항!
super
- 데이터를 꺼낼 때 어떤 타입이 나올지 모름 (super 사용시 Object 타입으로 꺼내는 이유)
- 데이터를 넣을 때 T 의 자손 데이터만 넣어야 함. 어떤 타입이 오더라도 꺼낼 수 있도록 하기 위함
PECS 공식
지네릭 메서드
https://ecsimsw.tistory.com/entry/%EC%9E%90%EB%B0%94-%EC%A0%9C%EB%84%A4%EB%A6%AD-%EB%A9%94%EC%86%8C%EB%93%9C
https://nozeroslope.tistory.com/285
댓글