개발 공부/Java

이펙티브 자바 아이템 29 - 이왕이면 제네릭 타입으로 만들라 - 완벽 공략

개발인생 2022. 12. 14. 09:00
반응형

아이템 29 - 이왕이면 제네릭 타입으로 만들라 - 완벽 공략

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

한정적 타입 매개변수

한정적 타입 매개변수는 제네릭 타입을 특정 타입으로 한정지을 수 있는 기능이다.

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

    // 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
    // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
    // 따라서 타입 안전성을 보장하지만,
    // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
//    @SuppressWarnings("unchecked")
    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) new Object[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);
    }

    // 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        for (String arg : List.of("a", "b", "c"))
            stack.push(arg);
        while (!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}

Stack 클래스를 컴파일 할 때 E 라는 타입은 Object 로 바뀌게 된다.

우리가 만약 타입을 한정짓고 싶다면

예를들어 Stack 클래스는 숫자만 받을 수 있도록하고 싶다면

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

    // 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
    // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
    // 따라서 타입 안전성을 보장하지만,
    // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
//    @SuppressWarnings("unchecked")
    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) new Object[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);
    }

    // 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        for (String arg : List.of("a", "b", "c"))
            stack.push(arg);
        while (!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}

위와같이 Stack<E extends Number>로 작성한다.

이렇게하면 Number 라는 클래스 또는 인터페이스를 구현하거나 상속한 클래스들로만 제한이된다.

Number 를 확장한 모든것들로 제한 이 된다.

Integer 는 포함이 되지만 String 은 포함되지 않는다.

public class Main {
    public static void main(String[] args) {
        Stack<String> stack = new Stack<>(); // 컴파일 에러
        for (String arg : List.of("a", "b", "c"))
            stack.push(arg);
        while (!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}

Stack<E extends Number> 클래스에 String 을 넣으려고하면 컴파일 에러가 발생한다.

public class Main {
   public static void main(String[] args) {
      Stack<Integer> stack = new Stack<>();
      for (Integer arg : List.of(1, 2, 3))
         stack.push(arg);
      while (!stack.isEmpty())
         System.out.println(stack.pop());
   }
}

하지만 위처럼 Integer 로 바꾼다해서 코드는 잘 동작하지 않는다.

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

    // 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
    // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
    // 따라서 타입 안전성을 보장하지만,
    // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
//    @SuppressWarnings("unchecked")
    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) new Object[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);
    }

    // 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        for (String arg : List.of("a", "b", "c"))
            stack.push(arg);
        while (!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}
private E[] elements; // 이 부분이 Number 의 배열로 변환

Stack<E extends Number>로 타입을 제한하고 나면 기존의 Obejct 배열이

Number 의 배열로 바뀐다.

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

위의 부분도 Number 의 배열로 캐스팅된다.

Stack<E> 를 컴파일 되었을 때 EObject 로 바뀌었다면

Stack<E extends Number> 를 컴파일 하면 ENumber 로 바뀌게 된다.

push()pop() 역시 ENumber 로 바뀌게 된다.

public class Main {
   public static void main(String[] args) {
      Stack<Integer> stack = new Stack<>();
      for (Integer arg : List.of(1, 2, 3))
         stack.push(arg);
      while (!stack.isEmpty())
         System.out.println(stack.pop());
   }
}

때문에 위의 코드에서는 Object 배열을 Number 의 배열로 바꾸려고 해서 에러가 난 것이다.

Object 배열은 아무거나 넣을 수 있기 때문에 Number 의 배열로 바꾸는게 위험하다.

반대로 Number 의 배열을 Object 배열로 바뀌는 건 가능하다.

공변이기 때문이고, 모든 NumberObject 를 상속받았기 때문이다.

// E[]를 이용한 제네릭 스택 (170-174쪽)
public class Stack<E extends Number> {
    private Number[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    // 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
    // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
    // 따라서 타입 안전성을 보장하지만,
    // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
    @SuppressWarnings("unchecked")
    public Stack() {
        elements = new Number[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

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

위와같이 배열을 Number 의 배열로 선언하고 값을 꺼낼 때 E 로 캐스팅하면 해결된다.

이때 경고 메세지가 발생하는데 해당 배열에 들어가는 값은 전부 E 타입이 보장되므로

@SuppressWarnings("unchecked") 를 사용해 경고 메세지를 무시할 수 있다.

이때 @SuppressWarnings("unchecked")가장 작은 단위로 사용한다.

이렇게 한정적 타입 매개변수를 사용하면 제한한 타입의 인스턴스를 만들거나 메서드를 호출할 수도 있다.

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

push(E e) 메서드는 E 라는 타입으로 매개변수를 받지만

해당 매개변수를 통해 Number 클래스가 가진 모든 메서드들을 사용할 수 있다.

타입 한정에는 여러가지 클래스나 인터페이스를 다수로 선언할 수 있다.

이때 받는 타입은 선언한 모든 것들을 구현하고 있어야한다.

public class Stack<E extends Number & Serializable> {

}

예를들어 위처럼 선언하면 실제타입 매개변수는 NumberSerializable 을 전부 상속하고 구현하고 있어야한다.

이렇게 다수의 클래스와 인터페이스를 선언할 때는 클래스를 먼저 적어야한다.

반응형