본문 바로가기
Language/Effective java

제네릭

by y.j 2022. 6. 19.
728x90

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

Stack을 제네릭 타입으로 만들어 보자.

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

 

1. 선언에 타입 매개변수를 추가하자. 타입이름으로느 보통 E를 사용한다.

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

    public Stack() {
        elements = (E[]) new Object[DEFALUT_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[])를 사용하는 것은 안전하지 않다. 형변환이 안전성을 해치지 않음을 확인하고 @SuppressWarning를 사용하여 제거한다.

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

다른 방법으로는 E대신에 Object를 사용하도록 하고 메서드에서 E로 형변환을 하는 것이다. 

public E pop() {
    if(size == 0)
        throw new EmptyStackException();
    
    @SuppressWarnings("unchecked")
    E result = (E) elements[--size];
    elements[size] = null;
    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;
}

 

불변 객체를 여러 타입으로 활용 할 수 있게 만드는 방법

소거때문에 어떤 타입이든 매개변수화 할 수 있다. 그래서 요청한 타입 매개변수에 맞게 타입 캐스팅을 해주는 정적 팩터리를 만들어야 한다.

private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
    return (UnaryOperator<T>) IDENTITY_FN;
}

UnaryOperator<Object>와 UnaryOperator<T>는 상속 관계도 아닌 서로 타른 타입이기 때문에 형변환 경고가 발생한다. 하지만 항등함수란 입력 값을 수정 없이 그대로 반환하는 특별한 함수이므로, T가 어떤 타입이든 UnaryOperator<T>를 사용해도 타입 안전하다. @SuppressWarning을 사용해서 경고를 없애주자!

 

재귀적 한정 타입

자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정한다. <E extends Comparable<E>>은 자기 자신만으로 타입을 한정시키는 표현이다.

public static <E extends Comparable<E>> E max(Collection<E> c);

 

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

다시 한번 상기시키면 List<Object>와 List<String>은 불공변이다. List<String>은 List<Object>의 하위타입이 아니라는 뜻이다. 

 

하위 타입으로 한정시키는 방법

위 pushAll함수는 언뜻보면 잘 되는 것처럼 보이지만 Stack<Number>선언하고 Integer값을 pushAll한다면 에러가 발생한다. 하위타입이라 되야 할거 같지만 제네릭은 불공변이기 때문에 하위타입이 아니라 서로 다른 타입이다. 

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

<? extends E>로 변경시키면 `E의 하위타입인 Iterable`이 인수로 받을수 있도록 강제한다. 

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

 

 

상위 타입을 정의하는 방법

아래 코드는 dst로 Stack에 있는 객체들을 옮기는 메서드이다. Stack<Number>로 정의하고 Collection<Object>에 넣는다면 괜찮을까? 당연히 안된다. 똑같이 Collection<Stack>과 Collection<Object>는 서로 다른 타입이기 때문이다.

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

E의 상위 타입으로 Collection을 한정시켜 사용해야 한다.

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

 

PECS : producer-extends, consumer-super

유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라. 생산자는 extends를 사용하고 소비자는 super를 사용하자. 타입을 정확히 지정할 때는 와일드카드를 쓰지 않아야 한다. 이것을 겟 풋 원리(Get And Put Principle)이라고 한다.

 즉, 매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용하라.

 


아이템 28 Choser Method 수정하기.

public Chooser(Collection<? extends T> choices)

 

아이템 30-2 uinon Method 수정하기.

반환타입에서는 한정적 와일드카드 타입을 사용하면 안된다.

public static <E> set<E> union(Set<? extends E> s1, Set<? extends E> s2)

 

명시적 타입 인수 ( 자바 7까지는 해줘야 한다. )

자바 7까지는 올바른 타입을 수정하지 못할 때 명시해줘야 한다.

Set<Number> numbers = Union.<Number>union(intergers, doubles);

 

코드 30-7 수정하기.

public static <E extends Comparable<? super E>> E max(List<? extends E> list)

기존의 List<E>는 E를 반환하는 생성자이므로 <? extends E>로 바꿔줄 수 있다. 

Comparable은 소비자이므로 <? super E>로 바꿔주면 된다.


 

메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라.

타입 매개변수와 와일드카드에는 공통된 부분이 있어서 메서드를 정의 할 때 둘 중 어느것을 사용해도 괜찮을 때가 많다.

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

public API라면 2번째가 더 낫다. 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해 줄 것이며 신경 써야 할 매입 매개변수도 없다.

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

public static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

 

 

 

728x90

'Language > Effective java' 카테고리의 다른 글

열거 타입과 애너테이션  (0) 2022.07.13
제네릭  (0) 2022.06.26
제네릭  (0) 2022.06.18
클래스와 인터페이스  (0) 2022.06.17
클래스와 인터페이스  (0) 2022.06.17

댓글