생각해보기
이펙티브 자바 -4- 본문
제네릭
제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때마다 형 변환해야 했다. 제네릭을 사용하면 컬렉션이 담을 수 있는 타입을 컴파일러가 알려주게 된다. 따라서 안전하고 명확한 프로그램을 할 수 있게 되었다
Raw 타입은 사용하지 말라
제네릭 타입(제네릭 클래스, 제네릭 인터페이스)는 일련의 매개변수화 타입을 정의한다. 예를 들어 List<String>의 경우 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다.
raw타입이란 제네릭 타입에서 매개변수를 전혀 사용하지 않을 때를 말한다. 따라서 사용자가 엉뚱한 타입의 인스턴스를 넣을려 해도 컴파일러가 오류를 내지 않는다
raw 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다. raw 타입을 절대 써서는 안된다
- 모든 원소 허용한다면 <Object>
- 원소의 타입을 몰라도 된다면 <?> (비한정 와일드 카드)
- Collection<?>에는 (null 외에는) 어떤 원소도 넣을 수 없다. 꺼낼수 만 있음
- class 리터널에는 raw 타입을 써야 한다
- List.class 허용하지만 List<String>.class 는 허용하지 않는다
- 런타임에 제네릭 타입 정보가 지워지므로 instanceOf 연산자는 raw 타입 이든 <?> 타입이든 똑같이 존재 한다
// raw 타입으로 써도 좋은 예 if(o instanceof Set){ Set<?> set = (Set<?>) o; }
정리
raw 타입을 사용하면 런타임 예외가 일어날 수 있으니 사용하면 안된다. Set<Object>와 Set<?> 는 안전하지만 raw 타입 Set은 안전하지 않다
비검사 경고를 제거하라
컴파일러가 알려주는 비검사 경고를 수정하여 할 수 있는 한 모든 비검사 경고를 제거하라. 모두 제거한다면 그 코드는 타입 안정성이 보장된다.
경고를 제거할 수 없지만 타입 안전하다고 확신이 든다면 @SuppressWarning 애노테이션을 달아 경고를 숨겨라. @Suppresswarning은 개별 지격변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있으므로 가능한 좁은 범위에 적용하자.
* 애노테이션은 선언에만 달 수 있다.
정리
비검사 경고는 중요하니 무시하지 말자. 모든 비검사 경고는 런타임에 ClassCastExzception을 일으킬 수 있는 잠재적 가능성을 가진다. 따라서 모든 비검사 경고를 제거하는 것이 좋다
배열보다는 리스트를 사용하라
배열과 제네릭 타입에는 중요한 차이가 있다. 배열은 공변이고 제네릭은 불공변이다.
공변이란
자기 자신과 자식 객체로 타입 변환을 허용해준다
Object[] before = new Long[1];
그렇기 때문에 위와 같은 문법을 허용시켜 준다
불공변이란
List<String>과 List<Object>가 있을 때 두 개의 타입은 전혀 관련이 없다는 뜻입니다.
public class Test {
public static void test(List<Object> list) {
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Gyunny");
test(list); // 컴파일 에러
}
}
제네릭이 불공변이 아니라면 위의 코드가 컴파일 에러가 발생하지 않는다. 하지만 불공변이라는 자기와 타입이 같은 것만 같다고 인식하는 특징 때문에 컴파일 에러가 발생한다
이러한 특성 때문에 제네릭이 컴파일 타임에 타입 안정성을 가지는 장점을 가질 수 있다.
정리
배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에 안전하지 않지만 컴파일 타임에는 그렇지 않다, 제네릭은 그 반대이다. 따라서 둘을 섞어 쓰다가 오류나 경고를 만나면 배열을 리스트로 대체하는 방법을 적용해보자.
* 배열에서 제네릭 같은 실체화 불가 타입으로 배열을 만들 수 없다
public class Test<E>{
// 불가능, 실체화 불가 타입으로 배열 만들 수 없음
private static E[] elements = new E[];
// 가능
private static E[] elements = E[] new Object[];
}
이왕이면 제네릭 타입으로 만들라
클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 따라서 새로운 타입을 설계할 때 형변환 없이도 사용할 수 있도록 하라. 형변환 없이 사용하려면 제네릭 타입으로 만들어야 하는 경우가 많다. 기존 타입 중 제네릭이었어야 하는 것이 있다면 제네릭 타입으로 변경해라
이왕이면 제네릭 메서드로 만들라
메서드도 제네릭으로 만들 수 있다.
// 타입 안전하지 않는 방식
public static Set union(Set s1, Set s2){
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
// 타입 안전한 방식
// 타입 매개변수(ex. E) 목록은 메서드의 제한자와 반환 타입 사이에 온다
public static <E> Set<E> union(Set<E> s1, Set<E> s2){
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다.
한정적 와일드 카드를 사용해 API 유연성을 높여라
Upper Bounded Wildcards (extends를 사용한 한정적 와일드카드)
: 타입의 제한을 풀어줄 때 사용합니다. 제네릭 타입들을 상위 제네릭 타입으로 묶어주는 것이라고 할 수 있습니다.
public static void process(List<? extends Foo> list) { /* ... */ }
Lower Bounded Wildcards (super를 사용한 한정적 와일드카드)
: 타입을 제한할 때 사용합니다. 유연성을 극대화하기 위해 지정된 타입의 상위 타입만 허용하도록 합니다.
// Integer의 상위 타입일 경우 허용 (Object, Number, Integer)
public static void addNumbers(List<? super Integer> list) { ... }
유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드 카드를 사용하라. 한편 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드 타입을 써도 좋을 것이 없다.
다음은 어떤 와일드 카드를 써야하는 지 도움을 주는 기본 원칙이다.
펙스(PECS) : producer-extends, conusmer-super
즉, 매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용해라
* 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라
정리
조금 복잡하더라도 와일드 카드 타입을 적용하면 API가 훨씬 유연해진다. PECS 공식을 기억하자
제네릭과 가변인수를 함께 쓸 때는 신중하라
가변인수(varargs) 메서드
키워드 ... 을 사용한다
void sum(String...str) {
for(String a:str)
System.out.println(a);
}
다음과 같이 타입...변수명 으로 사용한다.
가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다.
메서드가 제네릭 vararages 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
안전하게 만들기 위해서는 메서드가 varageages 배열에 아무것도 저장하지 않고(배열 변수 덮어쓰지 않고) 그 배열의 참조가 밖으로 노출되지 않는다면(신뢰할 수 없는 코드가 배열에 접근 금지) 안전하다
주의 : 시점에 따라 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단 할 수 있다
// pickTwo에서 컴파일러가 toArray에 넘길 T 인스턴스를 2개를 담을 vararges 매개변수 배열을 만드는 코드 생성
// 코드가 만드는 배열의 타입은 Object[], pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 가장 구체적인 타입이기 때문
public static <T> T[] pickTwo(T a, T b, T c){
switch (ThreadLocalRandom.current().nextInt(3)){
// pickTwo 항상 Object[] 타입 배열 반환환
case 0 : return toArray(a,b);
case 1 : return toArray(a,c);
case 2 : return toArray(b,c);
}
throw new AssertionError();
}
// 메서드가 반환하는 배열의 타입은 이 메서드에 인수를 넘기는 컴파일 타입에 결정된다
// 매개변수 배열을 return
public static <T> T[] toArray(T ... args){
return args;
}
public static void main(String[] rgs) throws IOException{
// Object[] 는 String[]의 하위 타입이 아니므로 오류 발생
String[] sses = Test.pickTwo("ss", "112","ttt");
}
정리
가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고 배열은 공변이고 제네릭은 불공변이다. 가변인수를 사용할 때 는 안전하게 만들어야 한다.
타입 안정 이종 컨테이너를 고려하라
컨테이너 대신 키를 매개변수화한 다음 컨테이너에 값을 넣거나, 뺄때 키 타입을 제공해주면 된다.
이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해 줄 것이다.
이러한 설계 방식을 타입 안전 이종 컨테이너 패턴이라고 한다.
다음은 타입별로 인스턴스를 저장하고 검색할 수 있는 클래스이다.
// 타입 안정 이종 컨테이너 패턴 - API
public class Favorites{
public <T> void putFavorite(Class<T> type, T instance);
public <T> getFavorite(Class<T> type);
}
// 타입 안정 이종 컨테이너 패턴 - 클라이언트
public static void main( final 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());
}
Favorites 인스턴스는 타입 안전하다. String을 요청했는데 Integer를 반환하는 일은 없다
타입 안정 이종 컨테이너 제약
- 악의적인 사용자가 Class객체를 제네릭이 아닌 raw 타입으로 넘기면 Favorites의 인스턴스 안정성이 쉽게 깨진다
- 해결 : putFavorites에셔 type.cast(instacne)로 Class<T> 제네릭을 이용해 동적으로 형변환한다
- Favorites 클래스는 실체화 불가 타입에는 사용할 수 없다. 즉 String이나 String[] 은 가능하지만 List<String>은 불가능하다
정리
일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정 되어 있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안정 이종 컨테이너를 만들 수 있다. 타입 안정 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
'자바' 카테고리의 다른 글
이펙티브 자바 -6- (0) | 2022.01.12 |
---|---|
이펙티브 자바 -5- (0) | 2022.01.07 |
이펙티브 자바 -3- (0) | 2022.01.03 |
이펙티브 자바 -2- (0) | 2021.12.30 |
이펙티브 자바 -1- (0) | 2021.12.29 |