-
5장 - 제네릭📕 book/이펙티브 자바 2022. 11. 1. 17:21
제네릭은 자바 5부터 사용할 수 있다.
제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때마다 형변환을 해야 했다.
- 누군가 실수로 엉뚱한 타입의 객체를 넣어두면 런타임에 형변환 오류가 발생
⇒ 제네릭을 사용해 컴파일 과정에서 차단하여 더 안전하고 명확한 프로그램을 만들자.
📌 로 타입은 사용하지 마라
클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라고 한다.
제네릭 타입 (generic type)
- 제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라 한다.
- 각각의 제네릭 타입은 일련의 매개변수화 타입 을 정의한다.
- ex) List<String> 은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입
- String이 정규 타입 매개변수 E에 해당하는 실제(actual) 타입 매개변수
제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다.
로 타입
- 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.
- List<E> 의 로 타입은 List 이다.
로 타입을 사용하면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다.
- 왜 만듬❓ ⇒ 호환성 때문
- 자바가 제네릭을 받아들이기까지 제네릭 없이 짠 코드가 많았다.
- 따라서, 기존 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와도 맞물려 돌아가게 하도록
List<Object> 같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입을 사용하면 타입 안전성을 잃게된다.
// static int numElementsInCommon(Set s1, Set s2) - 잘못된 예 Set이 뭔지 모른다. static int numElementsInCommon(Set<?> s1, Set<?> s2) { int result = 0; for(Object o1 : s1) if(s2.contains(o1) result++; return result; }
비한정 와일드카드 타입(unbounded wildcard type)을 대신 사용
- 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지않다면 물음표(?)를 사용하자
로 타입은 어떤 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다.
반면, 비한정 와일드카드 타입을 사용하면 null 이외에는 어떤 원소도 넣을 수 없다.
예외
- class 리터럴에는 로 타입을 사용해야 한다.
- instance of 연산자
- 런타임에는 제네릭 타입 정보가 지워진다
- instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.
if(o instanceof Set){ Set<?> s = (Set<?>) o; ... }
정리
- 로 타입을 사용하면 런타임에 예외가 일어날 수 있다.
- Set<Object> : 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입
- Set<?> : 모종의 타입 객체만 저장할 수 있는 와일드카드 타입
- Set<Object>, Set<?>는 안전하지만, 로 타입인 Set은 안전하지 않다.
한글 용어 예시 매개변수화 타입 List<String> 실제 타입 매개변수 String 제네릭 타입 List<E> 정규 타입 매개변수 E 비한정적 와일드카드 타입 List<?> 로 타입 List 한정적 타입 매개변수 <E extends Number> 재귀적 타입 한정 <T extends Comparable<T>> 한정적 와일드카드 타입 List<? extends Number> 제네릭 메서드 static <E> List<E> asList(E[] a) 타입 토큰 String.class 📌 비검사 경고를 제거하라
제네릭을 사용하면 많은 컴파일러 경고가 있다.
- 대부분의 비검사 경고는 쉽게 제거할 수 있다.
경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면 @SuppressWarnings(”unchecked”) 애너테이션을 달아 경고를 숨기자
- 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다.
- 항상 가능한 좁은 범위에 적용하자
- 절대로 클래스 전체에 적용해서는 안된다
- 심각한 경고를 놓칠 수 있다.
- 사용시 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다.
📌 배열보다는 리스트를 사용하라
배열과 제네릭의 차이
- 배열은 공변이다, 제네릭은 불공변이다.
- Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이다.
- 제네릭은 서로 다른 타입 Type1, Type2가 있을 때, List<Type1>는 List<Type2>의 하위 타입도 상위타입도 아니다.
Object[] objectArray = new Long[1]; ObjectArray[0] = "타입이 달라 넣을 수 없다"; // 런타임에 실패 // 컴파일 되지 않는다. List<Object> ol = new ArrayList<Long>(); ol.add("타입이 달라 넣을 수 없다");
- 배열은 실체화 된다.
- 배열은 런타임에도 자신이 담기로한 원소의 타입을 인지하고 확인
- 하지만, 제네릭은 타입 정보가 런타임에 소거
- 원소 타입을 컴파일타임에만 검사하며 런타임에는 알수조차 없다.
정리
배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다.
제네릭은 반대
⇒ 둘을 섞어 쓰기 쉽지 않다.
📌 이왕이면 제네릭 타입으로 만들어라
일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다.
- 이 때, 타입 이름으로는 보통 E를 사용
public class Stack<E> { private E[] elements; ... public Stack() { elements = new E[...]; // 문제 발생 } ... }
배열을 사용하는 코드를 제네릭으로 만들려할 때 문제 발생
- E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.
→ 해결 방법 2가지
제네릭 배열 생성을 금지하는 제약을 대놓고 우회
elements = (E[]) new Object[...];
- 가독성이 좋고 확실하게 E타입의 배열을 선언하여 한번만 형변환을 해주면 되고 코드가 짧다.
- 이 방법을 선호
- 하지만, 배열 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킨다.
elements 필드의 타입을 E[]에서 Object[]로 바꾸기
- 데이터를 꺼낼 때마다 형변환을 해주어야 한다.
타입 매개변수에 제약을 두는 제네릭 타입
class DelayQueue<E extends Delayed> implements BlockingQueue<E> ...
타입 매개변수 목록인 <E extends Delayed>는
java.util.concurrent.Delayed 의 하위 타입만 받는다는 뜻
⇒ 이러한 타입 매개변수 E를 한정적 타입 매개변수라 한다.
📌 이왕이면 제네릭 메서드로 만들라
클래스와 마찬가지로, 메서드도 제네릭으로 만들 수 있다.
메서드 선언에서 입력, 반환의 원소 타입을 타입 매개변수로 명시하고 메서드 안에서도 이 타입 매개변수만 사용
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<>(s1); result.addAll(s2); return result; }
때때로, 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때가 있다.
- 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화할 수 있다.
- 이렇게 하기 위해선 요청한 타입 매개변수에 맞게 매변 그 겍체의 타입을 바꿔주는 정적 팩터리를 만들어야 한다.
- ⇒ 제네릭 싱글턴 팩터리
public static void main(String[] args) { String[] strings = {"삼베", "대마", "나일론"}; UnaryOperator<String> sameString = identityFunction(); for(String s : strings) System.out.println(sameString.apply(s)); Number[] numbers ={1, 2.0, 3L}; UnaryOperator<Number> sameNumber = identityFunction(); for(Number n : numbers) System.out.println(sameNumber.apply(n)); }
- 제네릭 싱글턴을 사용하는 예
- UnaryOperator<String> 과 UnaryOperator<Number>로 사용
자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다.
→ 재귀적 타입 한정(recursive type bound)
- 주로 타입의 자연적 순서를 정하는 Comparable 인터페이스와 함께 쓰인다.
public static <E extends Comparable<E>> E max(Collection<E> c);
<E extends Comparable<E>>
- “ 모든 타입 E는 자신과 비교할 수 있다 “
📌 한정적 와일드카드를 사용해 API 유연성을 높이라
매개변수화 타입은 불공변
→ List<String> 은 List<Object>가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다.
(리스코프 치환 원칙에 어긋)
자바는 이런 상황을 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다.
- 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용
공식 - PECS(펙스)
PECS : producer-extends, consumer-super
매개변수화 타입 T가 생산자라면?
- T를 확장하는 와일드카드 타입을 사용
- <? extends T>
매개변수화 타입 T가 소비자라면
- <? super T>
제대로 사용하면 클라이언트는 와일드카드 타입이 쓰인지 의식하지 않아도 된다
클래스 사용자가 와일드카드 타입을 신경써야한다면 API에 문제가 있을 가능성이 크다.
Comparable
- 언제나 소비자
- Comparable<E> 보다는 Comparable<? super E>를 사용하는 것이 낫다.
Comparator
- Comparator<E> 보다 Comparator<? super E>를 사용하는 것이 낫다.
타입 매개변수와 와일드카드
타입 매개변수와 와일드카드에는 공통되는 부분이 많아, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많다.
public static <E> void swap(List<E> list, int i, int j); public static void swap(List<?> list, int i, int j);
- 어떤 것이 더 나을 까?
기본 규칙 : 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라
public static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); } // 와일드카드 타입을 실제 타입으로 변경해주는 private 도우미 메서드) private static <E> void swapHelper(List<E> list, int i, int j) { list.set(i, list.set(j, list.get(i))); }
- List<?>은 null외에는 어떤 값도 넣을 수 없어 문제가 발생
- 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 작성하여 활용
- 실제 타입을 알기 위해서 이 메서드는 제네릭 메서드여야 함.
📌 제네릭과 가변인수를 함께 쓸 때는 신중하라
가변 인수 메서드는 제네릭과 함께 자바 5때 함께 추가되어 서로 잘 어우러질것 같지만 그렇지 않다.
- 가변 인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해주는데 구현에 허점이 존재
→ 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어 진다.
⇒ 내부로 감춰야 했을 이 배열을 클라이언트에 노출하는 문제가 발생
그 결과 varargs 매개변수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생
가변 인수 (Variable argument, varargs)
- 매개변수의 개수를 동적으로 지정하는 기능
public static print_val(String... str) { for (String s : str) System.out.print(s + " "); }
제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
@SafeVarargs 에노테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치
- 메서드가 이 배열에 아무것도 저장하지 않고 그 배열의 참조가 밖으로 노출되지 않는다면 안전
- varargs 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 안전
제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다.
예외
- @SafeVarargs로 제대로 에너테이트된 또 다른 varargs 메서드에 넘기는 것은 안전
- 배열 내용의 일부 함수를 호출만 하는 일반 메서드에 넘기는 것도 안전
제네릭 varargs 매개변수를 안전하게 사용하는 메서드
@SafeVarargs static <T> List<T> flatten(List<? extends T>... lists) { List<T> result = new ArrayList<>(); for(List<? extends T> list : lists) result.add(list); return result; }
제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달자
다음 두 조건을 모두 만족하는 제네릭 varargs 메서드는 안전
- varargs 매개변수 배열에 아무것도 저장하지 않는다.
- 그 배열을 신뢰할 수 없는 코드에 노출하지 않는다.
📌 타입 안전 이종 컨테이너를 고려하라
제네릭은 Set, Map 등의 컬렉션과
ThreadLocal<T>, AtomicReference<T> 등의 단일원소 컨테이너에도 흔히 쓰인다.
- 매개변수화 되는 대상은 (원소가 아닌) 컨테이너 자신이다.
- 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
하지만, 더 유연한 수단이 필요할 때도 종종 있다.
→ 해결 방법
- 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공
이런 설계 방식을 타입 안전 이종 컨테이너 패턴 이라 한다.
각 타입의 Class 객체를 매개변수화한 키 역할로 사용
- class의 클래스가 제네릭이기 때문에 이런 방식이 동작한다
- 컴파일타임 타입정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고 받는 class 리터럴을 타입토큰(type token)이라 한다.
// 타입 안전 이종 컨테이너 패턴 API public class Favorites { public <T> void putFavorite(Class<T> type, T instance); public <T> T getFavorite(Class<T> type); }
// 타입 안전 이종 컨테이너 패턴 - 클라이언트 public static void main(String[] args) { Favorites f = new Favorties(); f.putFavorite(String.class, "Java"); f.putFavorite(Integer.class, 0xcafebabe); f.putFavorite(Class.class, Favorites.class); String s = f.getFavorite(String.class); int i = f.getFavorite(Integer.class); Class<?> c = f.getFavorite(Class.class); }
제약 사항
- 악의적인 클라이언트가 Class 객체를 (제네릭이 아닌)로 타입 으로 넘기면 Favorite 인스턴스의 타입 안전성이 쉽게 깨짐
- 실체화 불가 타입에는 사용할 수 없다.
- String, String[]은 저장할 수 있어도
- List<String>은 저장할 수 없다.
한정적 타입 매개변수
- 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰
참고 자료
'📕 book > 이펙티브 자바' 카테고리의 다른 글
7장 - 람다와 스트림 (0) 2022.11.07 6장 - 열거타입과 애너테이션 (0) 2022.11.04 4장 - 클래스와 인터페이스 (0) 2022.11.01 3장 - 모든 객체의 공통 메서드 (0) 2022.10.26 2장 - 객체 생성과 파괴 (0) 2022.10.25