[교재 EffectiveJava] 아이템 29. 이왕이면 제네릭 타입으로 만들라

반응형
728x90
반응형

배열 코드를 제네릭 코드로 변경

제네릭 타입과 메서드를 사용하는 일은 일반적으로 쉬운 편이지만, 제네릭 타입을 새로 만드는 일은 조금 더 어렵다.

 

제네릭을 사용하지 않은 기본 코드
package com.java.effective.item29;

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

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

    public Stack() {
        elements = new Object[DEFAULT_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);
        }
    }
    
    // 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
    public static void main(String[] args) {
        Stack stack = new Stack();
        for (String arg : List.of("a", "b", "c"))
            stack.push(arg);
        while (!stack.isEmpty())
            // Object 타입이므로 형변환이 필수
            System.out.println(((String)stack.pop()).toUpperCase());
    }
}

 

이 클래스는 Object[] elements 배열이 제네릭 타입이어야 마땅하다. 제네릭으로 바꿈으로써 단점을 장점으로 바꿀 수 있다.

 

위 코드는 해당 코드를 호출하는 클라이언트에서 형변환을 잘못했을때 런타임 오류가 발생할 위험이 있다. 

// Object 타입이므로 형변환이 필수
System.out.println(((String)stack.pop()).toUpperCase());

 

제네릭을 사용한 코드
package com.java.effective.item29;

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

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

    public StackGeneric() {
        elements = 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);
        }
    }
}

 

위 코드는 오류가 뜬다.

public StackGeneric() {
    elements = new Object[DEFAULT_INITIAL_CAPACITY]; // 컴파일 에러 발생
}

 

E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다. 배열을 사용하는 코드를 제네릭으로 바꾸려하면 이 문제가 항상 발생할 것이다. 

 

 

해결책 1. 제네릭 배열 생성

 public StackGeneric() {
     // 제네릭 배열은 만들 수 없다.
     // elements = new E[DEFAULT_INITIAL_CAPACITY];
     
     // Object로 선언하고 형 변환을 이때 1번만 한다.
     elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 컴파일 에러 발생
 }
 
// 코드 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());
}

 

컴파일러의 오류가 사라지고 경고가 생긴다. 위 코드로 오류는 해결했지만 타입 안전하지 않다. 컴파일러는 해당 프로그램이 타입 안전한지 증명할 방법이 없지만 우리는 가능하다. 이 비검사 형변환이 프로그램의 타입 안전성을 해치지 않음을 우리가 스스로 확인해야 한다. 배열 elements 는 private 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 거의 없다. push  메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다. 따라서 이 비검사 형변환은 확실히 안전하다.

 

@SuppressWarnings("unchecked") 추가하여 경고 숨김처리
@SuppressWarnings("unchecked")
public StackGeneric() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 컴파일 에러 발생
}

 

비검사 형변환의 안전함을 직접 증명했으므로 @SuppressWarnings("unchecked") 을 사용하여 경고를 숨긴다. 생성자 전체에 경고를 숨겨도 문제가 없다.

중요한 것은, 배열 elements 의 런타입 타입은 E[] 가 아닌 Object[] 다. (제네릭의 런타임 시점에 소거 특징)

 

 

해결책 2. element 필드의 타입을 변경 

package com.java.effective.item29;

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

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

//    @SuppressWarnings("unchecked")
//    public StackGeneric() {
//        elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 컴파일 에러 발생
//    }

    public StackGeneric() {
        elements = 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());
    }
}

 

elements 배열의 타입을 Object[] 로 변경했다.

 

또다시 오류가 발생한다.

public E pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    E result = elements[--size];
    elements[size] = null; // 다 쓴 참조 해제
    return result;
}

 

E result 로 받는 부분이 문제다. 이 부분도 형변환으로 수정하자.

// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
E result = (E) elements[--size];

 

오류는 사라지고 경고가 뜬다. E는 실체화 불가 타입이므로 컴파일러는 런타임 시에 이뤄지는 형변환이 안전한지 증명할 방법이 없다. 우리가 스스로 증명해보자. elements 배열에 원소를 추가하는 push 메서드에서 E 타입만 허용하므로 위 코드는 타입 안전하다.

@SuppressWarnings("unchecked")
E result = (E) elements[--size];

 

이번엔 해당 코드의 row 바로 위에 @SuppressWarnings("unchecked") 를 추가하였다. 

 

 

해결책1, 해결책2 정리

해결책 1의 방법이 가독성이 더 좋다. 배열의 타입을 E[] 로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필한다. 코드도 더 짧다. 또한 형변환도 생성자에 선언되어 있으므로 배열 생성시 단 한번만 실행된다. 해결책 2의 방법은 원소를 읽을때마다 형변환이 발생한다.

 

하지만 해결책 1의 경우, 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염이 발생한다. (E 가 Object 가 아닐 경우)

private E[] elements;
...

public StackGeneric() {
     // 제네릭 배열은 만들 수 없다.
     // elements = new E[DEFAULT_INITIAL_CAPACITY];
     
     // Object로 선언하고 형 변환을 이때 1번만 한다.
     elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 컴파일 에러 발생
 }
 
// 코드 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());
}

 

 

정리

package com.java.effective.item29;

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        StackGeneric<String> stack = new StackGeneric<>();

        Arrays.stream(args).forEach(stack::push);
        
        while (!stack.isEmpty()) {
            System.out.println(stack.pop().toUpperCase());
        }
    }
}

 

Stack 에서 꺼낸 원소에서 String 의 toUpperCase 메서드를 호출할때 명시적 형변환을 수행하지 않았다. (컴파일러에 의해 자동 생성된) 이 형변환이 항상 성공함을 보장한다.

 

StackGeneric<Object>, StackGeneric<int[]>, StackGeneric<List<String>>, StackGeneric 등 어떤 참조 타입으로도 StackGeneric 인스턴스를 생성할 수 있다. 단, 기본 타입은 사용할 수 없다. 자바 제네릭 타입 시스템은 기본 타입을 사용할 수 없다. (Stack<int>, Stack<double> 등)

 

 

한정적 타입 매개변수 (bounded type parameter)

class DelayQueue<E extends Delayed> implements BlockingQueue<E>

 

Delayed 의 하위 타입만 받는다는 의미이다. 이렇게 함으로써 DelayQueue 자신과 DelayQueue 를 사용하는 클라이언트는 DelayQueue 의 원소에서 형변환 없이 곧바로 Delayed 클래스의 메서드를 호출할 수 있다.

 

모든 타입은 자기 자신의 하위 타입이므로 DelayQueue<Delayed> 로도 사용할 수 있다.

▶ DelayQueue 클래스는 Delayed 인터페이스를 구현한 타입(E)을 요소로 가지는 제한된 타입 매개변수를 사용하기 때문에 'E'는 Delayed 인터페이스를 구현한 클래스 또는 하위클래스의 인스턴스여야 한다. 즉, E의 타입 매개변수로 Delayed 인터페이스를 구현한 어떤 클래스든 사용할 수 있다.

 

 

 

반응형

Designed by JB FACTORY