ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이펙티브 자바 : 제네릭 클래스와 제네릭 메서드
    책책책 책을 읽읍시다/프로그래밍 2023. 7. 7. 22:57

    제네릭 클래스


     아래와 같은 비(非)제네릭 스택 클래스가 있다고 해보자.

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

     이 클래스는 원래 제네릭 타입이어야 마땅하니 제네릭으로 만들어보자. 이 클래스를 제네릭으로 바꾼다고 해도 현재 버전을 사용하는 클라이언트에는 아무런 해가 없다. 오히려 지금 상태에서의 클라이언트는 스택에서 꺼낸 객체를 형변환해야 하는데, 이때 런타임 오류가 날 위험이 있다.

     일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다. 위 스택에서는 스택이 담을 원소의 타입 하나만 추가하면 된다. 이때 타입 이름으로는 보통 E를 사용한다.

     그런 다음 코드에 쓰인 Object를 적절한 타입 매개변수로 바꾸고 컴파일해보자. 빨간 네모로 표시된 부분이 바뀐 곳이다.

    제네릭을 실체화할 수 없어서 컴파일 에러

     E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다. 배열을 사용하는 코드를 제네릭으로 만들려 할 때는 이 문제가 항상 발목을 잡을 것이다. 적절한 해결책은 두 가지다. 첫 번째는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법이다. Object 배열을 생성한 다음 제네릭 배열로 형변환해보자. 이제 컴파일러는 오류 대신 경고를 내보낼 것이다. 이렇게도 할 수는 있지만 (일반적으로) 타입 안전하지 않다.

    타입 안전성을 보장하지 못하는 형변환

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

     비검사 형변환이 안전함을 직접 증명했다면 범위를 최소로 좁혀 @SuppressWarnings 어노테이션으로 해당 경고를 숨긴다. 이 예에서는 생성자가 비검사 배열 생성 말고는 하는 일이 없으니 생성자 전체에서 경고를 숨겨도 좋다. 어노테이션을 달면 Stack은 깔끔히 컴파일되고, 명시적으로 형변화하지 않아도 ClassCastException 걱정 없이 사용할 수 있게 된다.

    SuppressWarnings로 컴파일 에러 제거

     하지만 이 배열의 런타임 타입은 E[]가 아니라 Object[]다.

     제네릭 배열 생성 오류를 해결하는 두 번째 방법은 elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것이다. 이렇게 하면 첫 번째와는 다른 오류가 발생한다.

    타입 불일치로 컴파일 에러

     배열이 반환한 요소를 E로 형변환하면 오류 대신 경고가 뜬다.

    형변환이 안전한지 컴파일러가 몰라 발생하는 경고

     E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다. 이번에도 마찬가지로 우리가 직접 증명하고 경고를 숨길 수 있다. pop 메서드 전체에서 경고를 숨기지 말고, @SuppressWarnings로 할당문만 숨길 수 있다.

     

     제네릭 배열 생성을 제거하는 두 방법 모두 나름의 지지를 얻고 있다. 첫 번째 방법은 가독성이 더 좋다. 배열의 타입을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필한다. 코드도 더 짧다. 보통의 제네릭 클래스라면 코드 이곳저곳에서 이 배열을 자주 사용할 것이다. 첫 번째 방식에서는 형변환을 배열 생성 시 단 한번만 해주면 되지만, 두 번째 방ㅎ식에서는 배열에서 원소를 읽을 때마다 해줘야 한다. 따라서 현업에서는 첫 번째 방식을 더 선호하며 자주 사용한다. 하지만 (E가 Object가 아닌 한) 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염(heap pollution)을 일으킨다. 힙 오염이 마음에 걸린다면 두 번째 방식을 사용하자.

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

     

    제네릭 메서드


     클래스와 마찬가지로 메서드도 제네릭으로 만들 수 있다. 매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭이다. 예컨대 Collections의 '알고리즘' 메서드(binarySearch, sort 등)는 모두 제네릭이다.

     제네릭 메서드 작성법은 제네릭 타입 작성법과 비슷하다. 다음은 두 집합의 합집합을 반환하는, 문제가 있는 메서드다.

    public static Set union(Set s1, Set s2) {
        Set result = new HashSet(s1);
        result.addAll(s2);
        return result;
    }

    컴파일은 되지만 경고가 두 개 발생한다.

    • Raw use of parameterized class 'HashSet' 
    • Unchecked call to 'addAll(Collection<? extends E>)' as a member of raw type 'java.util.Set' 

     경고를 없애려면 이 메서드를 타입 안전하게 만들어야 한다. 메서드 선언에서의 세 집합(입력 2개, 반환 1개)의 원소 타입을 타입 매개변수로 명시하고, 메서드 안에서도 이 타입 매개변수만 사용하게 수정하면 된다. (타입 매개변수들을 선언하는) 타입 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 온다. 다음 코드에서 타입 매개변수 목록은 <E>이고 반환 타입은 Set<E>이다. 타입 매개변수의 명명 구칙은 제네릭 메서드나 제네릭 타입이나 똑같다.

    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
        Set<E> result = new HashSet<>(s1);
        result.addAll(s2);
        return result;
    }

     단순한 제네릭 메서드라면 이 정조면 충분하다. 이 메서드는 경고 없이 컴파일되며, 타입 안전하고, 쓰기도 쉽다. 다음은 이 메서드를 사용하는 간단한 프로그램이다. 직접 형변환하지 않아도 어떤 오류나 경고없이 컴파일된다.

    타입 안전하게 클라이언트에서 사용

     바뀐 union 메서드는 집합 3개(입력 2개, 반환 1개)의 타입이 모두 같아야 한. 이를 한정적 와일드카드 타입을 사용하여 더 유연하게 개선할 수 있다.

     때때로 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때가 있다. 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화할 수 있다. 하지만 이렇게 하려면 요청한 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩터리를 만들어야 한다. 이 패턴을 제네릭 싱글턴 팩터리라 하며, Collections.reverseOrder 같은 함수 객체나 (이따금) Collections.emptySet 같은 컬렉션용으로 사용한다.

     

    댓글

Designed by Tistory.