💡 item 26 : raw 타입은 사용하지 말자
raw 타입이란 제네릭 타입에서 타입 매개 변수를 전혀 사용하지 않은 타입을 말한다. 현재로서는 제네릭 이전의 코드와 호환되기 위해 사용될 뿐, 런타임 시점에 오류를 발생할 소지가 많다
컬렉션의 raw 타입
private final Collection stamps = ...;
...
stamps.add(new Coin(...)); // 실수로 동전을 넣는다.
private final Collection<Stamp> stamps = ...;
<Object>
List
와 달리 List<Object>
는 모든 타입을 허용한다는 의사를 컴파일러에 전달한 것이다.List
를 받는 메서드에 List<String>
을 넘길 수 있지만, List<Object>
를 받는 메서드에는 넘길 수 없다.
//가능
public static void main(String[] args) {
final List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0);
}
private static void unsafeAdd(final List list, final Integer valueOf) {
list.add(0);
}
//불가능
public static void main(String[] args) {
final List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0);
}
private static void unsafeAdd(final List<Object> list, final Integer valueOf) {
list.add(0);
}
?
)를 사용하자.List.class, String[].class, int.class
if (o instanceof Set) { // 로 타입
Set<?> s = (Set<?>) o; // 와일드카드 타입
}
instanceof
연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.💡 item 27 : **비검사 경고를 제거하라**
비검사 경고가 발생하는 코드
Set<Lark> exaltation = new HashSet();
해결한 코드
Set<Lark> exaltation = new HashSet<>();
@SuppressWarnings("unchecked")
를 이용해 비검사 경고를 숨기자.@SuppressWarnings("unchecked")
는 가능한 좁은 범위에 적용하자.@SuppressWarnings
애너테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다.@SuppressWarnings
가 달려있다면 지역변수나 아주 짧은 메서드 혹은 생성자로 옮기자.[기존 ArrayList의 toArray 메서드]
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
@copyOf()
@ 부분에서 경고가 발생한다. 이 경고를 제거하려면 지역변수를 추가해야 한다.[지역변수를 추가해 @SuppressWarnings의 범위를 좁힌다.]
public <T> T[] toArray(T[] a) {
if (a.length < size)
// 생성한 배열과 매개변수로 받은 배열이 모두 T[]로 같으므로
// 올바른 형변환이다.
@SuppressWarnings("unchecked")
T[] result = (T[]) Arrays.copyOf(elementData, size, a.getClass());
return result
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
@SuppressWarnings("unchecked")
에너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야한다.💡 item 28 : raw **배열보다는 리스트를 사용하라**
첫 번째 - 배열은 공변인 반면 리스트는 불공변이다.
공변: 함께 변한다
불공변: 함께 변하지 않는다.
Sub
가 Super
의 하위 타입이라면 Sub[]
는 배열 Super[]
의 하위 타입이 된다.Type1
, Type2
가 있을 때, List<Type1>
은 List<Type2>
의 하위 타입도 아니고 상위 타입도 아니다.두 번째 - 배열은 실체화된다.
Long
타입 배열에 String
타입 데이터를 입력하려고하면 ArrayStoreException
이 발생한다.[제네릭 배열 생성을 허용하지 않는 이유 - 컴파일되지 않는다.]
List<String>[] stringLists = new List<String>[1]; // (1) 허용된다고 가정해보자.
List<Integer> intList = List.of(42); // (2) 원소가 하나인 List<Integer> 생성
Object[] objects = stringLists; // (3) stringLists를 objects에 할당
objects[0] = intList; // (4) intList를 objects의 첫번째 원소로 저장한다.
String s = stringLists[0].get(0); // (5) stringList[0]에 들어가있는 첫번째 요소는 Integer이므로 형변환 오류 발생.
ClassCastException
이 발생할 수 있기 때문에 타입 안전하지 않기 때문이다.ClassCastException
이 발생하는 것을 막아주겠다는 제네릭 타입 시스템 취지에 어긋나는 일이기도 하다.배열은 실체화 불가 타입
E
, List<E>
, List<String>
같은 타입을 실체화 불가 타입이라고 한다.List<?>
, Map<?,?>
같은 비한정적 와일드카드 타입뿐이다.@SafeVarargs
@SafeVarargs
는 메서드 작성자가 해당 메서드가 타입 안전하다는 것을 보장하는 장치이다.
@SafeVarargs
를 사용하면 잠재적 오류에 대한 경고를 무시함으로써 해결할 수 있다. 만약, 메서드가 타입 안전하지 않다면 절대 @SafeVarargs
를 사용해서는 안된다.public class SafeVars {
@SafeVarargs
public static void print(List... names) {
for (List<String> name : names) {
System.out.println(name);
}
}
public static void main(String[] args) {
SafeVars safeVars = new SafeVars();
List<String> list = new ArrayList<>();
list.add("b");
list.add("c");
list.add("a");
print(list);
}
}
배열대신 컬렉션을 사용하자
E[]
대신 List<E>
를 사용하면 해결된다.[Chooser - 제네릭 적용 필요]
public class Chooser {
private final Object[] choiceArray;
public Chooser(final Object[] choiceArray) {
this.choiceArray = choiceArray;
}
public Object choose(){
Random random = ThreadLocalRandom.current();
return choiceArray[random.nextInt(choiceArray.length)];
}
}
[리스트 기반 Chooser - 타입 안정성 확보]
public class ListChooser {
private final List<T> choiceList;
public ListChooser(final Collection<T> choices) {
this.choiceList = new ArrayList<>(choices);
}
public T choose(){
Random random = ThreadLocalRandom.current();
return choiceList[random.nextInt(choiceList.size())];
}
}
질문?
위 두 코드 차이가 뭐임?
[Chooser - 제네릭 적용 필요]
public class Chooser {
private final Object[] choiceArray;
public Chooser(final Object[] choiceArray) {
this.choiceArray = choiceArray;
}
public Object choose(){
Random random = ThreadLocalRandom.current();
return choiceArray[random.nextInt(choiceArray.length)];
}
}
위 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다. 만약 타입이 다른 원소가 들어있으면 런타임시에 형변환 오류가 발생한다.
[리스트 기반 Chooser - 타입 안정성 확보]
public class ListChooser {
private final List<T> choiceList;
public ListChooser(final Collection<T> choices) {
this.choiceList = new ArrayList<>(choices);
}
public T choose(){
Random random = ThreadLocalRandom.current();
return choiceList[random.nextInt(choiceList.size())];
}
}
💡 item **29 : 이왕이면 제네릭 타입으로 만들라**
일반 클래스를 제네릭 타입으로 변경
[Object 기반 스택 - 제네릭이 절실한 강력후보]
E
를 사용한다public class ObjectStack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public ObjectStack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object 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);
}
}
}
타입 매개변수 오류를 해결하는 두가지 방법
public class ObjectStack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public ObjectStack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object 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);
}
}
}
public class GenericStack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public GenericStack() {
this.elements = new E[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);
}
}
}
E
와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문이다.타입 매개변수 오류를 해결하는 두가지 방법
1. Object
배열을 생성하고 그 다음 제네릭 배열로 형변환하는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법.
[Object 배열 생성 시 배열 형변환]
...
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public GenericStack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 경고 메시지 발생
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
...
// 비검사 형변환 경고 메시지 발생
Unchecked cast: 'java.lang.Object[]' to 'E[]
elements
배열은 private
필드에 저장된 후 클라이언트로 반환되거나 다른 메서드에 전달되지 않는다.push
메서드를 통해 배열에 저장되는 원소의 타입은 항상 E
이다.@SuppressWarnings("unchecked")
를 사용하여 경고를 숨긴다.[비검사 형변환 경고 숨김]
...
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
// 배열 elements는 push(E)로 넘어온 E인스턴스만 남는다.
// 타입 안정성을 보장하지만 이 배열의 런타임 타입은 Object[] 이다.
@SuppressWarnings("unchecked")
public GenericStack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 경고 메시지 발생
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
...
// 비검사 형변환 경고 메시지 발생
Unchecked cast: 'java.lang.Object[]' to 'E[]
2. elements
필드의 타입을 E[]
에서 Object[]
로 바꾸는 방법.
[Object 배열은 그대로 두고 pop() 사용시 형변환]
// 비검사 경고를 적절히 숨긴다.
public class GenericStack<E> {
private Object[] elements;
...
public GenericStack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
@SuppressWarnings("unchecked")
E result = (E) elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
...
}
제네릭 배열 생성을 제거하는 두 방법의 장단점
E
가 Object
가 아니기 때문에 (배열의 컴파일타임의 타입과 런타임의 타입이 다르기 때문에) 힙 오염을 일으킨다.이 item에 대해 얘기해보자
💡 item **아이템30 : 이왕이면 제네릭 메서드로 만들라**
제네릭 메서드 작성방법
Collections
의 binarySearch
, sort
등 알고리즘 메서드는 모두 제네릭이다.[제네릭 메서드]
public static <E> Set<E> union (Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
제네릭 싱글턴 팩터리
[제네릭 싱글턴 팩터리 패턴 - 항등함수]
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarinings("unchecked")
public static <T> UnaryOperator<T> identityFunction(){
return (UnaryOperator<T>) IDENTITY_FN;
}
T
가 어떤 타입이든 UnaryOperator<T>
를 사용해도 타입 안전하다.재귀적 한정적 타입
Comparable
과 함께 사용된다.public interface Comparable<T>{
int compareTo(T o);
}
T
는 Comparable<T>
를 구현한 타입이 비교할 수 있는 원소의 타입을 정의한다.[재귀적 타입 한정을 이용해 상호 비교 할 수 있음을 표현]
public static <E extends Comparable<E>> E max(Collection<E> c);
<E extends Comparable<E>>
가 모든 타입 E
는 자신과 비교할 수 있다는 의미를 갖는다.[재귀적 타입 한정을 이용한 최댓값 계산 메서드]
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;
}
💡**item31 한정적 와일드 카드를 사용해 API의 유연성을 높여라**
제네릭은 기본적을 불공변이다. List와 List은 서로 아무런 관계가 없는 타입이라는 뜻이다. 그러나 경우에 불공변보다 조금 더 유연한 방식이 필요할 때가 있다
→ 한정적 와일드 카드를 활용해 제네릭의 유연성을 높인다.
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
//여기에 일련의 원소들을 추가하는 메서드를 추가한다고 하자
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
이 코드는 문제는 없지만 완벽한 코드라 볼 수 없다. 그 이유는 만약 Stack로 선언한 후 pushAll(intVal)을 호출하면 어떻게 될까? intVal은 Integer이다.
결과 : 논리적으로 볼 때는 될 것 같았지만 컴파일 조차 되지 않는다. 그 이유는 불공변이기 때문에 Number를 상속한 Integer라도 Iterable와 Iterable 는 서로 아무런 연관이 없는 타입이기 때문이다.
//한정적 와일드 카드를 통해 해결한 코드
public void pushAll(Iterable<? extends E> iterable) {
for (E e : iterable) {
push(e);
}
}
<? extends E>
를 통해 우리는 와일드 카드를 활용하면 상속에 유연한 제네릭 코드를 만들 수 있음을 알았다. 그러나 비한정적 와일드 카드 적용에는 일련의 규칙이 있는데 이를 알아보자.PECS(펙스)
Producer - extends
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2);
Consumer - super
//Collection<E> 타입의 dst에 Stack 원소들을 담는 메서드이다. 이를 상속에 유연한 공변 입장에서 생각해보자..
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
다음과 같이 비교하면 쉬울 것 같다.
💡**item32 제네릭과 가변인수를 함께 쓸 때는 신중해라**
아예 이해 못함 말하면서 이해하고 싶음
제네릭 가변인수는 제대로 적용되지 않는다.
안전하게 사용하기 @SafeVarargs
1.복사해서 사용한다.
@SafeVarargs
static <T> List<T> flatten(List<? extends T> ...lists) {
for (List<? extends T> list: lists) {
result.addAll(list);
}
return result;
}
2. 수정없이 받아서 사용만 하고 외부에 varargs를 노출시키지 않는다.
3. 재정의하는 메서드에 사용하지 않는다.
List.of 를 사용하기
힙 오염에 대해 얘기해보자