티스토리 뷰
Generics는 raw-type Collection, 배열을 사용할 때의 문제를 해결하기 위해 나왔다, 그 문제가 어떤 문제일까?
바로 Runtime 시 발생할 수 있는 ClassCastException을 컴파일 에러로 잡아준다는 것이다
알고리즘 문제를 풀 때는 보통 Collection 보다 배열을 쓰게 되는데 성능 차이가 꽤 크기 때문이다
이와 반대로 웹 개발에는 배열을 쌩으로 쓰는 일은 흔치 않고 컬렉션을 사용한다
성능 차이가 나는데도 컬렉션을 쓰는 이유는 컬렉션과 배열의 차이로 기인한다
컬렉션을 사용할 때, 주로 raw-type을 사용하지 않고 Generics와 버무려서 사용하는데 Generics는 invariant, 불공변이다
제네릭을 사용하는 경우 다른 타입으로 지정하면 상속 관계와 상관없이 다른 타입이다
이 말은 String은 Object의 하위 타입이지만 List<String>은 List<Object>와 다른 타입이라는 것이다
이와 반대로 배열은 covariant 공변이다, String[]은 Object[]의 하위 타입이다
이로 인해 발생할 수 있는 문제는 Object[]로 업캐스팅하고 실제 구현체는 Object의 하위 타입으로 생성하고
런타임에 구현체로 넣어준 타입과 다른 타입을 넣으려 할 때 발생한다
Long 타입 배열로 실제 인스턴스를 만들고 String 타입을 넣으려 하면 ArrayStoreException을 던진다
Object[] objectArray = new Long[1];
objectArray[0] = "야호";
또 다른 차이점은 제네릭은 런타임 시 타입 정보가 소거된다는 점인데
제네릭이 등장하기 전 코드와의 상호 운용성 때문이라고 한다
컴파일 타임에만 타입 체크를 하고 런타임에는 List<String> -> List로 변환된다
배열은 런타임에도 타입이 유지되기 때문에 reify, 실체화된다고 표현한다
제네릭은 불공변, 배열은 공변이기 때문에 얘네를 버무려 사용할 수 없다
따라서 List<String> 타입의 배열 List<String>[], 임의의 타입 E에 대한 배열 E[] 와 같은 코드는 생성할 수 없다
컴파일러가 컴파일 시점에 배열의 실체화되는 타입을 알 수 없기 때문이다
List<String>[] stringLists = new List<String>[1];
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);
이펙티브 자바 책에 나온 예시만으로도 이해가 잘 되기 때문에 그대로 가져왔다
만약 List<String>[]이 허용된다면 List<String>[]을 만들고 Object[]로 형변환을 수행한다, 객체 조상님이라 가능하다
Object[]은 모든 걸 받아 줄 수 있으니 List<Integer>를 요소로 넣어준다
List<String> 타입의 stringList는 모든 요소의 타입을 제네릭을 이용해 String으로 지정했다
런타임 시 List로 치환된 stringList에서 요소를 꺼내 자동 형변환을 때려주어 String으로 변환하는데
stringList[0]은 List<Integer>이고, get(0)은 Integer니까 자동 형변환을 수행할 때 ClassCastException이 터져버린다
제네릭은 런타임 시 발생할 수 있는 ClassCastException을 막기 위해 나온 것이다 라는 점을 기억해보면
그래서 자바에서는 List<String>[]을 허용하지 않는다
물론 위와 같이 쌩으로는 사용할 수 없지만 이를 교묘히 피해 Generic Array를 사용하는 두 가지 방법이 있다
이런 방법도 있다는 걸 재미로 알아만 두고 사용은 자제하자
성능이 정말 중요하다면 사용할 수도 있을 듯하다
1. 내부에 Object[]를 가지고 있는 CustomArray<E> 클래스를 만든다
CustomArray에서 get으로 꺼내는 과정에 raw-type 사용으로 인한
비검사 경고를 제거하기 위해 @SuppressWarnings를 사용해야 한다
2. 같은 방식으로 CustomArray를 만들되 비검사 경고를 제거하기 위해 reflection을 사용한다
reflection으로 인한 보안 구멍, 성능 저하가 뒤따른다
이번 아이템에서 핵심은 제네릭과 배열이 어떤 차이가 있는지, 왜 둘을 비벼 먹을 수 없는지에 대해 이해하는 것이다
배열은 런타임에도 타입을 알 수 있게 실체화되고 Super-Sub 관계가 있는 공변이다
제네릭은 런타임에 타입 정보를 알 수 없는 소거 방식을 사용하고 Super-Sub 관계가 없는 불공변이다
'Java > Effective Java' 카테고리의 다른 글
[Item30] 따봉 제네릭스 (0) | 2022.03.09 |
---|---|
[Item29] 이왕이면 제네릭 써라 (0) | 2022.03.07 |
[Item27] @SuppressWarnings 알고 쓰자 (0) | 2022.03.06 |
[Item26] 타입으로 안전하게, 유연하게 (0) | 2022.03.04 |
[Item25] 자바에는 왜 top-level function이 없을까? (0) | 2022.03.01 |