제너릭 타입을 하나 정의하면 그에 따른 로타입도 함께 정의된다. 로타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 경우
를 말한다.
로우타입을 사용하게 되면 제네릭이 안겨주는 안정성과 표현력을 모두 잃게 된다.
안되는 예시
private final Collection stamps = ... ;
: 위처럼 사용하면 경고만 나오고 잘못된 코드인데 컴파일
되는 예시
private final Collection<Stamp> stamp = ...;
로 대신에 와일드 카드를 사용하는 것이 더 안정적이다. 타입 안정적이며 유연하다.
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }
로 타입으로 사용하지 말라는 것에도 예외가 있다.
1) 클래스 리터럴에는 로 타입을 써야한다.
List.class, String[].class, int.class 는 허용
List<String>.class, List<?>.class
2) instaceof
를 사용하는 올바른 예다. 런타임에는 제너릭 타입이 지워지므로 차라리 타입을 쓰는 것이 좋다.
if ( o instanceof Set) {
Set <?> s = ( Set<?> ) o;
}
제네릭을 사용하기 시작하면 수많은 컴파일러 경고를 보게 된다. 비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수 화 가변인수 타입 경고등이다. 이런 경고는 컴파일러가 알려준 대로 수정하면 사라진다. 이렇게 할 수 있는 한 모든 비검사 경고를 제거해라
. 안전한 문제의 경고 같은 경우에는 @SuppressWarnings("unchecked")
애너테이션을 달아 경고를 숨기자. 이 어노테이션을 사용할 때 주의할 점을 최소 범위에 적용해야하고, 왜 사용했는지 이유를 주석으로 남기자.
비검사 경고
warning: [unchecked] unchecked conversion
예를 들어 다음 코드를 보면 배열타입으로 선언되어있고 문법적으로 잘못되어 있다.
Object[] objectArray = new Long[1];
ObjectArray[0] = "타입이 달라 넣을 수 없다.";
그래서 런타임 단계에서 실패한다.
아래 리스트 타입에서는
List<Object> ol = new ArrayList<Long>();
ol.add("타입이 달라 넣을 수 없다.");
컴파일 단계, 즉 런타임 단계 보다 먼저 에러를 뱉어낸다.
Long 타입에다가 String 을 넣을려고 하기 때문이다.
(List 는 인터페이스 ,ArrayList 는 클래스 )
두번째로 배열의 런타임에 자신이 담기로 한 원소 타입을 인지하고 확인하는 성격과 제너릭의 타입정보가 런타임에는 소거되는 성격이 서로 안맞는다.
제너릭과 배열은 서로 규칙이 상반된다. 되도록 같이 쓰지 말자.
정석 코드
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection<T> chocies) {
choiceList = new ArrayList<> (choices ) ;
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
클래스나 메서드를 만들때 웬만해서 제네릭 타입으로 만들자. 컬랙션의 알고리즘 메서드 binarySearch, sort 들은 모두 제너릭이다. 제너릭은 클래스나 메서드의 범용성을 넓혀준다.
또한 안정성에도 좋다.
하지만 앞서 말했듯이 배열로 만든 클래스나 메서드는 제너릭이랑 성질이 안맞는데, 이때는 2가지 방법으로 우회할 수 있다.
1) 제너릭 배열을 생성 금지하는 제약을 대놓고 우회하는 방법
2) elements 필드의 타입을 E[]
에서 Object[]
로 변경하는 것
와일드 카드란 무엇인가?
https://velog.io/@borab/Generics
와일드 카드
는
< ? extends T > # 와일드 카드의 상한 제한. T와 그 자손만 가능
< ? super T > # 와일드 카드의 하한 제한. T와 그 조상만 가능
< ? > # 제한 없음. 모든 타입이 가능 < ? extends Object > 와 동일
위와 같은 제너릭의 사용법이다.
와일드 카드를 사용하지 않은 다음 케이스에서는 문제되는 코드도
public void pushAll(Iterable<E> src ) {
for( E e : src) {
push(e); // 타입 변수가 불공변이기 때문에
}
}
와일드 카드를 사용하면 손쉽게 문제를 해결 할 수 있다.
public void pushAll(Iterable<? extends E> src ) {
for( E e : src) {
push(e);
}
}
여기서 의미는 E의 하위타입이라는 뜻이다.
유연성을 극대화 하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드 카드 타입을 사용해라.
여기서 PECS 공식
을 기억하자.
즉, 생산자는 extends를 소비자는 super를 사용한다는 공식이다. 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드 카드 타입을 사용해라.
## 생산자
pubic void pushAll(Iterable<? extends E> src) {
for( E e : src) {
push(e);
}
}
## 소비자
public void popAll(Collection<? super E> dst) {
while(!isEmpty())
dst.add(pop());
}
가변인수
란?
varargs, 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 한다.
문제는 제너릭이랑 사용하면 타입 안정성이 깨진다.
static void dangerous(List<String> ... stringLists) {
List<Integer> intList = List.of(42);
Object[] object = stringLists;
object[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
가변 인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭은 타입 규칙이 서로 다르기 때문이다. 그래서 메서드에 제네릭 varargs 매개변수를 사용하고자 하면 @SafeVarargs
애너테이션을 달라 사용하는데 불편함이 없게끔하자. 하지만 메서드가 안전하지 않다면 사용하면 안된다.
여기서 안전하다는 기준은 다음과 같다.
1) varargs 매개 변수 배열에 아무것도 저장하지 않는다.
2) 그 배열(혹은 복제본) 은 신뢰할 수 없는 코드에 노출하지 않는다.
만약 이렇게 사용하는 방법을 사용하기 싫다면
static <T> List<T> flatten(List<List<? extends T>> lists ){
List<T> res = new ArrayList<>();
for(List<? extends T> list : lists)
res.addAll(list);
return res;
}
타입 안전 이종 컨테이너 패턴
: 컨테이너 대신 키를 매개변수화 한다음, 컨테이너에 값을 넣거나 뺄때 매개변수화한 키와 함께 제공하면된다. 이렇게 하면 제너릭 타입 시스템의 값이 타입이 키와 같음을 보장해줄 것이다.
코드로 패턴을 확인해보자
## 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 = Favorites();
f.putFavorite(String.class, "java");
f.putFavorite(Integer.class, 0xcafebebe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoritesInteger = f.getFavorite(Integer.class);
Class<?> favoritesClass = f.getFavorite(Class.class);
System.out.println("%s, %x, %s", favoriteString, favoritesInteger, favoritesClass.getName());
}
static Annotation getAnnotation( AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType =null; 비한정적 타입 토큰, 아무거나 받기 위해
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation( annotationType.asSubclass(Annotation.class);
}
컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다. 타입 안전 이종 컨테이너는 Class 객체를 타입 토큰이라고 하고 또 한 직접 구현한 키타입도 쓸 수 있다.