programming/Effective Java

[Effective Java] Chapter 5

AGAPE1225 2025. 2. 15. 22:29
반응형

Chapter 5 제네릭

 제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때마다 형변환을 했다. 그래서 누군가 실수로 엉뚠한 타입의 객체를 넣어두면 런타임에 형변환 오류가 나곤 했다. 반면, 제네릭을 사용하면 컬렉션이 담을 수 있는 타입을 컴파일러에게 알려주게 된다. 그래서 컴파일러는 알아서 형변환 코드를 추가할 수 있게 되고, 엉뚱한 타입의 객체를 넣으려는 시도를 컴파일 과정에서 차단햐여 더 안전하고 명확한 프로그램을 만들어 준다.

Item 26 로 타입은 사용하지 말라


제네릭 클래스, 제네릭 인터페이스: 클래스와 인터페이스 선언에 쓰이는 타입 매개변수

제네릭 타입: 제네릭 클래스와 제네릭 인터페이스를 통틀어 칭한다


 각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다. 먼저 클래스 이름이 나오고, 이어서 꺽쇠괄호 안에 실제 타입 매개변수들을 나열한다. 예컨대 List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다.

 

 마지막으로 제네릭 타입을 하나 정의하면 그에 딸린 로 타입도 함께 정의된다. 로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다. 예를 들어 List<E의 로 타입은 List다. 로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 도래하기 전 코드와 호환되도록 하기 위한 궁여지책이라 할 수 있다.

 

 제네릭을 지운하기 전에는 컬렉션을 다음과 같이 선언했다.

 

private final Collection stamps;

 

 해당 객체에는 어떤 타입을 입력하여도 컴파일이 가능하다. 그러니 매개변수화된 컬렉션 타입을 사용하자.

 

private final Collection<Stamp> stamps;

 

 로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 읽게 된다.

 

 List<Object> 처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다. 로 타입인 List와 매개변수화 타입인 List<Object>의 차이는 후자는 컴파일러에게 모든 객체의 타입을 혀용한다는 의미를 전달했다는 것이다.

 

 로 타입을 선언하고 싶다면 '?'를 사용하자. 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 타입을 만들어준다.

 

static int numElementsInCommon(Set<?> s1, Set<?> s2) {}

 

 Set<?>과 로 타입은 무엇이 다를까? 간단하게 말하자면 와일드카드 타입은 안전하고, 로 타입은 안전하지 않다. 로 타입은 어떤 원소도 넣을 수 있지만 Collection<?>에는 어떤 원소도 넣을 수 엇ㅂ다. 다른 타입의 원소를 넣으려하면 오류메세지가 발생한다.


Item 27 비검사 경고를 제거하라

 다이아몬드 연산자를 사용하면 실제 타입 매개변수를 추론해준다.

 

Set<Lark> exaltation - new HashSet();

 

 할 수 있는 한 모든 비검사 경고를 제거해야한다. 모두 제거한다면 그 토드는 타입 안정성이 보장된다. 즉 런타임에 ClassCastException이 발생할 일이 없다.

Item 28 배열보다는 리스트를 사용하라

배열과 제네릭 타입의 차이점은 다음과 같다.

공변

 배열은 공변이고 제네릭은 불공변이다. 공변은 Sub가 Super의 하위 타입일 때 Sub[]는 Super[]의 하위 타입이 된다는 뜻으로 함께 변함됨을 뜻한다.

 

 공변이 되면 런타임에서 버그를 발견하기 떄문에 위험하다.

실체화

 배열은 실체화된다. 이는 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다는 뜻이다. 그러나 제네릭은 타입 정보가 런타임에서 소거된다. 원소 타입을 컴파일타임에서만 검사하며 런타임에는 알수조차 없다는 뜻이다.

Item 29 이왕이면 제네릭 타입으로 만들라

일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다.

 

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

public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        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;
    }

    private void ensureCapacity() {
        if(elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }

    @Override
    public Stack clone() {
        try {
            Stack result = (Stack) super.clone();
            result.elements = elements.clone();
            return result;
        } catch (CloneNotSupportedException e){
            throw new AssertionError();
        }
    }
}

 

 여기서 제네릭 타입 E는 실체화 불가 타입으로, 배열을 만들 수 없다.

 

 해결 방법은 Object 배열을 생성한 다음 제네릭 배열로 형변환을 하는 것이다.

 

    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }

 

 해당 방법은 직관적이고 짧다. 하지만 배열의 런타임 타입이 컴파일 타임 타입과 달라 힙 오염을 일으킨다.

Item 30 이왕이면 제네릭 메서드로 만들라

- 로 타입을 사용한 메서드

public static Set union(Set s1, Set s2) {
    Set result = new HashSet(s1);
    result.addAll(s2);
    return result;
}

 

- 제네릭을 사용한 메서드

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

Item 31 한정적 와일드카드를 사용해 API 유연성을 높이라

 불공변의 법칙으로 인해 제네릭 클래스에서 발생할 수 있는 문제들.

 

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);

 

 Integer는 Number의 하위타입이니 잘 동작할 것 같지만 매개변수화 타입이 불공변이기에 위의 코드는 잘 동작하지 않는다.

 

 위의 상황을 대처할 수 있는 한정적 와일드카드타입이라는 특별한 매개변수화 타입을 지원한다. pushAll의 입력 매개변수 타입은 E의 하위타입의 Iterable이어야 하며 와일드 카드 타입 Iterable<? extends E> 가 정확하게 이런 뜻이다.

 

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

 

 E의 상위 티입이 Collection이어야 한다는 다음과 같이 작성할 수 있다.

 

    public void popAll(Collection<? super E> dst) {
        while(!isEmpty()){
            dst.add(pop());
        }
    }

 

 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.

Item 33 타입 안전 이종 컨테이너를 고려하라

 제네릭은 Set<E>, Map<K,V> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일원소 컨테이너에도 흔히 쓰인다. 이런 모든 쓰임에서 매개변수화되는 대상은 (원소가 아닌) 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다. 컨테이너의 일반적인 용도에 맞게 설계된 것이니 문제될 건 없다.

 

 하지만 더 유연한 수단이 필요할 때도 종종있다. 데이터베이스의 행은 임의 개수의 열을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있으면 멋질 것이다. 다행히 쉬운 해법이 있다. 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다. 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해줄 것이다. 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)이라 한다.

 

public class Favorites {
    public <T> void putFavorite(Class<T> type, T instance);
    public <T> T getFavorite(Class<T> type);
}

 

 

반응형

'programming > Effective Java' 카테고리의 다른 글

[Effective Java] Chapter 4  (2) 2025.02.01
[Effective Java] Chapter 3  (1) 2025.01.25
[Effective Java] Chapter 2  (3) 2025.01.18