Java/Effective Java 3

[이팩티브 자바3] 제 5장. 제네릭

예은파파 2020. 11. 8. 23:15

1. 로 타입(raw type)은 사용하지 말라.

 

  • 로타입(raw type) 이란? 로타입 은 제네릭(Generic) 타입에서 타입 매개변수를 전혀 사용하지 않은 때를 말한다.
  • 제네릭을 지원하기 전에는 컬렉션을 다음과 같이 선언했다.
// 컬렉션의 로 타입 - 따라하지 말 것!
// Stamp 인스턴스만 취급한다.
private final Collection stamps = ...;

// 실수로 동전을 넣는다.
stamp.add( new Coin(...) );

// 반복자의 로 타입 - 따라하지 말 것!
for( Integer i = stamp.iterator(); i.hasNext(); ) {	
	Stamp stamp = (Stamp) i.next();	// ClassCastException을 던진다.
	Stamp.cancle();
}

// 매개변수화된 컬렉션 타입 - 타입 안전성 확보!
// 이렇게 선언하면 컴파일러는 stamp에는 Stamp의 인스턴스만 넣어야 함을 컴파일러가 인지
private final Collection<Stamp> stamps = ...;

 

2. 배열보다는 리스트를 사용하라.

 

  • 배열과 제네릭에는 매우 다른 타입 규칙이 적용된다. 배열은 공변이고 실체화 되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다. (제네릭은 반대)
  • 위와 같은 이유로 둘을 섞어 쓰기란 쉽지 않다. 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용한다.
// 배열 - 런타임에 실패한다.
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다.";

// 리스트 - 컴파일되지 않는다.
List<Object> ol = new ArrayList<Long>();	// 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");

 

3. 이왕이면 제네릭 타입으로 만들라.

 

  • Object 기반의 Stack 클래스 
package Generic;

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack {

	private Object[] elements;
	private int size = 0;
	private static final int DEFAULT_INITIAL_CAPACITY = 16;
	
	public Stack() {
		
		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 );
	}
}

 

  • 제네릭 타입으로 변경한 Stack 클래스
package Generic;

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack<E> {

	private E[] elements;
	private int size = 0;
	private static final int DEFAULT_INITIAL_CAPACITY = 16;
	
	@SuppressWarnings("unchecked")
	public Stack() {
		
		elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
	}
	
	public void push(E e) {
		
		ensureCapacity();
		elements[size++] = e;
	}
	
	public Object 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 );
	}
}

 

4. 한정적 와일드카드를 사용해 API 유연성을 높여라.

 

  • 매개변수화 타입은 불공변(invariant) 이다. 즉, 서로 다른 타입 Type1 과  Type2가 있을 때, List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다.
  • 하지만 때론 불공변 방식보다 유연한 무언가가 필요하다.
// Stack 의 Public API를 추려보았다.
public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

// 와일드카드 타입을 사용하지 않은 pushAll 메서드 - 결함 존재.
public void pushAll(Iterable<E> src) {
    for(E e : src )
        push(e);
}
  • 위 pushAll 메서드는 깨끗이 컴파일되지만 완벽하진 않다. Iterable src의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동한다. 하지만 Stack<Number>로 선언한 후 pushAll(intVal)을 호출하면 오류메시지가 뜬다. 매개변수화 타입이 불공변 이기 때문이다.
// 생산자(producer) 매개변수에 와일드카드 타입 적용
public void pushAll(Iterable<? extends E> src {
    for(E e : src ) 
        push(e);
}
  • 팩스(PECS) : producer-extends, consumer-super 공식 
    - 매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용.