제네릭을 사용하면 타입을 컴파일러에 알려주고 컴파일러가 알아서 형변환 코드를 추가해 준다. 5장은 이런 제네릭을 사용할 때 주의사항과 팁을 알려준다.
용어 정리
제네릭 타입
제네릭 클래스/인터페이스 : 클래스와 인터페이스 선언에 타입 매개변수가 쓰인다. (List)
제네릭 클래스/인터페이스를 통틀어 제네릭 타입이라 한다.
- 매개변수화 타입
- 각 제네릭 타입은 일련의 매개변수화 타입을 정의한다. 이름<실제 타입 매개변수> (List<String>)
- 로 타입
- 제네릭 타입을 하나 정의하면 로 타입도 함께 정의된다.
- 로 타입은 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다. (List<String>에서 List)
- 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작한다. (제네릭 없던 코드와 호환되기 위함)
한글 용어 | 영문 용어 | 예 |
매개변수화 타입 | parameterized type | List<String> |
실제 타입 매개변수 | actual type parameter | String |
제네릭 타입 | generic type | List<E> |
정규 타입 매개변수 | formal type parameter | E |
비한정적 와일드카드 타입 | unbounded wildcard type | List<?> |
로 타입 | raw type | List |
한정적 타입 매개변수 | bounded type parameter | <E extends Number> |
재귀적 타입 한정 | recursive type bound | <T extends Comparable<T>> |
한정적 와일드카드 타입 | bounded wildcard type | List<? extends Number> |
제네릭 메서드 | generic method | static <E> List<E> asList(E[] a) |
타입 토큰 | type token | String.class |
26. 로 타입 사용 금지
제네릭을 활용하면 타입의 정보가 주석이 아닌 타입 선언 자체에 녹아들게 되고 컴파일러가 해당 타입임을 인지하게 된다.
컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가해 절대 실패하지 않음을 보장하고, 이로 인해 타입 안전성이 확보된다.
로 타입 사용 금지
로 타입을 사용하면 제네릭의 안전성, 표현력을 전부 잃게 된다. 로 타입은 단지 제네릭이 나오기 이전 코드와의 호환성을 위해 만든 것이다. 원소의 타입을 몰라도 되는 로 타입을 사용하고 싶다면 List<Object>와 같이 임의 객체를 허용하는 매개변수화 타입을 사용하거나 비한정적 와일드카드 타입을 사용하는 것이 좋다.
비한정적 와일드카드 타입
제네릭 타입인 Set<E>의 비한정적 와일드카드 타입은 Set<?>다.
비한정적 와일드카드 타입을 사용하면 null 외의 어떤 원소도 넣을 수 없다. 어떤 타입인지 모르기 때문에 타입이 존재하는 값을 넣을 수 없게 해놨고, 이로 인해 타입 안전성을 지킬 수 있다. 만일 이런 제약을 받아들일 수 없다면 제네릭 메서드 (<E>Set<E>)나 한정적 와일드카드 타입(<? super E>)을 사용하면 된다.
로 타입 쓰지 말라는 규칙의 예외
- class 리터럴에는 로 타입을 써야 한다.
- List.class, String[].class, int.class와 같이 사용해야 한다. (자바 명세)
- instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.
- 런타임에는 제네릭 타입 정보가 지워지기 때문
- instanceof은 로타입, 비한정적 와일드카드 타입이든 똑같이 동작하기 때문에 로 타입을 쓰는게 깔끔하다.
27. 비검사 경고 제거
제네릭을 사용하면 비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입 경고, 비검사 변환 경고 등과 같이 많은 컴파일러 경고가 나온다. 이런 경고는 런타임에 ClassCastException을 일으킬 수 있는 잠재적인 가능성을 뜻한다. 가능한 모든 비검사 경고를 제거한다면 코드는 타입 안전성이 보장된다.
@SuppressWarnings("unchecked")
경고를 제거할 수 없지만 타입 안전하다고 확신할 수 있으면 이 애너테이션을 달아 경고를 숨길 수 있다.
@SuppressWarnings는 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다. 하지만 이 애너테이션은 항상 가능한 좁은 범위에 적용해 심각한 경고를 놓치지 않아야 한다.
한 줄이 넘는 메서드나 생성자에 달린 @SuppressWarnings 애너테이션은 지역 변수 선언으로 옮겨야 한다. return에 달고 싶다면 반환값을 담을 지역변수를 선언해 변수에 달면 된다.
또, 애너테이션을 사용해 경고를 무시한다면 안전한 이유를 주석으로 남겨야 한다.
28. 배열보다 리스트 사용
배열과 제네릭 차이
- 배열은 공변이고 제네릭은 불공변이다.
- Sub가 Super의 하위 타입이면 Sub[]는 Super[]의 하위 타입이다.
- List<Type1>과 List<Type2>는 서로 상하위 타입이 아닌 다른 타입이다.
- 호환되지 않는 타입을 넣으면 배열은 런타임, 리스트는 컴파일할 때 에러가 나타난다.
- 배열은 실체화되지만 제네릭은 타입 정보가 런타임 시에 소거된다.
- 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
- 제네릭은 원소 타입을 컴파일 시에만 검사하고 런타임에는 알 수없다.
제네릭 배열 사용 X
위와 같은 차이로 배열과 제네릭은 잘 어우러지지 못하며 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수없다. new List<E>[], new List<String>[], new E[]와 같이 작성하면 컴파일 시 제네릭 배열 생성 오류가 난다.
제네릭 배열을 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수있어 타입 안전성이 사라질 수 있기 때문에 막아 놨다.
배열로 형변환 시 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜰 때 대부분 배열인 E[] 대신 List<E>를 사용하면 해결된다. 코드가 조금 복잡해지고 성능이 살짝 나빠질 수 있지만 타입 안전성과 상호운용성이 좋아지니 List를 사용하는 것이 좋다.
29. 이왕이면 제네릭 타입
일반 클래스 제네릭 클래스로 만드는 방법
- 클래스 선언에 타입 매개변수 추가 (보통 E)
- 코드에 쓰인 Object를 적절한 타입 매개변수로 바꾸고 컴파일해 나타나는 오류 해결
배열 사용하는 코드를 제네릭으로 만드는 해결책
E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다. 이 문제를 해결할 수 있는 두가지 방법이 있다.
- Object 배열을 생성한 후에 제네릭 배열로 형변환
- 제네릭 배열 생성을 금지하는 제약을 우회
- 컴파일러는 오류 대신 경고를 내보내지만 일반적으로 타입 안전하지 않음
- 비검사 형변환이 안전함을 증명했다면 범위를 최소로 좁혀 @SuppressWarnings 애너테이션으로 해당 경고를 숨기면 됨
- 가독성이 좋고 코드 짧으며 E[]로 선언해 E 타입 인스턴스만 받음을 어필하고 형변환을 배열 생성 시 한 번만 해주면 됨
- E가 Object가 아닌 한, 배열의 런타임 타임이 컴파일 타임 타입과 달라 힙 오염을 일으킴
- 현업에서 이 방식을 더 선호하고 자주 사용함
- elements 필드의 타입을 E[]에서 Object[]로 바꾸기
- 배열이 반환한 원소를 E로 형변환해 오류 대신 경고가 뜨도록 하기
- E는 실체화 불가 타입이어서 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없음
- 첫 번째처럼 직접 증명하고 경고를 숨길 수 있다.
- 배열에서 원소 읽을 때마다 형변환 해줘야 함
- 힙 오염이 일어나지 않음
제네릭 배열
제네릭 타입 안에서 리스트를 사용하는 것이 항상 가능하지 않고, 항상 더 좋은 것도 아니다. 자바는 리스트를 기본 타입으로 제공하지 않아 ArrayList와 같은 제네릭 타입도 배열로 구현되어 있고 HashMap과 같은 제네릭 타입은 성능을 높이기 위해 배열을 사용한다.
한정적 타입 매개변수
한정적 타입 매개변수란 타입 매개변수에 제약을 두는 제네릭 타입도으로, 이런 타입 매개변수 E를 말한다.
<E extends Delayed>는 Delayed의 하위 타입만 받는다는 뜻이며 모든 타입은 자기 자신의 하위 타입이기 때문에 Delayed도 사용 가능하다.
30. 이왕이면 제네릭 메서드
매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭이다. (Collections.sort 등)
제네릭 메서드 작성법
제네릭 메서드 작성법은 제네릭 타입 작성법과 비슷하다.
메서드 선언에서의 원소 타입을 타입 매개변수로 명시하고 메서드 안에서도 이 타입 매개변수만 사용하게 하면 된다. 또, 타입 매개변수들을 선언하는 타입 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 온다.
public static <E> Set<E> union(Set<E> s1, Set<E> s2){
Set<E> result;
return result;
}
위의 union 메서드는 집합 3개의 타입이 모두 같아야 하는데, 한정적 와일드카드 타입을 사용해 더 유연하게 개선할 수 있다.
제네릭 싱글턴 팩터리
불변 객체를 여러 타입으로 활용해야 할 때, 제네릭은 런타임에 타입 정보가 소거되어 하나의 객체를 어떤 타입으로든 매개변수화할 수 있다. 이때 요청한 타입 매개변수에 맞게 그 객체의 타입을 바꿔주는 정적 팩터리를 만들어야 하는데 이런 패턴을 제네릭 싱글턴 팩터리라 한다. (Collections.reverseOrder과 같은 함수 객체, Collections.emptySet 과 같은 컬렉션용으로 사용)
재귀적 타입 한정
자기 자신이 들어간 표현식을 사용해 타입 매개변수의 허용 범위를 한정할 수 있다.
재귀적 타입 한정은 주로 타입의 자연적인 순서를 정하는 Comparable 인터페이스와 함께 쓰인다.
31. 한정적 와일드카드
매개변수화 타입은 불공변이어서 서로 다른 타입이 있을 경우에 List<T1> List<T2>는 서로의 상/하위 타입이 아니다. 하지만 이보다 유연한 방식이 필요할 때도 있다. 그때 한정적 와일드카드를 사용하면 된다.
한정적 와일드카드 타입
유연성을 극대화하기 위해서는 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용해야 한다.
- <? extends E> : E의 하위타입의 ~
- <? super E> : E의 상위타입의 ~
public void pushAll(Iterable<? extends E> src){
...
}
public void popAll(Collection<? super E> dst){
...
}
반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다. 클래스 사용자가 와일드카드 타입을 신경써야 한다면 API에 문제가 있을 가능성이 크다.
입력 매개변수가 생산자와 소비자 역할을 동시에 하면 와일드카드 타입을 써도 좋을 것이 없으며 타입을 정확하게 지정해야 하는 상황으로, 이때는 와일드카드 타입을 사용하면 안된다. 다행히 공식이 있다.
와일드카드 선택 기준
겟풋 원칙 PECS : producer-extends, consumer-super
- 매개변수화 타입 T가 생산자라면 <? extends T> 사용
- 매개변수화 타입 T가 소비자라면 <? super T> 사용
타입 매개변수와 와일드카드 선택 기준
메서드 선언에 타입 매개변수가 한 번이라도 나오면 와일드 카드로 대체하는 것이 좋다. 비한정적 타입 매개변수라면 비한정적 와일드카드로, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.
또, 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드(제네릭)로 따로 작성해 활용하면 오류를 해결할 수 있다. (방금 꺼낸 원소를 리스트에 다시 넣을 수 없는 등..)
32. 제네릭과 가변인수 신중하게 함께 사용
가변인수 메서드와 제네릭은 자바 5 때 함께 추가되었지만 잘 어우러지지 못한다.
가변인수
가변인수 메서드 호출 시 가변인수를 담기 위한 배열이 자동으로 하나 만들어지는데, 이 배열을 클라이언트에 노출한다.
- varargs 매개변수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생
- 메서드 선언 시 실체화 불가 타입으로 varargs 매개변수를 선언하면 컴파일러 경고
- 가변인수 메서드 호출 시에도 varargs 매개변수가 실체화 불가 타입으로 추론된다면 해당 호출에 대해서도 경고
→ 매개변수화 타입의 변수가 타입이 다른 객체 참조 시 힙 오염 발생, 타입 안전성 깨짐
→ 네레기 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
제네릭 varargs 매개변수
자바에서는 제네릭 배열을 프로그래머가 직접 생성하는 것은 허용하지 않지만 제네릭 varargs 매개변수를 받는 메서드는 선언할 수 있게 했다. 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용하기 때문에 언어 설계자는 이 모순을 수용했다. (Arrays.asList(T... a), Collections.addAll(Collection<? super T> c, T... elements) 등)
@SafeVarargs
자바 7에 추가된 @SafeVarargs 애너테이션으로 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 됐다.
@SafeVarargs 애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치로 메서드가 안전한지 확실할 때에만 사용해야 한다. 다음과 같은 상황에서 메서드는 타입 안전하다.
- 메서드가 varargs 매개변수 담는 제네릭 배열에 아무것도 저장하지 않음 (매개변수들을 덮어쓰지 않음)
- 배열의 참조가 밖으로 노출되지 않음 (신뢰할 수없는 코드가 배열에 접근할 수 없음)
이때 예외가 있다.
- @SafeVarargs로 제대로 애노테이트된 또 다른 varargs 메서드에 넘기는 것은 안전함
- 이 배열 내용의 일부 함수를 호출만 하는 일반 메서드 (varargs를 받지 않는) 에 넘기는 것은 안전함
@SafeVarargs 애너테이션 사용해야할 땔 정하는 규칙
- 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs 달아야 한다.
- = 안전하지 않은 varargs 메서드는 절대 작성하면 안된다.
- 재정의할 수 없는 메서드에만 달아야 한다. (자바 8에서는 정적 메서드와 final 메서드에만 달 수 있게 되었고, 자바 9부터는 private 인스턴스 메서드에도 허용된다.)
varargs 매개변수 대신 List를 사용하도록 변경할 수도 있다. 이 방법을 사용하면 컴파일러가 메서드의 타입 안정성을 검증할 수 있지만 코드가 살짝 지저분해지고 속도가 조금 느려질 수 있다.
33. 타입 안전 이종 컨테이너 고려
제네릭이 단일원소 컨테이너에서 사용될 때 매개변수화 되는 대상은 컨테이너 자신이며 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다. 더 유연하게 사용하고 싶을 때 타입 안전 이종 컨테이너 패턴을 사용하면 된다.
타입 안전 이종 컨테이너
컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공한다. 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해준다.
타입 안전 이종 컨테이너는 Class를 키로 사용하고, 이렇게 사용되는 Class 객체를 타입 토큰이라 한다.
제약
- 악의적인 클라이언트가 Class 객체를 로 타입으로 넘기면 인스턴스의 타입 안전성이 쉽게 깨진다 (컴파일 시 비검사 경고가 뜸)
- 동적 형변환을 사용해 타입 불변식을 어길 일이 없도록 보장 가능
- 실체화 불가 타입에는 사용할 수 없다
- List<String>.class는 문법 오류 발생함