-
[Java] 이펙티브 자바 (4) - 제네릭Java 2020. 4. 18. 15:45반응형
아이템 26. 로 타입은 사용하지 말라
Generic Type(제네릭 타입)
일련의 매개변수화 타입(parameterized)을 정의
클래스(혹은) 인터페이스 이름이 나오고, 이어서 <> 안에 실제 타입 매개변수들을 나열
제네릭 활용시 타입만 취급하는지의 정보가 주석이 아닌 타입 선언 자체에 녹아들게 됨
-> 컴파일러가 의도하지 않은 동작시 컴파일 오류를 발생시킴
Raw Type(로 타입)
제네릭 타입을 하나 정의하면 그에 딸린 로 타입도 함께 정의
타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 생기기 전 코드와 호환되도록 하기 위한 궁여 지책
로 타입을 그냥 사용시 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 됨(컴파일 오류가 아닌 런타임 오류!)
Unbounded Whildcard Type(비한정적 와일드카드 타입)
제네릭 타입을 사용하고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 때는 ?를 사용
제네릭 타입 Set<E>의 비한정적 와일드카드 타입은 Set<?>
-> 어떤 타이빙라도 담을 수 있는 가장 범용적인 매개변수화 Set
와일드카드 타입은 안전하고, 로 타입은 안전하지 않음
- 로 타입 컬렉션 : 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉬움
- Collection<?>에는 (null 외에는) 어떤 원소도 넣을 수 없음
Generic이 아닌 Raw Type을 써야하는 경우
1. class 리터럴
- 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 함(배열과 기본 타입은 허용)
- ex) List.class, String[].class, int.class - 허용
- ex) List<String>.class, List<?>.class - 비허용
2. instanceof
- 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 지정할 수 없음
- 로 타입과 와일드카드 타입 모두 instanceof는 동일하게 동작됨
- 비한정적 와일드 타입의 <?>는 이런 경우 아무런 역할 없이 코드만 지저분하게 만드므로 로 타입을 쓰는 것이 깔끔
-> 로 타입으로 instanceof 확인 후 와일드카드로 형변환하여 사용
용어
Generic Interface(제네릭 클래스) : 클래스 선언에 타입 매개변수(type parameter)가 쓰이는 경우. ex) ArrayList<E>
Generic Interface(제네릭 인터페이스) : 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이는 경우. ex) List<E>
Generic Type(제네릭 타입) : 제네릭 클래스와 제네릭 인터페이스를 통틀어 부르는 용어
Raw Type(로 타입) : 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때. ex) List
Parameterized Type(매개변수화 타입) : ex) List<String>
Actual Type Parameter(실제 타입 매개변수) : ex) String
Formal Type Parameter(정규 타입 매개변수) : ex) E
Unbounded Wildcard Type(비한정적 와일드카드 타입) : ex) List<?>
Bounded Type Parameter(한정적 타입 매개변수) : ex) <E extends Number>
Recursive Type Bound(재귀적 타입 한정) : ex) <T extends Comparable<T>>
Generic Method(제네릭 메서드) : static <E> List<E> asList(E[] a)
Type Toekn(타입 토큰) : String.class
아이템 27. 비검사 경고를 제거하라
제네릭 비검사 경고
비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입 경고, 비검사 변환 경고 등
비검사 경고는 -Xlint:uncheck 옵션 추가시 컴파일러가 무엇이 잘못되었는지 친절히 설명해 줌
할 수 있는 한 모든 비검사 경고를 제거
- 모두 제거한다면 해당 코드는 타입 안전성이 보장됨
- 런타임에 ClassCastException이 발생할 일이 없음
- 의도한 대로 잘 동작하리라 확신
경고를 제거할 수 없지만 타입 안전하다고 확신한다면
- @SuppressWarning("unchecked") 애너테이션으로 경고를 숨김
- 타입 안전함을 검증하지 않은 채 경고를 숨기는 경우
- 경고 없이 컴파일되지만, 런타임에 여전히 ClassCastException을 던질 수 있음
- 안전하다고 검증된 비검사 경고를 숨기지 않는 경우
- 진짜 문제를 알리는 새 경고가 나와도 눈치채지 못할 수 있음
@SuppressWarnings 애너테이션
- 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있음
- 항상 가능한 좁은 범위에 적용할 것
- 변수, 아주 짧은 메서드, 생성자 등
- 클래스 전체에 적용시 자칫 심각한 경고를 놓칠 수 있음
- 한 줄이 넘는 메서드나 생성자에 달린 @SuppressWarning 애너테이션
- 발견시 지역변수 선언쪽으로 옮길 것
- 깔끔하게 컴파일되고 비검사 경고를 숨기는 범위도 최소화 됨
- 해당 경고를 무시해도 안전한 이유를 항상 주석으로 남길 것
아이템 28. 배열보다는 리스트를 사용하라
배열 vs 제네릭 타입
1. 배열은 공변(covariant), 제네릭은 불공변(invariant)
- Sub가 Super의 하위 타입이라면?
- 배열 : Sub[]는 Super[]의 하위 타입이 됨(공변; 같이 변함) => 관련 문제가 런타임에 발견됨
- 제네릭 : List<Sub>는 List<Super>의 하위 타입도 상위 타입도 아님 => 관련 문제가 컴파일에 발견됨 (선호)
2. 배열은 실체화(reify), 제네릭은 소거(erasure)
- 배열 : 런타임에도 자신이 담기로 한 원소이 타입을 인지하고 확인함
- 제네릭 : 타입 정보가 런타임에는 제거되어 알 수 없음
소거 : 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘, 자바 5가 제네릭으로 순조롭게 전환되도록 해 줌
위의 주요 타입으로 인해 배열과 제네릭 타입은 잘 어우러지지 못함
- 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 선언할 수 없음
제네릭 배열을 만들지 못하게 막은 이유
- 타입 안전하지 않기 때문
- 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임 에러 ClassCastException이 발생할 수 있음
- 제네릭의 취지 : 런타임에 ClassCastException이 발생하는 것을 막아줌
- new List<E>[], new List<String>[], new E[] 식으로 작성하면 컴파일시 제네릭 배열 생성 오류를 일으킴
제네릭 배열을 만들지 못해 불편한 경우
- 제네릭 컬랙션에서는 자신의 원소 타입을 담은 배열을 반환하는 것이 보통은 불가능
- 제네릭 타입과 가변인수 메서드(varargs method)를 같이 쓰면 해석하기 어려운 경고 메시지를 받게 됨
- 가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 이때 해당 배열의 원소가 실체화 불가 타입이라면 경고가 발생함. @SafeVarargs 애너테이션으로 대처 가능
E[] 대신 컬렉션인 List<E>를 사용
- 배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고과 뜨는 경우 대부분은 List<E> 사용시 해결
-> 배열 대신 리스트 사용
- 코드가 조금 복잡해지고 성능이 나빠질 수 있지만, 타입 안전성과 상호운용성이 좋아짐
용어
Varargs Method(가변인수 메서드) : Object... 처럼 ...을 붙여서 여러 인수를 받을 수 있도록 한 것
아이템 29. 이왕이면 제네릭 타입으로 만들라
일반 타입보다는 제네릭 타입
- 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하기 쓰기 편함
- 새로운 타입을 설계할 때, 형변환 없이도 사용할 수 있도록 할 것 => 제네릭 타입으로 만들어야 할 경우가 많음
- 기존 타입 중 제네릭이었어야 하는 부분을 제네릭 타입으로 변경 => 클라이언트에는 영향을 주지 않으면서, 새로운 사용자를 편하게 해 줌
일반 클래스를 제네릭 클래스로 만드는 것
1. 클래스 선언에 타입 매개변수를 추가하기
2. 배열을 사용하는 코드의 경우 오류 발생 => 해결법
배열을 사용하는 코드의 해결법 1) 제네릭 배열 생성을 금지하는 제약을 우회
- Object 배열을 생성한 후 제네릭 배열로 형변환 - (일반적으로) 타입 안전하지 않음
- 비검사 형변환이 프로그램의 타입 안전성을 해치지 않음을 스스로 확인해야 함
- 안전함을 확인했다면, 범위를 최소로 좁혀 @SuppressWarnings 애너테이션으로 해당 경고를 숨김
- 장점 : 가독성이 더 좋음
배열을 사용하는 코드의 해결법 2) Elements 필드의 타입을 E[]에서 Object[]로 변경
- 필드의 타입을 Object[]로 변경 후 형변환
- 1)과 동일하게 타입 안전성을 해치지 않음을 스스로 확인 후 @SuppressWarnings 애너테이션으로 경고를 숨겨야 함
- 장점 : 힙 오염이 항상 없음
배열보다는 리스트를 우선하라와 모순되는 것이 아닌가?
- 제네릭 타입 내에서 리스트를 사용하는 것이 항상 가능하지도, 꼭 더 좋은 것도 아님
- 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList같은 제네릭 타입은 기본 타입인 배열을 사용해 구현
- HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용
제네릭 타입에서의 기본 타입 사용
- 대다수의 제네릭 타입은 타입 매개변수에 아무런 제한을 두지 않음
- 단, 기본 타입은 사용할 수 없음 -> 박싱된 기본 타입을 사용해 우회할 수 있음
Bounded Type Parameter(한정적 타입 매개변수)
- 매개변수에 제약을 두는 제네릭 타입
- 예제 - java.util.concurrent.DelayQueue
- class DelayQueue<E extends Delayed> implements BlockingQueue<E>
- <E extends Delayer> => java.util.concurrent.Delayed의 하위 타입만 받는다는 의미
=> DelayQueue 자신과 DelayQueue를 사용하는 클라이언트는 DelayQueue의 원소에서 (형변환없이 바로) Delayed 클래스의 메서드 호출 가능
- 모든 타입은 자기 자신의 하위 타입
- 예제를 DelayQueue<Delayed>로도 사용 가능
아이템 30. 이왕이면 제네릭 메서드로 만들라
제네릭 메서드
- 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉬움
- 타입과 마찬가지로, 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그러려면 제네릭 메서드여야 함
- 형변환을 해줘야 하는 기존 메서드는 제네릭하게 만들기 => 새로운 사용자를 편하게 만들어줌
제네릭 메서드 작성법
- (타입 매개변수들을 선언하는) 타입 매개변수 목록은 제한자와 반환 타입 사이에 옴
- 메서드 선언에서의 원소 타입(입력/반환)을 타입 매개변수로 명시하고, 메서드 내에서도 해당 타입 매개변수만 사용하도록 수정
- 한정적 와일드카드 타입을 사용하여 더 유연하게 개선할 수도 있음
제네릭 싱글턴 팩터리
- 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때, 요청한 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩터리를 만들어야 함
- Collections.reverseOrder 같은 함수 객체나 Collections.emptySet 같은 컬렉션용으로 사용
항등함수(identity function)를 담은 클래스
- 항등함수 객체는 상태가 없으므로 요청할 때마다 새로 생성하는 것은 낭비
- 제네릭은 소거 방식이라, 제네릭 싱글턴 팩터리 하나면 생성 가능
- 항등 함수는 타입 안전하다는 사실을 알고 있으므로, 비검사 형변환 경고는 숨겨도 안심 -> @SupressWarnings 이용
재귀적 타입 한정(recursive type bound)
- 드물게, 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있음
- 주로 타입의 자연적 순서를 정하는 Comparable 인터페이스와 함께 쓰임
- 예) <E extends Comparable<E>>
용어
Identity Function(항등 함수) : 입력 값을 수정 없이 그대로 반환하는 특별한 함수
아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
불공변
- Type1과 Type2가 있을 때, List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아님
- List<Object>엔 무엇이든 넣을 수 있지만 List<String>에는 문자열만 넣을 수 있음. List<String>은 List<Object>가 하는 일을 제대로 수행할 수 없으니 하위 타입이 될 수 없음(리스코프 치환 원칙)
한정적 와일드카드
- <? extends E> : 'E의 하위타입인 ?' - 모든 타입은 자신의 하위타입
- ex) Iterable<? extends E> : E의 하위타입(?)의 Iterable
- <? super E> : 'E의 상위타입인 ?' - 모든 타입은 자신의 상위타입
- ex) Collection<? super E> : E의 상위 타입(?)의 Collection
- 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용할 것
- 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 쓰지 말아야 함
- 반환 타입에는 한정적 와일드카드 타입을 사용하지 말 것
- 유연성을 높여주기는 커녕 클라이언트 코드에서도 와일드카드 타입을 써야하기 때문
- 클래스 사용자가 와일드카드 타입을 신경써야 한다면 그 API에 무슨 문제가 있을 가능성이 큼
한정적 와일드 카드 타입 적용
- 조금 복잡하더라도 사용하면 API가 훨씬 유연해짐
- 널리 쓰일 라이브러리 작성시, 반드시 와일드카드 타입을 적절히 사용해줘야 함
- PECS 공식(Get and Put Principle - 겟풋원칙)
- Producer(생산자) - extends 사용, Consumer(소비자) - super 사용
- Comparable과 Comparator는 모두 소비자!
- Comparable<? super E>, Comparator<? super E>
- ex) public static <E extends Comparable<? super E>> E max(List<? extends E> list)
=> Comparable을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하는 경우
public interface Comparable<E> public interface Delayed extends Comparable<Delayed> public interface scheduledFuture<V> extends Delayed, Future<V>
타입 매개변수 vs 와일드카드
타입 매개변수 public static <E> void swap(List<E> list, int i, int j); 와일드카드 public static void swap<List<?> list, int i, int j);
- 기본 규칙
- 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체
- 비한정적 타입 매개변수라면 비한정적 와일드카드로
- 한정적 타입 매개변수라면 한정적 와일드카드로
- 와일드카드 선언의 문제
- 꺼낸 원소를 리스트에 다시 넣을 수 없음 => 리스트 타입이 List<?>인데, null 외에 어떤 값도 넣을 수 없음
- private 도우미 메서드를 따로 작성하여 활용하면 됨 (이 도우미 메서드는 타입 매개변수 형태와 완전히 동일해짐)
- 내부에서는 좀 더 복잡하지만, 외부에서는 와일드카드 기반의 선언을 유지할 수 있음
용어
- 매개변수(parameter) : 메서드 선언에 정의한 변수
- ex) 클래스 선언시 정의한 제네릭 class Set<T> {...} 의 T는 타입 매개변수
- 인수(argument) : 메서드 호출시 넘기는 '실젯값'
- ex) 호출시 넘기는 인수 제네릭 Set<Integer> = ...; 의 Integer는 타입 인수
아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
가변인수(varargs)
- 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해주는데, 구현 방식에 허점이 있음
- 가변인수 메서드를 호출할 경우, 가변인수를 담기 위한 배열이 자동으로 하나 생성 됨
-> 내부로 감춰야 하는 이 배열을 클라이언트에 노출하는 문제가 생김
-> varargs 매개변수에 제네릭이나 매개변수화 타입이 포함될 경우 알기 어려운 컴파일 경고가 발생
힙 오염(Heap pollution)
- 매개변수화 타입의 변수가 타입이 다른 객체를 참조할 경우 발생
가변인수와 제네릭
- 가변인수 : 배열을 노출하여 추상화가 완벽하지 못함
- 제네릭 : 배열과 타입 규칙이 서로 다름
제네릭 varargs 매개변수
- 타입 안전하지는 않지만, 허용됨
- 허용하는 이유 = 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용하기 때문
- 메서드에 제네릭 (혹은 매개변수화된) varargs 매개변수를 사용하고자 한다면, 메서드가 타입 안전한지 확인한 후 @SafeVarargs 애너테이션을 달아둘 것
@SafeVarargs
- 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치
- 안전한게 확실하지 않으면 절대 사용하면 안 됨
안전한 제네릭 varargs 메서드 - 두 조건을 모두 만족해야 함
- 메서드가 varargs 매개변수 배열에 아무것도 저장하지 않음
- 그 배열(혹은 복제본)의 참조를 신뢰할 수 없는 코드에 노출하지 않음
=> 제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않음
- 예외1) @SafeVarargs로 제대로 애노테이트된 또 다른 varargs 메서드에 넘기는 것은 안전
- 예외 2) 그저 배열 내용의 일부 함수를 호출만 하는(varargs를 받지 않는) 일반 메서드에 넘기는 것도 안전
= 즉 varargs 매개변수 배열이 호출자로부터 그 메서드를 순수하게 인수들을 전달하는 일만 한다면, 안전!
@SafeVarargs 애너테이션의 대안 - varargs 매개변수를 List 매개변수로 변경
static<T> List<T> flatten(List<? extends T>... lists) {...} -> static <T> List<T> flatten(List<List<? extends T>> list) {...}
아이템 33. 타입 안전 이종 컨테이너를 고려하라
타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)
- 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있음, 하지만 컨테이너 자체가 아닌 Key를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있음
- Class를 Key로 사용, 이런식으로 쓰이는 Class 객체를 타입 토큰이라고 함
- 직접 구현한 Key 타입도 사용할 수 있음
- 예) DB의 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭 타입인 Column<T>를 키로 사용 가능
타입 토큰(type token)
- 컴파일타임의 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴
- Class를 Key로 사용할 수 있는 이유는 각 타입의 Class 객체를 매개변수화한 키 역할로 사용해야 하는데, class의 리터럴이 제네릭이기 때문
- class 리터럴의 타입은 Class가 아닌 Class<T>
- String.class의 타입은 Class<String>, Integer.class의 타입은 Class<Integer>
타입 이종 컨테이너 패턴 구현
- Map<Class<?>, Object>
- 와일드 카드 타입이 중첩(nested)되었음 - 맵이 아니라 키가 와일드카드 타입
- 값 타입이 Object : 키와 값 사이의 타입 관계를 보증하지 않음(하지만 사용자가 알고 있음)
자바 printf의 %n
- %n은 플랫폼에 맞는 줄바꿈 문자로 자동으로 대체
주의할 점
1. 악의적인 클라이언트가 Class 객체를 (제네릭이 아닌) 로 타입으로 넘기면 타입 안전성이 쉽게 깨짐
- 잘 짜여진 클라이언트 코드에서는 컴파일할 때 비검사 경고가 뜰 것
2. 실체화 불가 타입에는 사용할 수 없음
- 슈퍼 타입 토큰(super type token)으로 해결하고자 하는 시도도 있음
한정적 타입 토큰을 활용한 메서드들이 허용하는 타입 제한
- 한정적 타입 토큰 : 단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰
- 애너테이션 API는 한정적 타입 토큰을 적극적으로 사용함
Class 클래스의 asSubclass 메서드
- 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환
-> 형변환 된다는 것 : 이 클래스가 인수로 명시한 클래스의 하위 클래스라는 뜻
- 성공 : 인수로 받은 클래스 객체를 반환
- 실패 : ClassCastException
반응형'Java' 카테고리의 다른 글
[Java] 이펙티브 자바 (3) - 클래스와 인터페이스 (0) 2020.04.18 [Java] 이펙티브 자바 (2) - 모든 객체의 공통 메서드 (0) 2020.04.18 [Java] 이펙티브 자바 (1) - 객체 생성과 파괴 (0) 2020.04.18 [Java] GOF 디자인패턴 용어 정리 (0) 2020.04.16 자바의 I/O (0) 2020.03.13