ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [이펙티브 자바 스터디] 제네릭 - 2
    언어/JAVA 2024. 6. 7. 18:42

    배열보다는 리스트를 사용하라

     

    배열은 공변(함께 변함)이다.

    하지만 제네릭은 불공변이다.

     

    배열의 공변성으로 인한 문제

    Object[] objectArray = new Long[1];
    objectArray[0] = "응 안돼";  // 런타임 에러 발생

     

     

    제네릭의 이점

    List<Object> list = new ArrayList<Long>(); // 컴파일 에러
    ...

     

    배열은 실체화된다.

    그래서 런타임 시에도 그 타입 정보가 존재한다.

     

    하지만 제네릭은 그렇지 않다.

    컴파일 시점까지만 존재하고 이후엔 Object 로 변환되거나 제거된다.

     

     

    그래서 배열은 제네릭 타입, 매개변수화 타입 , 타입 매개변수로 사용할 수 없다.

     

    List<String>[] stringLists = new List<String>[1]; // (1)
    List<Integer> intList = List.of(42); // (2)
    Object[] objects = stringLists; // (3)
    objects[0] = intList; // (4)
    String s = stringLists[0].get(0); // (5)

     

    1) 이 가능하다고 가정한다면

    배열의 공변성으로 인한 문제가 생긴다.

    List<String> 배열을 Object[] 배열에 할당되고 

    List<Integer> 인스턴스가 이 objects 의 원소로 저장되는 것도 성공한다. 4까지는 문제가 없으나

    5번에서 형변환할 때 문제가 생기게 되는 것이다!

     

     

    배열을 제네릭으로 만들 수 없어 귀찮을 때도 있다.

    제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는 게 보통은 불가능하다.

    실체화되지 않기 때문이다.

    또한 가변인수 메서드와 함께 쓰면 해석하기 어려운 경고 메시지를 받게 된다.

     

    가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 

    이때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생하는 것이라고 한다.

     

    배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경
    우 대부분은 배열인 E[] 대신 컬렉션인 List<E>를 사용하면 해결된다.

     

    정리

    배열과 제네릭을 섞어 쓰기란 쉽지 않고 이를 쓰다가 오류나 경고를 만나면,

    가장 먼저 배열을 리스트로 바꾸는 방식을 적용해보라고 한다.

     


     

    이왕이면 제네릭 타입으로 만들라.
    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);
        }
    }

     

    위의 코드는 Object 기반 스택이다. 

    이에 제네릭을 적용해보자.

     

    일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다.

     

    public class Stack<E> {  // 제네릭을 적용하였고 
        private E[] elements;  // 이 부분이다.
        private int size = 0;
        private static final int DEFAULT_INITIAL_CAPACITY = 16;
        public Stack() {
            elements = new E[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;
        } ...
    }

     

    오류가 난 부분을 보면 

    제네릭 그러니까 실체화 불가 타입으로는 배열을 만들 수 없음으로 나타나는 오류이다.

    이 해결책으로는 두 가지가 있다.

     

    첫번째 Object 배열을 생성한 다음 제네릭 배열로 형변환

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

     

    Object 배열을 생성 후 형변환을 해주면 된다.

    이렇게 되면 오류가 아닌 컴파일러의 경고가 뜨게 된다.

     

    이 때 컴파일러는 이 프로그램이 타입 안전한지 증명할 수 없지만

    우리는 가능하다. 

    elements 는 private 필드에 저장되고 

    아래의 push 메소드가 E 타입인 원소만을 배열에 저장하고 있기 때문이다.

    그렇기에 전에 배웠던 @SuppressWarnings 애너테이션으로

    해결할 수 있다.

     

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

     

    두 번째. elements 필드의 타입을 E[]에서 Object[]로 바꾸기

    public class Stack<E> {
        private Object[] elements;
    ...
        public E pop() {
            if (size == 0)
            throw new EmptyStackException();
            E result = (E[]) elements[--size];
            elements[size] = null;
            return result;
        }
    ...

     

    꺼내 쓸 때 타입 캐스팅을 진행해주면 된다.

    이 경우에도 경고를 하는데 이 또한 @SuppressWarnings 애너테이션으로

    해결할 수 있다.

     

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

     

    정리하자면

    • 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하기 때문에 일반 클래스를 제네릭 클래스로 바꾸는 것이 좋고 그것에는 두 가지 방법이 있다.
    • 첫 번째 방법의 장점은 코드가 짧고 가독성이 더 좋다. 그리고 형변환을 배열 생성 시의 단 한 번만 해주면 된다.
    • 두 번째 방법의 장점은 *힙 오염을 일으키지 않는다.
      • 힙 오염이란?
        • 제네릭 힙 오염 과정은 제네릭의 특성으로 인해 런타임 시에는 타입이 Object 로 변환되거나 소거되는데 이로 인해서 런타임에 어느 타입의 데이터든 저장할 수 있게 된다.
        • List<String> -> List<Object> -> List<Integer> 라는 우리가 보기에 어색한 업캐스팅과 다운캐스팅과정으로 발생할 문제를 컴파일러가 알지 못하는 것이다.
        • 그래서 String 인 시점에 문자열이 들어가고 Integer 인 시점에 Int 값이 들어가면 리스트에 문자열과 정수가 같이 들어가 힙 오염이 되는 것이다.
        • 이에 대한 방지책으로 결국은 개발자가 제네릭을 잘 활용해야 하는 것이 1차적으로 자바에서는 Collections 클래스의 checkList() 라는 메서드를 지원한다. 그래서 사용시점이 아닌 삽입 시점에 힙 오염이 되는 상황 자체를 방지할 수 있게 하였다.

    이왕이면 제네릭 메서드로 만들라

     

    클래스와 마찬가지로, 메서드도 제네릭으로 만들 수 있다. 매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭이다.

     

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

     

    코드를 설명하자면 <E> 에서 제네릭 타입 매개변수를 선언하고 이 메소드에 사용될 타입을 나타내고 호출 시 구체적인 타입으로 대체된다.

     

    이렇게 제네릭으로 선언하면

    타입 안전하고, 쓰기도 쉽다. 직접 형변환하지 않아도 어떤 오류나 경고 없이 컴파일된다.

     

    이번엔 항등함수(identity function)를 담은 클래스를 만들어 보자.

     

    항등 함수는 입력한 값을 그대로 반환하는 함수이다.

     

    private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
        
    @SuppressWarnings("unchecked")
    public static <T> UnaryOperator<T> identityFunction() {
        return (UnaryOperator<T>) IDENTITY_FN; 
    }

     

    여기서도 비검사 형변환 경고가 나오지만 우리는 이 것이 항등 함수이기에 안전한 것을 알 수 있다.

    그래서 @SuppressWarnings를 사용해서 경고를 없애준다.

     

    UnaryOperator 는 무엇일까?

     

    • 자바에서 제공하는 함수형 인터페이스
    • 인수와 반환 결과가 동일한 경우에 사용할 수 있다.
    • Java 8 부터 사용할 수 있다.
    • 여기에 static 으로 identity 라는 메서드가 UnaryOperator는 인자로 받은 값을 그대로 반환해준다.

     

    정리

    • 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다.
    • 타입과 마찬가지로, 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하려면 제네릭 메서드가 되어야 한다. 

     


     

     

    한정적 와일드카드를 사용해 API 유연성을
    높이라



    public void pushAll(Iterable<E> src) { 
            for (E e : src) push(e);
    }

     

    pushAll 을 개발해보자.

     

    Stack<Number> numberStack = new Stack<>(); 
    Iterable<Integer> integers = ...; 
    numberStack.pushAll(integers);

     

    이렇게 하면 문제없이 잘 작동해야 하는 것처럼 보인다.

    하지만 이는 오류 메시지가 뜬다. 매개변수화 타입이 불공변이기 때문이다.

     

    자바는 이런 상황에 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다. 

    pushAll의 입력 매개변수 타입은 ‘E의 Iterable’이 아니라 ‘E의 하위 타입의 Iterable’이어야 하며,

    와일드 카드 타입 Iterable<? extends E>가 이런 뜻이다.

     

    하지만 이 extends 이 상황에 완전 들어맞지는 않는다.

    하위 타입이란 자기 자신도 포함하지만, 그렇다고 자신을 확장(extends)한 것은 아니기 때문이다.

     

    public void pushAll(Iterable<? extends E> src) { 
            for (E e : src)
                push(e); 
        }

     

     

     

     

Designed by Tistory.