티스토리 뷰

728x90
반응형

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


  • 제네릭은 타입 안정성을 보장해주고 그렇기 때문에 형변환과 타입추론을 생략해줍니다. 그래서 이런 장점을 최대한 살려서 제네릭 타입으로 코드를 작성해주는것이 좋습니다.

💡제네릭을 사용하지 않은 Stack 

  • 아래 Stack 예제는 제네릭을 사용하지 않은 Object 배열 기반의 Stack입니다.
  • 아래 예제를 사용해도 문제는 발생하지 않지만 왜 제네릭으로 리펙토링을 해야할까요? 
    • 그 이유는 pop 메서드를 통해 꺼낸 객체를 형변환을 해야하는데 이때 런타임 오류가 발생할 수 있습니다. 또한 형변환을 위해 매번 형변환하는 코드를 작성해야 합니다.
    • 아래 예제에서는 Food 객체로 형변환을 했지만 런타임시에는 예외가 발생하게 됩니다.
public class ObjectStack {

    private Object[] elements;
    private int size;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public ObjectStack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object o) {
        ensureCapacity();
        elements[size++] = o;
    }

    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);
        }
    }
}

public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {

        ObjectStack objectStack = new ObjectStack();
        objectStack.push(new Food("피자"));
        objectStack.push("String");

        Food food = (Food) objectStack.pop(); // ClassCastException --> String
    }
}

 

💡제네릭을 사용한 Stack 

  • 제네릭을 사용하였지만 생성자 부분에서 new E를 사용하여 컴파일 에러가 발생하게 됩니다. 원인은 java: generic array creation 으로 E와 같은 정규 타입 매개변수는 실체화가 불가능합니다. 그러므로 해결방법 2가지를 알아보겠습니다.
public class GenericStack<E> {

    private E[] elements;
    private int size;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public GenericStack() {
        this.elements = new E[DEFAULT_INITIAL_CAPACITY]; // Compile Error! 발생
    }

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

    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);
        }
    }
}

 

💡해결책 1. Object 배열을 생성하지만 제네릭 배열로 형변환

  • 일단 elements 필드는 private 접근 제어자이며 외부에서 접근이 불가능합니다.
  • 또한 push 메서드를 통해 elements에 저장될 때의 타입은 항상 E입니다.
  • 위의 특징들로 인해 이 비검사 형변환은 확실히 안전하며, 이 코드가 안전하다는걸 확인했으니 @SuppressWarnings 어노테이션을 사용해 경고를 숨기고 주석을 남겨 놓습니다.
// 배열 elemtns 는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안전성을 보장하지만, 
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다.
@SuppressWarnings("unchecked")
public GenericStack() {
    this.elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // Compile Error! 발생
}

 

💡해결책 2. elements 필드의 타입을 E에서 Object로 바꾸기

  • 이 방법을 사용하면 첫번째 해결책과는 다른 오류가 발생합니다. elements는 Object 배열이지만 pop 메서드를 통해 꺼낼 경우 E로 형변환해서 반환이 이루어지는데 이 경우 unchecked cast 경고가 발생하게 됩니다. E는 실체화 불가능한 타입이기 때문에 컴파일러는 런타임에 이루어지는 형변환이 안전한지 증명할 방법이 없기 때문에, 이 역시 첫번째 방법 처럼 직접 증명 후 경고를 숨길 수 있습니다.
public class GenericStack<E> {

    private Object[] elements;
    ... 생략

    public GenericStack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY]; // Compile Error! 발생
    }

    ... 생략

    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        //push 메서드에서 E타입만 허용하기에 이 형변환은 안전하다.
        @SuppressWarnings("unchecked")
        E result = (E) elements[--size];
        elements[size] = null;
        return result;
    }
    ... 생략
}

 

💡두 가지 해결책의 장단점

  • 첫번째 해결방법은 가독성이 좋습니다. 생성자에서 E[] 한번만 형변환을 해주면 됩니다. 하지만 단점은 E가 Object 타입이 아닌 한 런타임 타입이 컴파일 타입과 달라 힙 오염을 발생시킬 수 있습니다.
  • 두번째 해결방법은 첫번째 방법보다는 힙 오염을 발생시킬 수 있는 가능성이 적습니다.

 

💡왜 리스트가 아닌 배열을 사용했을까?

  • 아이템 28에서는 배열보다는 리스트를 사용하라하고, 위의 예제도 배열을 사용했습니다. 왜 그럴까요? 우선 제네릭 타입 안에서 리스트를 사용하는 것은 항상 가능하지도 않을 뿐더러 반드시 좋다는 보장이 없습니다. 그리고 자바에서는 리스트를 기본 타입으로 제공하지 않습니다. 그래서 컬렉션 프레임워크의 구현체들(ArrayList, HashMap 등)도 배열을 사용해 구현해야 합니다.

 

💡매개변수에 제약을 둔 제네릭 1)

  • 제네릭의 매개변수 중 기본 타입은 사용할 수 없습니다. 이는 자바 네네릭 타입 시스템의 근본적인 문제이나, 래퍼 클래스를 사용하여 우회할 수 있습니다.
// 가능
Stack<Object>, Stack<Double>, Stack<int[]>, Stack<List<String>>

// 불가능
Stack<int>, Stack<double>

 

💡매개변수에 제약을 둔 제네릭 2)

  • 예를 들어 Item 클래스를 최상위 구조로 Item <- Music <- Dance <- BreakDance 라는 상속 구조를 가진 객체가 있다고 했을 때, 여기서 Stack 클래스에 들어갈 수 있는 타입을 Dance, BreakDance라는 제약을 두면 아래와 같이 작성할 수 있습니다.
public class Stack<E extends Dance>
or
public class Stack<Dance> // BreakDance는 Dance의 하위 타입이기에 가능

 

 

참고 자료)

https://catsbi.oopy.io/83635cfe-1cab-43f2-a943-56a9efd83fb2

 

 

 

 

 

728x90
반응형