관리 메뉴

bright jazz music

[effective java 3rd - ch.05 제네릭] Item29 : 이왕이면 제네릭 타입으로 만들라 본문

JAVA/effective java 3rd edition

[effective java 3rd - ch.05 제네릭] Item29 : 이왕이면 제네릭 타입으로 만들라

bright jazz music 2022. 11. 28. 13:24

JDK가 제공하는 제네릭 타입과 메서드를 사용하기는 쉽다.

그러나 제네릭 타입을 새로 만들기는 어렵다. 그러나 배워둘 가치가 있다.

 

아래의 스택 코드를 보자.

 

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() { //생성시 크기가 16인 Object타입의 배열 생성 => elements에 할당
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();	//배열에 원소가 들어갈 공간이 있는지 확인	
        elements[size++] = e; 
        //파라미터로 들어온 Object타입 e를 elements배열의 원소로 삽입
        //이 때 e가 대입된 후 size의 값이 하나 늘어남.
    }

    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) //배열의 길이가 size와 같은 경우
            elements = Arrays.copyOf(elements, 2 * size+1); 
            //Arrays.copyOf(원본배열, 원본에서 복사하고자 하는 요소들의 길이)
            // 2*size +1의 크기만큼 배열에 추가됨
            //size가 0이면 1, 1이면 3, 2이면 5
    }
}

 

위 코드는 제네릭 대신 Object를 사용하고 있다. 이런 경우 제네릭으로 수정할 수 있다면 제네릭으로 변경하는 것이 좋다. 이 클래스를 제네릭으로 변경한다고 해서 발생할 문제는 없다. 오히려 지금 상태에서의 클라이언트가 스택에서 꺼낸 객체를 형변환 할 때 런타임 오류가 날 위험이 있다.

 

제네릭으로의 변경

 

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

public class Stack<E> { //클래스 성언에 타입 매개변수 추가
    private E[] elements; //Object를 E로 교체
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) {    //Object를 E로 교체
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() {   //Object를 E로 교체
        if (size == 0)
            throw new EmptyStackException();
        E result = elements[--size];   ////Object를 E로 교체
        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);
    }
}

 

클래스 선언부에 타입 매개변수를 추가하였다. 또한 코드 내 Object를 타입 매개변수 E로 교체하였다.

 

 

elements = new Object[DEFAULT_INITIAL_CAPACITY];

/*
오류 발생

generic array createion

E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.

 

elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];

/*
이렇게 캐스팅이 가능하지만 일반적으로 타입 안전하지는 않다.

컴파일러는 이 프로그램이 타입 안전한지 증명하지 못한다. 결국 작성자가 판단해야 한다. elements는 private 필드에 저장되고 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 없다. push 메서드를 통해 배열에 저장되는 원소 타입은 항상 E다. 따라서 이 비검사 형변환은 안전하다.

 

 

배열을 사용한 코드를 제네릭으로 만드는 방법 1

 

비검사 형변환이 안전하다면 어노테이션을 사용하여 경고를 없애는 방법을 사용할 수 있다. 이 때 범위를 최소로 좁혀 @SuppressWarnings 애너테이션으로 경고를 숨긴다. 애너테이션을 달면 Stack은 깔끔하게 컴파일 되고 명시적으로 형변환하지 않아도 ClassCastException 걱정 없이 사용가능하다.

/*
배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
따라서 타입 안전성을 보장하지만, 이 배열의 런타임 타입은 E[]가 아닌 Object[]다.
*/

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

}

 

배열을 사용한 코드를 제네릭으로 만드는 방법2

 

elements필드의 타입을 E[]에서 Object[]로 변경하는 방법

