본문 바로가기
기타/이펙티브 자바

[이펙티브자바] 5.제네릭

by 창이2 2022. 7. 8.

1. Raw타입은 사용하지 말아라

1.1. Raw 타입(타입 매개변수가 없는 제네릭 타입)을 사용하면 제너릭의 안정성과 표현력을 잃게 된다.

더보기
public class EffectiveJava {
    private final Collection stamps = new ArrayList();
    
// 타입을 명시해서 작성
//private final Collection<Stamp> stamps = new ArrayList();
    
    
	//Compile에는 문제 없으나 Runtime시 ClassCastException 에러 발생
    public void main(){
        stamps.add(new Coin());
        stamps.add(new Stamp());

        Iterator iterator = stamps.iterator();
        while(iterator.hasNext()){
            Stamp stamp = (Stamp)iterator.next();
        }
    }

    class Coin{
        public Coin(){

        }
    }

    class Stamp{
        public Stamp(){

        }
    }
}

1.2. 비한정적 와일드 카드(?)를 통해 Raw 타입 대신 사용하라.

비한정적 와일드 카드는 어떤타입이 올지 모르면서도 제너릭을 쓰고싶을때 사용하며 null이외에는 어떤 원소도 넣을 수 없다.

 

2. 비검사 경고를 제거하라

2.1. SuppressWarnings은 클래스 전체가 아닌 구체적인 부분에 적용하라. 섬세하게 경고를 잡을 수 있다.

 

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

3.1. 배열은 공변(함께 변한다는 뜻으로 Sub가 Super의 하위 타입이라면 Sub[] 는 Super[] 의 하위타입)이고 리스트는 반대로 불공변 이다.

더보기
//상위 Object가 Long으로 변한다.
Object[] obj = new Long[1];
obj[0] = "문자열을 넣으면 에러가 납니다."; // Runtime에서 에러 발생
        
//List는 불공변이기 때문에 Compile Time에서 에러가 발생
List<Object> list = new ArrayList<Long>();

3.2. 제너릭 배열은 자바에서 제공하지 않는다. 왜냐하면 런타임에 Exception이 일어날 수 있기때문에 제너릭의 취지와는 어긋나기 때문이다.

3.3. 배열은 컴파일 타임에 타입이 안전하지 않지만 런타임에 타입이 안전하다. 반면 제너릭은 컴파일타임 타입이 안전하지만 런타임에 타입이 안전하지 않다.(런타임에 타입 소거)

 

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

3.1. 제네릭이 적용되지 않은 Stack 클래스에서 제네릭으로 만드는 2가지 방법 

  3.1.1 스택 생성시 데이터 배열을 (E) 로 한번만 캐스팅하는 방법 (런타임 타입이 컴파일 타입과 달라 힙오염)

  3.1.2 매번 데이터를 참조할때마다 (E) 로 캐스팅 (실제 현재 적용된 스택 구현 방법)

 

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

4.1. <? extends E> 는 E의 매개변수 타입이 아니라 E의 하위 타입의 매개변수 라는 의미이다.

더보기
Stack<Number> numberStack = new Stack<>();
ArrayList<Integer> arrayList = new ArrayList<>();
Iterable<Integer> integers = arrayList;
numberStack.pushAll(integers);

public void pushAll(Iterable<? extends E> src){
    for(E e : src) push(e);
}

//pushAll이라는 함수를 정의하려고 할때 Number의 하위 타입이라는 것을 명시하면 더 유연해진다

4.2. <? super E> 는 E의 매개변수 타입이 아니라 E의 상위 타입의 매개변수 라는 의미이다.

더보기
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = new ArrayList<>();
numberStack.popAll(objects);

public void popAll(Collections<? super E> dst){
    while(!isEmpty()) dst.add(pop());
}
//objects들을 옮겨 담으려고 할때 E의 상위타입(Object 등) 을 지정하면 더 유연하게 대응할 수 있다.

4.3. PECS 원칙을 지켜라(Producer-Extends, Consumer-Super)

4.4. 함수의 반환타입에는 한정적 와일드카드를 사용하면 안된다. 클라이언트가 한정적 와일드카트를 써야하기 때문이다.

4.5. parameter는 메서드에서 정의한 변수이고 argument는 함수 호출시 넘기는 실제 값 이다.

void add(int value) // parameter

add(10) // argument

 

5. 제네릭과 가변인수를 함께 쓸 때는 신중하라.

5.1. 제네릭과 varargs 는 궁합이 좋지 않다. varargs 는 내부적으로 배열을 만드는데 이때문에 추상화가 완벽하지않다.

 

6. 타입 안전 이종 컨테이너를 고려하라

6.1.타입 안전 이종 컨테이너는 타입을 제너릭을 키값으로 하여 제네릭 타입의 값이  키값과 같은 타입임을 보장한다.

6.2. Raw 타입으로 인자를 넘기면 타입 안정성이 깨진다.

6.3. 실체 불가 타입에는 사용할 수 없다. 예를들면 List<String>은 Class객체를 얻을 수 없다.

class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();
    
    //Class 타입을 키값으로 한다.
    public <T> void putFavorite(Class<T> type, T instance){
        favorites.put(Objects.requireNonNull(type), instance);
    }

	//
    public <T> T getFavorite(Class<T> type){
        return type.cast(favorites.get(type));
    }
    
}

//타입안정성이 깨진다.
favorites.putFavorite((Class)Integer.class, "11111");

 

7. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라.

7.1. 마커 인터페이스는 이를 구현한 클래스의 인스턴스를 구분 지을수 있다.

7.2. 마커 인터페이스는 적용 대상을 더 정밀하게  지정할 수 있다.

7.3. 마커 애너테이션을 적극적으로 활용하는 프레임워크는 마커 애너테이션을 활용하는것이 좋다.

댓글