자바의 가변성에는 크게 공변, 무공변, 반공변이 존재한다.
제네릭을 잘 사용하려면 이 가변성에 대한 이해가 필요하다.
변성을 제대로 이해하려면 "타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 하위 타입인가?" 라는 질문에서 시작하는게 좋다.
배열은 공변, 제네릭은 무공변이 기본이라고 다들 알고 있을 것이다.
기본적으로 제네릭은 무공변이다.
무공변이라고 하니 헷갈리는것 같다. 사전적으로 번역해보면 불공변으로 나오게 된다.
타입 S가 T의 하위 타입일 때, Box[S]와 Box[T] 사이에 상속 관계가 없는 것
쉽게 말하면 너는너, 나는 나 인 느낌이다.
그래서 선언한 유형만 들어갈 수 있게 코드를 구성할 수 있다.
Object
에는 Object
만, String
에는 String
만 들어갈 수 있단 얘기이다.
void invariance() {
// 제네릭은 기본적으로 무공변
List<Object> objectList = new ArrayList<>();
List<String> stringList = new ArrayList<>();
objectList.add(1);
objectList.add(1.0);
stringList.add("aaaaa");
}
공변(covariance)는 타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 하위 타입 임을 나타내는 개념
@Test
void arrayTest() {
Object[] arr = new Long[5]; //배열에서는 공변이고, Long은 Object의 하위타입이기에 할당이 가능하다.
arr[0] = "arr"; //공변으로 인해 선언한 arr은 Object로 참조가 된상태라 String도 할당 가능.
// 여기서 런타임에 ArrayStoreException 발생
}
자바에서 이 배열을 공변으로 열어두지 않았다면, 다형성의 이점을 살릴 수 없게 됐을 수 있다.
Arrays.swap()
Arrays의 메소드를 하나를 가져와봤는데,
만약 공변이 아니었다면, 이 배열 스왑 메소드는 객체별로 전부 구현해주어야 했을 것이다.
제네릭이 있기전엔 형변환에 대한 에러가 나더라도,
다형성의 장점으로 얻을 수 있는 이득이 많았을 것 같다.
리스트의 공변
void variance() {
List<? extends Object> list = new ArrayList<Number>();
list.add(1); //컴파일 에러
list.add(1.0); //컴파일 에러
list.get(0); // 정상 로직
}
이처럼 선언을 했을때 add는 선언된 제네릭으로 변수를 넣게 되어있는데,
무공변으로 만들었을 경우
공변인 경우
위와 같은 경우에는 capture of ? extends Object e
Object의 하위타입은 맞지만,
어떤 타입인지는 모른다? 라는 뜻이라고 생각된다.
그래서 list.get(0)
이 Object로 형변환은 가능하지만, 반대로 add()
를 통해 null을 제외한 무언가를 추가해줄 수는 없다는 소리이다. 안에 들어가는 객체가 정확하게 뭔지 모르기 때문이다.
그래서 정확한 타입이 어떤건지는 모르기 때문에 개발자가 null을 제외하고는 아무것도 추가하지 못하게 막을 수 있다라고 봐도 될 것 같다.
그래서 자주쓰던 Collections 클래스의 UnmodifiableList를 찾아보게 되었다.
생성자에 이런식으로 공변을 이용해서 막아주고 있는것을 볼 수 있었다.
그러면서 List의 구현체이기 때문에 밖에서는 add에 어떤 값을 넣어줄 수는 있기에, 그대로
Override
로 재정의 한 뒤에 Exception을 던져주게 만든것을 확인할 수 있었다.
반공변 처음 봤을때 반만 된다 이런생각을 했었다.ㅋㅋㅋㅋㅋㅋㅋ
그게 아니라 공변의 반대
타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 상위 타입 임을 나타내는 개념입니다.
@Test
void contravariance() {
List<? super Number> list = new ArrayList<>();
list.add(1.0);
final Number number = (Number) list.get(0);
final Object object = list.get(0);
}
Number
를 포함한 Number
의 상위 타입들만 들어갈 수 있게 설정한 상태이다.
아까는 하위타입이 뭔지 알 수가 없다는 것이었는데,
이 코드는 Number
상위인건 알겠는데 상위 누구인지를 알 수 없는 상태이다.
super
키워드 다음에 붙은 클래스까지의 형은 전부 넣을 수 있다는 소리와도 같다.
다시말하면, 최소 Number 타입은 보장이 된다는 소리와 같다.
그래서 list.get(0); 에서 최상 타입인 Object로 꺼내서
형에 맞는 캐스팅 or instanceof
를 통해 값을 읽어오는게 가능하다.
이렇게 자바의 가변성에 대해 알아보았다.
얼추 정리되면서 감은 잡은것 같다.
PECS(Producer Extends Consumer Super)를 보면서,
일반적으로 소비(Consume)라는게 스타크래프트의 디파일러가 저글링을 컨슘해서 저글링을 잡아먹기때문에,
스타크래프트의 컨슘
어떤 컬렉션이 만들어지는 과정이 컨슘이라고 생각하고 값을 빼내는 과정(get)이 동작한다고 알고 있었다.
반대로 생산자(Producer)는 말그대로 생산이기에 값을 생성해주는(new) or 더해주는(add) 것이 생산자로 알고 있었다.
반대로 알고있던 것이다.
컬렉션을 뒤져서 어떤 작업들을 처리 해주어야 한다면 그게 바로 컬렉션 값을 빼내(get) 뭔가를 만들기 때문에 생산자가 되어 extends
를 사용해야 한다는 것이고,
컬렉션에 값을 추가해야되면 매개변수로 주어진 값이 소비되어 컬렉션에 들어가니(add) 소비자 관점이라고 보는것 같다.
그래서 이 경우에는 super
를 사용해주면 되겠다.
휴..되게 어렵다 😇😇😇
아무튼 읽기전용으로 만들고 싶을때에는 extends
를 사용하는것.
좀더 안전하게 데이터 삽입을 하고싶다면 super
를 사용하는 것만 기억하면 될 것 같다.