public class Stack<E> { //클래스 성언에 타입 매개변수 추가
    private Object[] elements; //E에서 다시 Object로 
    
    
/*
또다시 경고 발생
warning: [unchecked] unchecked cast

found: Object, required: E
	E result = (E) elements[--size]; 바로 여기

E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 수 없다. 이번에도 마찬가지로 어노테이션을 사용하여 경고를 숨길 수 있다. pop메서드 전체에 경고를 숨기지 않고 비검사 형변환을 수행하는 할당문에서만 경고를 숨겨보자.

 

public E pop() {   //Object를 E로 교체
    if (size == 0)
        throw new EmptyStackException();
        
    //push에서 E타입만 허용하므로 이 형변환은 안전
    //배열에 E타입만 삽입되기 때문에 E 외의 다른 타입이 나올 일이 없음.
    @SuppressWarnings("unchecked") E result = (E) elements[--size];
    
    elements[size] = null; //다 쓴 참조 해제
    return result;
}

 

 

 

배열을 사용한 코드를 제네릭으로 만드는 방법1의 특징

장점

  • 가독성이 좋다.
  • 배열의 타입을 E[]로 선언하여 오직 E타입 인스턴스만 받음을 확실히 어필.
  • 코드가 짧다. 형변환을 배열 생성 시 단 한 번만 해주면 된다. 따라서 현업에서는 1번을 더 자주 사용한다.

단점

  • 배열의 런타임 타입이 컴파일 타입과 달라 힙오염(heap pollution)을 일으킨다. E가 Object인 경우는 예외이다.
  • 힙 오염에 예민한 사람은 2번 방법을 사용하기도 한다.

여기서는 힙 오염이 해가 되지 않았다.

 

----

 

예제.

명령줄 인수들을 역순으로 바꿔 대문자로 출력하는 프로그램.(Stack.java 사용)

 

public class TestMain{
    public static void main(String[] args){
        Stack<String> stack = new Stack<>();
        for (String arg : args)
            stack.push(arg);

        while(!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}

 

---

 

Stack의 예는 배열보다는 리스트를 사용하라는 조언과 모순된다. 배열을 리스트로 변경하지 않고 배열을 제네릭처럼 사용하기 위한 방법을 설명하였기 때문이다.

 

사실 제네릭 타입 안에서 리스트를 사용하는 게 항상 가능하지도, 더 나은 것도 아니다. 자바가 리스트를 기본타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야 한다. HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.

 

Stack의 예처럼 다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다.

Stack<Object>, Stack<int[]>, Stack<List<String>>, Stack 등 어떤 참조 타입으로도 Stack을 만들 수 있다. 다만 기본타입은 사용할 수 없다. Stack<int>나 Stack<double>을 만드려고 하면 컴파일 오류가 발생한다. 이는 자바 제네릭 타입의 근본 문제이나 '박싱된 기본타입'을 사용해 우회할 수 있다.

 

타입 매개변수에 제약을 두는 제네릭 타입

타입 매개변수에 제약을 두는 제네릭 타입도 있다.

e.g. java.util.concurrent.DealyQueue의 선언

 

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

 

타입매개변수 목록인 <E extends Delayed>는 java.util.concurrent.Delayed의 하위 타입만 받겠다는 뜻이다. 이렇게 하여 DelayQueue 자신과 DelayQueue를 implements 하는 DelayQueue의 원소에서 형변환 없이 바로 Delayed 클래스의 메서드를 호출할 수 있다. 이러한 타입 매개변수 E를 한정적 타입 매개변수( bounded type parameter)라고 한다. 또한 모든 타입은 자기 자신의 하위 타입이므로 DelayQueue<Delayed>로도 사용될 수 있다.

 

---

 

핵심정리

클라이언트에서 직접 형변환 하는 타입보다 제네릭이 안전하고 편하다. 따라서 새로운 타입을 설계할 때면 형변환 없이도 사용할 수 있도록 하라. 기존 타입 중 제네릭이 어룰리는 게 있다면 제네릭 타입으로 변경하자. 기존 클라이언트에게 영향을 주지 않는 동시에 사용자를 편하게 한다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1. 제네릭 배열 생성을 금지하는 제약을 우회하기

Object 배열을 생성하고 이를 제네릭 배열로 형변환 해보자. 컴파일러는 오류 대신 경고를 발생시킬 것이다. 하지만 그렇다고 경고를 무시한 채 프로그램을 구동시키는 것은 지양해야 한다.

 

public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    //Unchecked cast: 'java.lang.Object[]' to 'E[]' 
}

컴파일러는 이 프로그램이 타입 안전한지 증명할 수 없다. 배열 elements는 private 필드에 저장되고 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 없다. 또한 push메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다. 따라서 이 비검사 형변환은 안전하다. 따라서 범위를 최소로 좁혀 @SuppressWarnings로 경고를 숨긴다. 이 생성자는 비검사 배열 생성 말고는 역할이 없으니 생성자 전체에서 경고를 숨겨도 된다. 이 어노테이션을 달면 Stack은 문제 없이 컴파일 되고, 명시적인 형변환 없이도 ClassCastException 걱정 없이 사용 가능하다.

 

2. elements 필드 타입을 E[]에서 Object[]로 변경하기

제네릭 배열 생성 오류를 해결하는 두 번째 방법은 elements 필드의 타입을 E[]에서 Object[]ㄹ ㅗ바꾸는 것이다.

 

 

private Object[] elements; //Object를 E로 교체

 

 

 

 

 

 

 

 

 

 

 

 

 

Comments