(Effective Java) 5장 제네릭
[!info] 제네릭의 본질은, 런타임에
ClassCastException
이 발생하는 것을 막아주어 타입 안전성을 높이는데 있다.
아이템 26. raw 타입은 사용하지 마라 (제네릭 쓸 때 <>를 빼먹지 않도록 주의해라)
- raw타입이란
List<>
가 아니라 그냥List
로, 제네릭을 타입 지정 없이 쓰는 것을 말한다.- 이걸로 받으면 아무 타입이나 담을 수 있게 된다. 이는 타입 안전성을 해치게 된다.
- 예를 들어 List로 캐스팅해서 쓰면,
List<String>
에 Integer를 넣을 수 있다!
1
2
3
4
5
6
List<String> a = new ArrayList<>();
a.add("qwer");
List b = a;
b.add(Integer.valueOf(333));
System.out.println(b.get(0));
System.out.println(b.get(1));
List<Object>
와List
는 다르다. 이는 변성을 지정하지 않으면List<String>
등과 호환 되지 않는다.- 어떤 제네릭 타입이든 받고 싶은 경우 비한정적 와일드카드
List<?>
를 사용한다.- 여기에는 null 말고 어떤 원소도 넣을 수 없다는 점이 그냥 List와의 차이점이다.
- 원소를 넣을 수 없으니 타입 안전성을 해치지 않는다.
- raw 타입을 사용해야 하는 경우는 크게 두 가지다.
List.class
이를 타입 토큰(type token)이라 부른다. <>를 쓰면 접근이 안된다.o instanceof List
로 검사해야 하는 경우. 런타임에는 제네릭 타입 정보가 지워지기 때문에. 이 때 검사 후 형변환은 그냥 List가 아니라List<?>
로 해야 한다!
아이템 27. 비검사 경고를 제거하라
- 비검사 경고란
warning: [unchecked]
를 말하는데 casting할 때 검사 안했다고 뜨는 경고.
아이템 28. 배열보다는 리스트를 사용하라
- 제네릭은 무공변이나, 배열은 공변이다.
- 배열은 다른 타입을 넣으려고 할 때, 런타임에 에러가 발생하지만 제네릭은 컴파일 타임에 발생해서 좀 더 안전하다.
- 실체화 불가 타입에 대한 배열은 생성할 수 없다.
- non-reifiable type
new List<E>[], new List<String>[], new E[]
는 컴파일 타임에 generic array creation 에러가 발생한다.- 이런 타입들을 실체화 불가 타입(non-reifiable type) 이라고 한다. 런타임에 타입 정보가 없어지는 타입들.
- 코틀린에는 reified 키워드가 존재한다.
- [Coding/Kotlin Java] - [Kotlin] 제네릭 : 타입 파라미터 소거(erasure), 실체화(reified)
- 제네릭 배열이 허용된다고 가정하면 런타임에 ClassCastException이 발생할 수 있는데, 이는 컴파일 타임에 타입 에러를 체크하겠다는 제네릭의 취지와 어긋나기 때문이다.
- 단,
E[]
는 사용해야 하는 경우 우회해서 사용하기도 한다. (아이템 29)
아이템 29. 이왕이면 제네릭 타입으로 만들라 (제네릭 배열 우회 생성 방법)
- 유틸리티 클래스(Stack 등) 만들 때는 제네릭 타입으로 만드는 것이 좋다.
- 내부에서 가지고있는 컨테이너 변수는 제네릭 배열을 사용해도 되고, 제네릭 리스트를 사용해도 된다.
- 아이템 28에서 배열 보다는 리스트를 사용하라고 했지만, 경우에 따라 리스트를 사용할 수 없는 경우도 있다.
- 성능은 배열이 우세하다. 실제로 HashMap같은건 성능 때문에 내부적으로 배열을 쓴다.
- ArrayList 같은 제네릭 타입을 만드는 경우에도 결국은 기본 타입인 배열을 사용해야 한다.
- 아이템 28에서 배열 보다는 리스트를 사용하라고 했지만, 경우에 따라 리스트를 사용할 수 없는 경우도 있다.
- 제네릭 배열 우회 생성 방법1 17번 라인에서 캐스팅해주는 이유는, E는 실체화 불가 타입이라 new E[1]이 불가능하기 때문.
- 제네릭 배열 우회 생성 방법2 선언은 Object로 하고 28번 라인에서 쓸 때 (E)로 캐스팅.
- 1이 좀 더 보편적인 방법이다.
- 1은 가독성이 좋다.
E[]
타입으로 선언하기 때문에 확실히 알 수 있다. - 1은 캐스팅도 생성해서 담을 때 한 번만 해도 된다. 반면 2는 접근 시 마다 캐스팅해줘야 한다.
- 경우에 따라서 1은 배열의 런타임 타입이 컴파일 타입과 달라 힙 오염(아이템32)을 일으킬 가능성도 있다.
아이템 30. 이왕이면 제네릭 메서드로 만들라
- 제네릭 싱글턴 팩터리 패턴
- GenericSingletonFactory.java
- identityFunction()을 호출할 때 type을 명시해주어 해당 타입으로 캐스팅한 결과를 리턴해준다.
- 제네릭 싱글턴 팩터리를 안쓴다면?
- 쓸 때 마다 매번 바깥에서 캐스팅 해주거나,
UnaryOperator<String> IDENTITY\_FN_STR
이런 식으로 타입을 지정해서 변수를 다 만들어놔야 한다.
- GenericSingletonFactory.java
- 재귀적 타입 한정
- 예제)
<E extends Comparable<E>>
- “Comparable
를 구현한 클래스만 E가 될 수 있다." - 근데 사실, 이 보다는
<E extends Comparable<? super E>>
를 쓰는 편이 좋다.(아이템 31)
- 예제)
아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높여라
- [Coding/Kotlin Java] - [Kotlin] 제네릭 : 변성(variance), 타입 프로젝션(type projection)
- 생산자나 소비자용 입력 파라미터에
<? extends|super T>
를 잘 활용하라는 내용.- 제네릭은 기본적으로 무공변이니까. 공변성을 지정해서 더 유연하게 만들어 줘라.
- 와일드카드 적용 원칙 PECS : T의 producer라면-extends, consumer라면-super
- 생산이란 T 인스턴스를 [조건에 맞는걸 찾아 반환하거나, 내부 컬렉션에 추가하거나] 하는 등의 활동을 의미한다. T 인스턴스를 새로 생성한다는 의미가 아니다.
- 반대로 소비란 [내부의 T 인스턴스를 외부 컬렉션에 추가] 하는 등의 활동을 의미한다.
1
2
3
4
5
6
7
8
9
class AnimalList<T> {
ArrayList<T> animals = new ArrayList<>(); // T can be Tiger
public AnimalList(Collection<? extends T> producer) {
animals.addAll(producer); // producer can be Collection<WhiteTiger>
}
public void export(Collection<? super T> consumer) {
consumer.consume(animals); // consumer can be Collection<Animal>
}
}
- 주의) 리턴 타입을 적을 때는 extends, super 등 한정적 와일드카드를 쓰면 안된다. 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문.
- 타입 매개변수 T 자체를 제한하는건 괜찮음.예제
- 비교 가능한(Comparable) 클래스만 E가 될 수 있도록 하려면, 이렇게 쓴다.
<E extends Comparable<? super E>> E
- super를 안쓰고 그냥
<E extends Comparable<E>>
로 지정하는 경우, Comparable을 간접적으로 상속하는 케이스는 실제로는 비교 가능하지만 E가 될 수 없다. - 간접적으로 상속한다는건 Comparable을 상속한 다른 인터페이스를 구현하는 경우. Comparable이 조부모가 됨.
- Comparator도 마찬가지다.
아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
- 가변인수로 제네릭을 받는 경우 힙 오염(heap pollution)이 발생할 수 있음.예시
- 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 하나 만들어진다.
- 실체화 불가 타입을 가변 인수로 받는 경우(
List<String>...이나 T...
) 실체화 불가 타입에 대한 배열이 만들어진다는 뜻이다. - 제네릭 등 실체화 불가 타입의 배열은 원래는 불가하다. 타입 안전성이 깨질 수 있기 때문에.
- 여기서 타입 안전성이 깨진다는건 “런타임에
ClassCastException
이 발생할 수 있다.”를 의미 - 그래서 컴파일 타임에 경고로 알려준다.
- 여기서 타입 안전성이 깨진다는건 “런타임에
- 제네릭 배열을 직접 만드는 것은 막아 두었으면서, 가변인수에서 생성하는건 경고로 끝내는 이유는?
T...
같은게 실무에서 매우 유용하기 때문!- 실제로
List.of(E...), Arrays.asList(T... a)
등 자바 라이브러리도 이런 메서드를 여럿 제공한다.
- 위에서 컴파일 타임에 경고가 발생한다고 했는데, 이런 메서드에 대한 경고는 본적이 없다. 이는
@SafeVarargs
애너테이션을 붙여 주었기 때문이다. - 규칙 : 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드를 정의할 때는 @SafeVarargs를 붙여준다.
- 안전하지 않아도 붙이라는 뜻이 아니라, varargs 메서드를 만들거면 무조건 안전하게 만들라는 뜻이다.
- 안전한 제네릭 varargs 메서드란 다음 두 조건을 만족하는 것을 말한다.
- varargs 매개변수 배열에 아무것도 저장하지 않는다.
- 그 배열(혹은 복제본)을 외부에 노출하지 않는다. (메서드 내에서만 사용하고 리턴하거나 전달하지 않는다.)
- 안전하지 않은예시
- 여기서 toArray()에 String을 넘기면서 리턴 타입이 String[]일 것으로 예상하겠지만, 리턴 타입은 Object[] 다.
- 그래서 attribute에 담으면서
ClassCastException
이 발생한다.
- 그래서 attribute에 담으면서
- toArray가 T…를 받는데 이는 사실 배열로 넘어간다 . 그래서 컴파일러는 pickTwo에 T 인스턴스를 담을 배열을 만드는 코드를 생성하고, 이 배열을 toArray에 전달하게 된다.
- 이 때 이 배열이 Object[]타입이 되는데, pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이 Object[]이기 때문이다.
- pickTwo에서
<T extends String>
으로 제한을 해주면 이 args의 타입이 String[]으로 바뀐다!
- pickTwo에서
- 그래서 args는 Object[] 타입이 되고, 이걸 그대로 리턴하면 Object[] 타입이 리턴된다.
- 여기서 toArray()에 String을 넘기면서 리턴 타입이 String[]일 것으로 예상하겠지만, 리턴 타입은 Object[] 다.
아이템 33. 타입 안전 이종 컨테이너를 고려하라
- type safe heterogeneous container pattern
- 원래 자바같은 언어에서
Map<K, V>
같은걸 생각해보면 한 컨테이너에 한 가지 타입의 변수들만 담을 수 있다. - 타입 안전 이종 컨테이너는 여러 타입을 담을 수 있는 컨테이너를 말한다. 타입 안전하게!
Map<Class<?>, Object>
이런 식으로 컨테이너 키를 타입으로 사용하고, 불러올 때도 타입을 지정해서 불러오는 식이다.- 예시
- Class 정보를 사용하는 방식이기 때문에
List<String>
같은건 저장할 수 없다.List나
List<String>
이나 같은 Class 이기 때문에 구분이 안된다. - super type token을 이용하면
List<String>
같은 것도 저장 가능하긴 한데, 썩 완벽하진 않다.- 스프링의 ParameterizedTypeReference가 바로 super type token이다.
- Class 정보를 사용하는 방식이기 때문에
- 가끔 쓸일이 있을 것 같다. 실제로 클래스에 붙이는 애너테이션은 interface로 되어 있기 때문에 타입 안전 이종 컨테이너에 타입을 키로 해서 보관된다.예시
getAnnotation(Class<T>)
같은 메서드를 호출하면 타입으로 컨테이너를 뒤져서 반환해준다.Class<?>
를Class<? extends Annotation>
으로 형변환 해야 하는 경우, 당연히 경고가 발생하므로.asSubclass()
를 이용한다.
java.util.Collections.checkedSet, checkedList, checkedMap
같은 녀석들이 타입 안전 이종 컨테이너 방식으로 구현된 컬렉션의 래퍼인데, 제네릭과 raw 타입을 섞어 사용하는 어플리케이션에서 실수하지 않도록 도와준다.
+추가1. 클래스 내에서 T가 전역적으로 쓰일게 아니라면, Method T가 낫다.
1
2
3
4
5
6
7
@Component
public class CyberSourceClient {
public <T> void runTransaction(T request) {
// 여기서 Class T를 쓸지 Method T를 쓸지 고민이 좀 됐는데, 클래스 내에서 T가 전역적으로 쓰일게 아니라면, generic 범위는 작은 것이 좋지.
// 사용하는 측에서도 method T가 사용하기 더 편하고. class T는 각 타입 마다 각각 다른 CyberSourceClient 사용해줘야 하니까
// 더군다나 <T, R> 이라면? class T,R은 경우의 수가 너무 많아 난감해진다.
This post is licensed under CC BY 4.0 by the author.