728x90
반응형
5장 - 제네릭
- 5장 용어 정리
한글 용어 | 영문 용어 | 예 |
---|---|---|
매개변수화 타입 | 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 bound | <T 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 |
💡 로 타입은 사용하지 말라
“로 타입을 사용하면 런타임에 예외가 일어날 수 있으므로 사용하면 안 됨”
1. raw type 사용의 문제점
- 로 타입을 사용하면 제네릭이 안겨주는 타입 안정성과 표현력을 모두 잃게 됨.
- 문제가 되는 경우 예시 코드
- 아래의 예시를 통해 로 타입 사용시 런타임 예외가 일어날 수 있음을 확인
// Stamp 인스턴스만 취급한다.
private final Collection stamps = ...;
// stamps에 도장(Stamp) 대신 동전(Coin)을 넣어도 아무 오류 없이 컴파일되고 실행 됨
stamps.add(new Coin(...));
// 컬렉션에서 이 동전을 꺼낼 때, 런타임 예외가 발생
for (Iterator i = stamps.iterator(); i.hasNext();) {
Stamp stamp = (Stamp) i.next(); // ClassCastException을 던진다.
stamp.cancel();
}
2. 비한정적 와일드 카드 : ?
- 제네릭의 하위 타입 규칙
- 몇 가지 예시
String
은Object
의 하위 타입인가? : 하위타입이다.List<String>
은List
의 하위 타입인가? : 하위타입이다.List<String>
은List<Object>
의 하위 타입인가? : 아니다. 상하관계가 없다.
→ 매개변수화 타입간에는 상하관계가 없다.
→ 따라서 아래 코드는 컴파일되지 않음.
public class Raw {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
// List<String>이 List<Object>의 하위타입이 아님.
unsafeAdd(strings, "test");
}
private static void unsafeAdd(List<Object> list, Object o) {
list.add(o);
}
}
- 그러면 위와 같은 상황을 어떻게 해결하는가? : 비한정적 와일드 카드를 사용한다.
public class Raw {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
// List<String>이 List<Object>의 하위타입이 아님.
unsafeAdd(strings, "test");
}
// Object를 ?로 바꿔주었음.
private static void unsafeAdd(List<?> list, Object o) {
list.add(o);
}
}
- 비한정적 와일드카드 타입(
?
)은 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 때 사용.
3. raw type을 사용해야하는 경우
- raw type을 사용해야 하는 예외 경우가 2가지 있음
- class 리터럴에는 로 타입을 써야 함.
- ex)
List.class
,String[].class
,int.class
가능 /List<String>.class
,List<?>.class
불가능
- ex)
- instanceof 연산자
- 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없음.
- 비한정적 와일드 카드 타입이든 로 타입이든 instanceof는 완전히 똑같이 동작함. 그러니 깔끔한 로 타입을 사용하자
- ex)
if(o instanceof Set) { ... }
💡 비검사 경고를 제거하라
“비검사 경고는 중요하니 무시하지 말자”
1. 비검사 경고
- 비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입 경고, 비검사 변환 경고 등이 있음
- 비검사 경고 예시
// 타입 매개변수를 명시하지 않음 : 비검사 변환 경고
Set<Lark> exaltation = new HashSet();
// 다이아몬드 연산자(<>)로 해결 : 자바7부터 타입 매개변수를 추론해 줌
Set<Lark> exaltation = new HashSet<>();
- 할 수 있는 한 모든 비검사 경고를 제거하자.
- 모두 제거한다면 그 코드는 타입 안정성 보장 → 런타임에
ClassCastException
발생 절대 X
- 모두 제거한다면 그 코드는 타입 안정성 보장 → 런타임에
2. @SuppressWarnings 어노테이션
- 경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면,
@SuppressWarnings(”unchecked”)
어노테이션을 달아 경고를 숨기자. @SuppressWarnings
어노테이션은 항상 가능한 한 좁은 범위에 적용하자- 변수 선언, 아주 짧은 메서드, 생성자 등
- 절대로 클래스 전체에 적용해서는 안 됨
@SuppressWarnings(”unchecked”)
어노테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 함.
💡 배열보다는 리스트를 사용하라
1. 배열 vs 리스트
배열 | 리스트(제네릭) | |
---|---|---|
문법 | String[] |
List<String> |
상하관계 | Sub가 Super의 하위 타입일 때, Sub [] 도 Super[] 의 하위 타입→공변(covariant) |
Type1이 Type2의 하위 타입일 때, List <Type1> 는 List<Type2> 의 하위 타입도 상위 타입도 아님 (상하관계 없음).→ 불공변(invariant) |
타입 안전 | 컴파일 : 타입 안전 X 런타임 : 타입 안전 O → 컴파일 시점에 타입 안전성을 보장 받지 못하여 런타임에 예외 발생 가능 |
컴파일 : 타입 안전 O 런타임 : 타입 안전 X → 컴파일 시점에 타입 안전성을 보장 받음 |
실체화 | 실체화 → 런타임에도 자신이 담기로 한 원소의 타입을 인지 & 확인 |
실체화 불가(소거 매커니즘) → 런타임에는 원소의 타입 정보를 알 수 없음(컴파일 시점에 타입 정보 소거), 비한정적 와일드 카드 타입은 예외. |
2. 배열보다는 리스트
- 공변일 때 발생할 수 있는 문제
// 배열은 공변이므로 정상적으로 컴파일 됨
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다." // 런타임에 ArrayStoreException을 던진다
// 제네릭은 불공변이므로 컴파일이 실패 함
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입
ol.add("타입이 달라 넣을 수 없다.");
→ 이렇듯 배열은 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일 시점에 실수를 바로 잡을 수 있음.
- 주요 차이들로 인해 둘을 섞어 쓰기는 어려움. 리스트를 사용하자.
- 둘을 섞어 쓰다가 오류나 경고를 만나면, 배열을 리스트로 대체해보자.
💡 이왕이면 제네릭 타입으로 만들라
“클라이언트에서 직접 형변환해야하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다”
1. 제네릭 타입
- 타입 매개변수를 이용해 제네릭으로 만든 스택
// E[]를 이용한 제네릭 스택
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
- 제네릭 타입의 장점 : 클라이언트에서 직접 형변환하지 않아도 됨.
2. 한정적 타입 매개변수
- 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다.
Stack<Object>
,Stack<String>
등 어떤 참조 타입으로도 Stack을 만들 수 있음.- 단,
Stack<int>
처럼 기본 타입은 사용할 수 없음.
- 한정적 타입 매개변수 : 하위 타입만 받을 수 있음
- ex)
<E extends Delayed>
:java.util.concurrent.Delayed
의 하위 타입만 받을 수 있음.
- ex)
💡 이왕이면 제네릭 메서드로 만들라
“클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다
제네릭 메서드가 더 안전하며 사용하기 쉽다”
1. 제네릭 메서드
- 타입 매개변수 목록은
<E>
이고, 반환 타입은Set<E>
인 제네릭 메서드
// 제네릭 메서드
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
- 단순한 제네릭 메서드라면 이 이 정도로 충분함
- 현재 이 메서드는 집합 3개(입력 2개, 반환 1개)의 타입이 모두 같아야 하는데, 한정적 와일드카드 타입을 사용하여 더 유연하게 개선할 수 있음.
- 제네릭 메서드의 장점 : 클라이언트에서 입력 매개변수와 반환 값을 명시적으로 형변환하지 않아도 됨.
2. 재귀적 타입 한정
- 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있음
- 주로 타입의 자연적 순서를 정하는
Comparable
인터페이스와 함께 쓰임
// 컬렉션에서 최댓값을 반환한다. - 재귀적 타입 한정 사용
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty())
throw new IllegalArgumentException("컬렉션이 비어 있습니다.");
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return result;
}
<E extends Comparable<E>>
: “모든 타입 E는 자신과 비교할 수 있다.”
💡 한정적 와일드카드를 사용해 API 유연성을 높이라
“PECS공식으로 API를 유연하게 만들자”
1. PECS 공식
- PECS : producer-extends, consumer-super
- 매개변수화 타입
T
가 생산자라면<? extends T>
를 사용(하위타입으로 제한)하고, 소비자라면<? super T>
를 사용(상위타입으로 제한)하라. - produce-extends 예시
- 생성자로 넘겨지는
choices
컬렉션은T
타입의 값을 생산하기만 하니(그리고 나중을 위해 저장해둔다),T
를 확장하는 와일드카드 타입을 사용해 선언해야 함. - 이렇게 하면
Chooser<Number>
의 생성자에List<Integer>
를 넘길 수 있음.
- 생성자로 넘겨지는
public class Chooser<T> {
private final List<T> choiceList;
private final Random rnd = new Random();
// T 생산자 매개변수에 와일드카드 타입 적용
public Chooser(Collection<? extends T> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
return choiceList.get(rnd.nextInt(choiceList.size()));
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5, 6);
Chooser<Number> chooser = new Chooser<>(intList);
for (int i = 0; i < 10; i++) {
Number choice = chooser.choose();
System.out.println(choice);
}
}
}
- consumer-super 예시
- 스택의
popAll
의dst
매개변수는 Stack으로 부터E
인스턴스를 소비하므로dst
의 적절한 타입은Collection<? super E> dst
임.// E 소비자(consumer) 매개변수에 와일드카드 타입 적용 public void popAll(Collection<? super E> dst) { while (!isEmpty()) dst.add(pop()); }
- 스택의
2. 비한정적 타입 매개변수 vs 비한정적 와일드카드
- 기본 규칙 : 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라.
- 아래 예시에서는 간단한 두 번째 선언이 더 나음 (public API에 적합)
// swap 메서드의 두 가지 선언
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
💡 제네릭과 가변인수를 함께 쓸 때는 신중하라
“제네릭과 가변인수를 혼용하면 타입 안정성이 깨진다”
1. 가변 인수(...
)
- 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해줌.
- 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 생성됨.
2. 제네릭과 가변인수의 혼용
- 제네릭과 가변인수를 혼용했을 때의 문제
- 힙 오염(heap pollution) : 매개변수화 타입의 변수가 타입이 다른 객체 참조했을 때 발생
- 아래 예시처럼 타입 안정성이 깨지니 제네릭
varargs
배열 매개변수에 값을 저장하는 것은 안전하지 않음.
// 제네릭과 varargs를 혼용하면 타입 안전성이 깨진다!
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
3. @SafeVarargs 어노테이션
- 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치
- 제네릭이나 매개변수화 타입의
varargs
매개변수를 받는 모든 메서드에@SafeVarargs
를 달아라.- 이 말은 안전하지 않은
varargs
메서드는 절대 작성해서는 안된다는 뜻임.
- 이 말은 안전하지 않은
- 다음 두 조건을 만족하는 제네릭
varargs
메서드는 안전함varargs
매개변수 배열에 아무것도 저장하지 않는다.- 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
- 제네릭
varargs
매개변수를 안전하게 사용하는 메서드 예시
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
4. 가변인수 대신 List를 사용하는 방식
- “배열보다는 리스트를 사용하라”라는 조언에 따라 (실체는 배열인)
varargs
매개변수를List
매개변수로 바꿀 수 있음 - 제네릭
varargs
매개변수를List
로 대체한 메서드 예시
static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
💡 타입 안전 이종 컨테이너를 고려하라
“컨테이너에서 매개변수화 타입의 수가 임의의 수라면
타입 안전 이종 컨테이너 패턴을 사용하자”
1. 타입 안전 이종 컨테이너 패턴
- 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 방식
- 각 타입의 클래스 객체를 키 역할로 사용하는 예제를 살펴보자
용어 | 설명 |
---|---|
class 리터럴 | String.class |
class 리터럴의 타입 | Class |
타입 토큰 | 메서드들이 주고 받는 class 리터럴 |
- 타입 안전 이종 컨테이너 패턴 - API 코드 |
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
- 타입 안전 이종 컨테이너 패턴 - API 구현
cast
메서드는 형변환 연산자의 동적 버전이다.- 주어진 인수가
Class
객체가 알려주는 타입의 인스턴스인지를 검사한 다음, 맞다면 그 인수를 그대로 반환, 아니면ClassCastException
을 던짐.
- 주어진 인수가
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
// 동적 형변환으로 런타임 타입 안전성 확보
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
- 타입 안전 이종 컨테이너 패턴 - 클라이언트
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString,
favoriteInteger, favoriteClass.getName());
}
- 이 프로그램은
Java cafebabe Favorites
를 출력함.
2. 한정적 타입 토큰
- 한정적 타입 토큰 : 한정적 타입 매개변수나 한정적 와일드카드를 사용해 표현 가능한 타입을 제한하는 타입 토큰
- 사용 예시 :
getAnnotation
메서드- annotationType 인수는 어노테이션 타입을 뜻하는 한정적 타입 토큰이다.
- 이 메서드는 토큰으로 명시한 타입의 어노테이션이 대상 요소에 달려있다면 어노테이션을 반환하고, 없다면 null을 반환함
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
728x90
반응형
'📚 Reading > Tech' 카테고리의 다른 글
이펙티브 자바 - 7장. 람다와 스트림 (0) | 2023.01.24 |
---|---|
이펙티브 자바 - 6장. 열거 타입과 애너테이션 (0) | 2023.01.24 |
이펙티브 자바 - 4장. 클래스와 인터페이스 (0) | 2023.01.24 |
이펙티브 자바 - 3장. 모든 객체의 공통 메서드 (0) | 2023.01.24 |
이펙티브 자바 - 2장. 객체 생성과 파괴 (0) | 2023.01.24 |