이 글에서는 Generics에서 Covariance, Contravariance의 개념을 설명하고 어떤 상황에서 이런 개념을 사용하는지 설명하려고 합니다.
설명하기 전에, 다음 용어들의 의미가 무엇인지 알아야 합니다.
- Invariance (변함없는)
- Covariance (변하는)
- Contravariance (반대로 변하는)
그리고, 위의 개념을 코드로 설명할 때 다음 클래스들을 이용할 것입니다. 이 클래스들은 자바에서 기본으로 제공하는 클래스입니다.
public abstract class Number { }
public final class Double extends Number { }
public final class Integer extends Number { }
public final class Long extends Number { }
Invariance
Invariance는 변하지 않는 성질을 의미합니다.
자바에서 다음 코드는 매우 자연스럽습니다. Double은 Number를 상속하기 때문에 Number number = numDouble
처럼 할당이 가능합니다.
Double numDouble = 1.1;
Number number = numDouble;
System.out.println(number);
하지만 다음 코드는 컴파일 에러가 발생합니다. Double이 Number를 상속한다고 해서 List<Double>
이 List<Number>
를 상속한다고 말할 수 없기 때문입니다.
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
List<Number> numbers = doubles; // compile error
즉, 클래스의 상속관계가 Generics에서는 상속관계로 유지되지 않는 것을 Invariance라고 합니다.
List<Double>
이 List<Number>
를 상속하지 않는 이유는 Generics는 컴파일 단계에서 Generics의 타입이 지워지기 때문입니다.
JVM은 Runtime에 List 객체만 알고 있고, Generics의 타입이 Double인지, Number인지 알지 못합니다.
Covariance
Covariance는 변하는 성질을 의미합니다. 클래스의 상속관계가 Generics에서도 상속관계가 유지되는 것을 말합니다.
Generics 객체를 선언할 때 Covariance를 설정하면 Generics에서도 상속관계가 유지됩니다.
Covariance 설정은 <T>
대신에 <? extends ParentClass>
처럼 입력하면 됩니다.
만약 다음 관계가 성립한다면,
Number number = new Double(1.1);
다음 관계도 성립한다는 것을 의미합니다.
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
List<? extends Number> numbers = doubles;
하지만, Covariance를 이용하여 객체를 할당하면, 객체를 read할 수 있지만 write는 어렵습니다.
다음 코드에서 get()
은 문제없지만 add()
는 컴파일에러가 발생합니다.
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
List<? extends Number> numbers = doubles; // ok
Number number = numbers.get(0);
System.out.println(number);
numbers.add(1.1); // compile error
get()
이 문제 없는 것은 쉽게 알 수 있습니다. get()
으로 리턴된 객체는 Number를 상속하는 Long, Integer, Dobule 중에 하나이고, Number로 할당받을 수 있습니다.
하지만, add()
는 조금 다릅니다. 다음과 같이 List의 타입이 정해져있지 않아서 add()
로 특정 클래스의 객체를 추가할 수 없습니다.
// compile error, List<>의 타입이 Double인지 알 수 없음
numbers.add(1.1);
// compile error, List<>의 타입이 Integer인지 알 수 없음
numbers.add(1);
// compile error, List<>의 타입이 Long인지 알 수 없음
numbers.add(1L);
Contravariance
Contravariance는 반대로 변하는 성질입니다. 클래스의 상속관계가 Generics에서는 반대인 것을 것을 의미합니다.
Contravariance는 <T>
대신에 <? super ParentClass>
으로 설정할 수 있습니다.
만약 다음 관계가 성립한다면,
Number number = new Double(1.1);
다음 관계도 성립한다는 것을 의미합니다.
여기서 list
는 Double 또는 Double을 상속하는 클래스의 객체가 저장될 수 있습니다.
List<Number> numbers = Arrays.asList(1.1, 2, 3L);
List<? super Double> list = numbers;
다음 코드에서 numbers.add()
는 컴파일은 가능하지만, numbers.get()
은 컴파일 에러가 발생합니다.
List<Number> numbers = Arrays.asList(1.1, 2, 3L);
List<? super Double> list = numbers;
Double number = list.get(0); // compile error
list.add(new Double(4));
get()
이 어려운 이유는, 리턴되는 값이 Double이거나 Double을 상속하는 클래스의 객체가 될 수 있기 때문입니다.
따라서 특정 타입으로 할당이 어렵습니다.
add()
는 컴파일은 되지만 Runtime에 UnsupportedOperationException 에러가 발생합니다.
그 이유는 list가 가리키는 객체의 타입은 List<Number>
이고, 이미 타입이 결정되어 Double 객체가 추가 될 수 없는 것 같습니다.
만약 다음과 같이 위의 코드에서 numbers를 ArrayList<Object>
로 변경하면 컴파일도 성공하고 리스트에 객체를 추가하는 것도 성공합니다.
List<Object> numbers = new ArrayList<Object>();
List<? super Double> list = numbers;
numbers.add(new Double(4.4));
Covariance, Contravariance를 함께 사용하는 예제
dzone의 예제를 가져왔습니다.
다음 예제는 두가지 성질을 모두 이용하여 List에 아이템을 copy합니다.
static void copy(List<? extends Number> source, List<? super Number> destiny) {
for(Number number : source) {
destiny.add(number);
}
}
List<Integer> myInts = Arrays.asList(1,2,3,4);
List<Double> myDoubles = Arrays.asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);
for (Object obj : myObjs) {
System.out.println(obj);
}
결과
1
2
3
4
3.14
6.28
정리
Java의 Generics는 Invariance입니다. 상속관계에 따라 Generics 객체를 서로 할당 가능하도록 설정하는 개념이 Covariance와 Contravariance입니다.
Invariance를 이해할 때는 Java의 Generics는 컴파일 될 때 타입이 결정되고 Runtime에 타입을 알 수 없다는 것을 연관지어 이해하면 좋을 것 같습니다.
학습하다보면 혼란스럽기 때문에 이해하고 싶지 않은데요. 간혹 Generics를 사용할 때 이 두개의 개념이 필요할 때가 있습니다. 개인적으로 Contravariance보다 Covariance를 더 많이 사용하는 것 같습니다.
참고
Related Posts
- Java - Unsupported class file major version 61 에러
- Java - String.matches()로 문자열 패턴 확인 및 다양한 예제 소개
- Java - 문자열 공백제거 (trim, replace)
- Java - replace()와 replaceAll()의 차이점
- Java - ArrayList 초기화, 4가지 방법
- Java - 배열 정렬(Sorting) (오름차순, 내림차순)
- Java - 문자열(String)을 비교하는 방법 (==, equals, compare)
- Java - StringBuilder 사용 방법, 예제
- Java - 로그 출력, 파일 저장 방법 (Logger 라이브러리)
- Java IllegalArgumentException 의미, 발생 이유
- Java - NullPointerException 원인, 해결 방법
- Seleninum의 ConnectionFailedException: Unable to establish websocket connection 해결
- Java - compareTo(), 객체 크기 비교
- Java - BufferedWriter로 파일 쓰기
- Java - BufferedReader로 파일 읽기
- Java charAt() 함수 알아보기
- Java - BigInteger 범위, 비교, 연산, 형변환
- Java contains()로 문자(대소문자 X) 포함 확인
- Java - Set(HashSet)를 배열로 변환
- Java - 문자열 첫번째 문자, 마지막 문자 확인
- Java - 문자열 한글자씩 자르기
- Java - 문자열 단어 개수 가져오기
- Java - 1초마다 반복 실행
- Java - 배열을 Set(HashSet)로 변환
- Java - 여러 Set(HashSet) 합치기
- Java - 명령행 인자 입력 받기
- Java - 리스트 역순으로 순회, 3가지 방법
- Java - 특정 조건으로 리스트 필터링, 3가지 방법
- Java - HashMap 모든 요소들의 합계, 평균 계산
- Java - 특정 조건으로 HashMap 필터링
- Java - 싱글톤(Singleton) 패턴 구현
- Java - 숫자 왼쪽에 0으로 채우기
- Java - String 배열 초기화 방법
- Java - 정렬된 순서로 Map(HashMap) 순회
- Java - HashMap에서 key, value 가져오기