이펙티브 자바 5

qq·2024년 1월 24일
0
post-thumbnail

이펙티브 자바 5장

💡 item 26 : raw 타입은 사용하지 말자

raw 타입이란 제네릭 타입에서 타입 매개 변수를 전혀 사용하지 않은 타입을 말한다. 현재로서는 제네릭 이전의 코드와 호환되기 위해 사용될 뿐, 런타임 시점에 오류를 발생할 소지가 많다

  • 클래스, 인터페이스 선언에 ‘타입 매개변수'가 쓰이면 제네릭 클래스, 인터페이스라고 부름
  • ‘제네릭 타입’은 매개변수화 타입을 정의한다.
  • ‘제네릭 타입'을 정의하면 ‘raw 타입' 도 정의된다. (타입 매개변수를 사용하지 않는 타입) ex) List → List (제네릭 도래 전 코드와 호환을 위해 존재)

raw type의 단점

컬렉션의 raw 타입

private final Collection stamps = ...;
...
stamps.add(new Coin(...)); // 실수로 동전을 넣는다.
  • 따라서 오류가 발생하고 한참 뒤인 런타임에서야 오류를 알아챌 수 있는데 이렇게 되면 원인을 제공한 코드와 런타임에 문제가 발생한코드가 떨어져있어 에러를 잡기위해 코드 전체를 훑어봐야 할 수도 있다.
  • 이런 문제를 해결하기 위해 매개변수화된 컬렉션 타입으로 타입 안정성을 확보해야한다.

매개변수화된 컬렉션 타입

private final Collection<Stamp> stamps = ...;
  • 이렇게 선언하면 컴파일러가 stamps 컬렉션에 Stamp 인스턴스만 넣어야함을 인지하여 의도대로 동작함을 보장해준다.
  • 만약, 다른 타입의 인스턴스를 넣으려하면 컴파일 오류가 발생하며 문제를 알려준다.

List와 List<Object>

  • 로 타입을 쓰면 제네릭이 안겨주는 안정성과 표현성을 모두 잃게된다.
  • 그럼에도 불구하고 이런 로 타입을 남겨놓은 이유는 자바에 제네릭을 받아들이기 이전 코드와의 호환성 때문이다.
  • 제네릭 타입이 아닌 List와 달리 List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에 전달한 것이다.

제네릭 하위 타입 규칙

List를 받는 메서드에 List<String>을 넘길 수 있지만, List<Object>를 받는 메서드에는 넘길 수 없다.

//가능

public static void main(String[] args) {
        final List<String> strings = new ArrayList<>();
        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0);
}

private static void unsafeAdd(final List list, final Integer valueOf) {
        list.add(0);
}

//불가능

public static void main(String[] args) {
        final List<String> strings = new ArrayList<>();
        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0);
}

private static void unsafeAdd(final List<Object> list, final Integer valueOf) {
        list.add(0);
}

비한정적 와일드카드 타입

  • 제네릭 타입을 사용하고싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표(?)를 사용하자.

로 타입을 사용해야하는 경우

  • class 리터럴에는 로타입을 사용해야한다.
List.class, String[].class, int.class
  • instanceof 연산자 사용시 로 타입을 사용하자.
if (o instanceof Set) { // 로 타입
	Set<?> s = (Set<?>) o; // 와일드카드 타입
}
  • 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.

💡 item 27 : **비검사 경고를 제거하라**

비검사 경고가 발생하는 코드

Set<Lark> exaltation = new HashSet();

해결한 코드

Set<Lark> exaltation = new HashSet<>();

@SuppressWarnings("uncheck")를 이용한 비검사 경고 제거

  • 만약, 경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면 @SuppressWarnings("unchecked")를 이용해 비검사 경고를 숨기자.
  • 만약, 타입 안전하다고 검증된 코드의 검사를 그대로 두면 진짜 문제를 알리는 경고 코드를 구분하기 쉽지 않다.
  • 또한 타입 안전함을 검증하지 않은 채 경고를 숨기면 잘못된 보안인식을 심어주는 꼴이된다.

@SuppressWarnings("uncheck") 사용범위

  • @SuppressWarnings("unchecked")는 가능한 좁은 범위에 적용하자.
  • @SuppressWarnings 애너테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다.
  • 한줄이 넘는 메서드나 생성자에 @SuppressWarnings가 달려있다면 지역변수나 아주 짧은 메서드 혹은 생성자로 옮기자.
  • !! 절대로 클래스 전체에 적용해서는 안된다. !!

[기존 ArrayList의 toArray 메서드]

public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
}
  • 위 코드를 컴파일하면 @copyOf()@ 부분에서 경고가 발생한다. 이 경고를 제거하려면 지역변수를 추가해야 한다.

[지역변수를 추가해 @SuppressWarnings의 범위를 좁힌다.]

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // 생성한 배열과 매개변수로 받은 배열이 모두 T[]로 같으므로
        // 올바른 형변환이다.
        @SuppressWarnings("unchecked") 
        T[] result = (T[]) Arrays.copyOf(elementData, size, a.getClass()); 
        return result
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}
  • @SuppressWarnings("unchecked") 에너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야한다.

💡 item 28 : raw **배열보다는 리스트를 사용하라**

배열 VS 리스트

첫 번째 - 배열은 공변인 반면 리스트는 불공변이다.

공변: 함께 변한다
불공변: 함께 변하지 않는다.
  • 배열의 경우 Sub가 Super의 하위 타입이라면 Sub[]는 배열 Super[]의 하위 타입이 된다.
  • 반면, 리스트의 경우 서로 다른 타입 Type1Type2가 있을 때, List<Type1>은 List<Type2>의 하위 타입도 아니고 상위 타입도 아니다.

두 번째 - 배열은 실체화된다.

  • 배열은 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 그래서 Long 타입 배열에 String 타입 데이터를 입력하려고하면 ArrayStoreException이 발생한다.
  • 반면, 리스트는 타입 정보가 런타임에는 소거된다. 원소 타입을 컴파일시에만 검사하며 런타임에는 알 수 없다는 말이다.

[제네릭 배열 생성을 허용하지 않는 이유 - 컴파일되지 않는다.]

List<String>[] stringLists = new List<String>[1]; // (1) 허용된다고 가정해보자.
List<Integer> intList = List.of(42);              // (2) 원소가 하나인 List<Integer> 생성
Object[] objects = stringLists;                   // (3) stringLists를 objects에 할당
objects[0] = intList;                             // (4) intList를 objects의 첫번째 원소로 저장한다.
String s = stringLists[0].get(0);                 // (5) stringList[0]에 들어가있는 첫번째 요소는 Integer이므로 형변환 오류 발생.
  • 제네릭 배열을 만들지 못하게하는 이유는 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있기 때문에 타입 안전하지 않기 때문이다.
  • 또한 런타임에 ClassCastException이 발생하는 것을 막아주겠다는 제네릭 타입 시스템 취지에 어긋나는 일이기도 하다.

배열은 실체화 불가 타입

  • EList<E>List<String> 같은 타입을 실체화 불가 타입이라고 한다.
  • 쉽게 말해, 실체화 되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다
  • 소거 매커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?>Map<?,?> 같은 비한정적 와일드카드 타입뿐이다.
  • 참고로, 배열은 비한정적 와일드카드로 만들수는 있지만 유용하게 쓰일 일은 거의 없다.

@SafeVarargs

@SafeVarargs는 메서드 작성자가 해당 메서드가 타입 안전하다는 것을 보장하는 장치이다.

  • 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는게 보통은 불가능하다. 또한 제네릭 타입과 가변인수 메서드를 함께 쓰면 해석하기 어려운 경고 메시지를 받게 된다.
  • 가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데 이때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생한다.
  • 이 때 @SafeVarargs를 사용하면 잠재적 오류에 대한 경고를 무시함으로써 해결할 수 있다. 만약, 메서드가 타입 안전하지 않다면 절대 @SafeVarargs를 사용해서는 안된다.
public class SafeVars {
    @SafeVarargs
    public static void print(List... names) {
        for (List<String> name : names) {
            System.out.println(name);
        }
    }

    public static void main(String[] args) {
        SafeVars safeVars = new SafeVars();
        List<String> list = new ArrayList<>();

        list.add("b");
        list.add("c");
        list.add("a");
        print(list);
    }
}

배열대신 컬렉션을 사용하자

  • 배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분 배열 E[]대신 List<E>를 사용하면 해결된다.
  • 조금 복잡해지고 성능이 나빠질 수 있지만 타입안정성이 보장되고 상호 운용성이 좋아진다.

[Chooser - 제네릭 적용 필요]

public class Chooser {
    private final Object[] choiceArray;

    public Chooser(final Object[] choiceArray) {
        this.choiceArray = choiceArray;
    }

    public Object choose(){
        Random random = ThreadLocalRandom.current();
        return choiceArray[random.nextInt(choiceArray.length)];
    }
}

[리스트 기반 Chooser - 타입 안정성 확보]

public class ListChooser {
    private final List<T> choiceList;

    public ListChooser(final Collection<T> choices) {
        this.choiceList = new ArrayList<>(choices);
    }
    
    public T choose(){
        Random random = ThreadLocalRandom.current();
        return choiceList[random.nextInt(choiceList.size())];
    }
}
  • 코드는 조금 길어졌지만 리스트를 사용함으로써 런타임에 ClassCastException을 만날일이 없어졌다.

질문?

위 두 코드 차이가 뭐임?

[Chooser - 제네릭 적용 필요]

public class Chooser {
    private final Object[] choiceArray;

    public Chooser(final Object[] choiceArray) {
        this.choiceArray = choiceArray;
    }

    public Object choose(){
        Random random = ThreadLocalRandom.current();
        return choiceArray[random.nextInt(choiceArray.length)];
    }
}
위 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다. 만약 타입이 다른 원소가 들어있으면 런타임시에 형변환 오류가 발생한다.

[리스트 기반 Chooser - 타입 안정성 확보]

public class ListChooser {
    private final List<T> choiceList;

    public ListChooser(final Collection<T> choices) {
        this.choiceList = new ArrayList<>(choices);
    }
    
    public T choose(){
        Random random = ThreadLocalRandom.current();
        return choiceList[random.nextInt(choiceList.size())];
    }
}

💡 item **29 : 이왕이면 제네릭 타입으로 만들라**

일반 클래스를 제네릭 타입으로 변경

[Object 기반 스택 - 제네릭이 절실한 강력후보]

  • 위와 같은 코드 상태로는 스택에서 꺼낸 객체를 형변환해야 하는데 이때 런타임 오류가 발생할 가능성이 있다. 따라서 제네릭 타입 클래스로 바꾸는것이 좋다.
  • 일반 클래스에서 제네릭 타입 클래스로 만드는 시작은 타입 매개변수를 추가하는 것이다. 이때 타입이름으로 보통 E를 사용한다
public class ObjectStack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public ObjectStack() {
        this.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);
        }
    }
}

타입 매개변수 오류를 해결하는 두가지 방법

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

    public ObjectStack() {
        this.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);
        }
    }
}
public class GenericStack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public GenericStack() {
        this.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;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}
  • 하지만 • 위 코드의 생성자에서 오류가 발생한다. 아이템 28에서 설명한 것처럼 E와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문이다.
  • 위 문제를 해결하는 두 가지 방법이 있다.

타입 매개변수 오류를 해결하는 두가지 방법

1. Object 배열을 생성하고 그 다음 제네릭 배열로 형변환하는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법.

[Object 배열 생성 시 배열 형변환]

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

public GenericStack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 경고 메시지 발생
}

private void ensureCapacity() {
    if (elements.length == size) {
        elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
...
// 비검사 형변환 경고 메시지 발생
Unchecked cast: 'java.lang.Object[]' to 'E[]
  • 위 코드에서는 비검사 형변환 경고 메시지가 발생한다.
  • 위 코드에서 elements 배열은 private 필드에 저장된 후 클라이언트로 반환되거나 다른 메서드에 전달되지 않는다.
  • push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E이다.
  • 위 두가지 이유로 이 비검사 형변환은 확실히 안전하다.
  • 비검사 형변환 경고를 없애기 위해 범위를 최대한 줄여 @SuppressWarnings("unchecked")를 사용하여 경고를 숨긴다.

[비검사 형변환 경고 숨김]

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

// 배열 elements는 push(E)로 넘어온 E인스턴스만 남는다.
// 타입 안정성을 보장하지만 이 배열의 런타임 타입은 Object[] 이다.
@SuppressWarnings("unchecked")
public GenericStack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 경고 메시지 발생
}

private void ensureCapacity() {
    if (elements.length == size) {
        elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
...
// 비검사 형변환 경고 메시지 발생
Unchecked cast: 'java.lang.Object[]' to 'E[]
  • 생성자가 배열 생성말고는 따로 하는 기능이 없기 때문에 생성자 전체에서 경고를 숨겨도 된다.

2. elements 필드의 타입을 E[]에서 Object[]로 바꾸는 방법.

[Object 배열은 그대로 두고 pop() 사용시 형변환]

// 비검사 경고를 적절히 숨긴다.
public class GenericStack<E> {
    private Object[] elements;
	...
    public GenericStack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

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

    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
		// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
        @SuppressWarnings("unchecked")
        E result = (E) elements[--size];
        elements[size] = null; // 다 쓴 참조 해제

        return result;
    }
	...
}

제네릭 배열 생성을 제거하는 두 방법의 장단점

  • 1 번째 방법은 가독성이 좋고 코드가 짧다. 또한 배열을 한번만 생성하면 된다. 하지만, E가 Object가 아니기 때문에 (배열의 컴파일타임의 타입과 런타임의 타입이 다르기 때문에) 힙 오염을 일으킨다.
  • 두 번째 방법은 배열을 원소를 읽을때마다 생성해야 한다.

이 item에 대해 얘기해보자

💡 item **아이템30 : 이왕이면 제네릭 메서드로 만들라**

제네릭 메서드 작성방법

  • 매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭이다.
    • Collections의 binarySearchsort 등 알고리즘 메서드는 모두 제네릭이다.
  • 제네릭 메서드 작성법은 제네릭 타입 작성법과 비슷하다.
  • 타입 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 온다.

[제네릭 메서드]

public static <E> Set<E> union (Set<E> s1, Set<E> s2){
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}
  • 위 코드는 세개의 Set 집합이 타입이 모두 같아야 한다. 이를 한정적 와일드 카드 타입을 이용하면 더 유연하게 개선이 가능하다.

제네릭 싱글턴 팩터리

  • 불변 객체를 여러 타입으로 활용할 때가 있다. 제네릭은 런타임시 타입 정보가 소거 되므로 하나의 객체를 어떤 타입으로든 매개변수화 할 수 있다.
  • 불변 객체를 여러 타입으로 활용할 때가 있다. 제네릭은 런타임시 타입 정보가 소거 되므로 하나의 객체를 어떤 타입으로든 매개변수화 할 수 있다.
  • 이 정적 팩터리를 제네릭 싱글턴 팩터리라고 한다.

[제네릭 싱글턴 팩터리 패턴 - 항등함수]

private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

@SuppressWarinings("unchecked")
public static <T> UnaryOperator<T> identityFunction(){
    return (UnaryOperator<T>) IDENTITY_FN;
}
  • 항등함수는 입력 값을 수정 없이 그대로 반환하는 특별한 함수이므로 T가 어떤 타입이든 UnaryOperator<T>를 사용해도 타입 안전하다.

재귀적 한정적 타입

  • 재귀적 타입 한정은 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정하는 개념이다.
  • 이런 재귀적 타입 한정은 주로 타입의 자연적 순서를 지정해주는 Comparable과 함께 사용된다.
public interface Comparable<T>{
	int compareTo(T o);
}
  • 타입 매개변수 T는 Comparable<T>를 구현한 타입이 비교할 수 있는 원소의 타입을 정의한다.

[재귀적 타입 한정을 이용해 상호 비교 할 수 있음을 표현]

public static <E extends Comparable<E>> E max(Collection<E> c);
  • <E extends Comparable<E>>가 모든 타입 E는 자신과 비교할 수 있다는 의미를 갖는다.
  • 아래는 재귀적 타입 한정을 이용한 메서드를 구현했다. 컴파일오류나 경고는 발생하지 않으며 컬렉션에 담긴 원소의 자연적 순서를 기준으로 최댓값을 계산한다.

[재귀적 타입 한정을 이용한 최댓값 계산 메서드]

public static <E extends Comparable<E>> E max(Collection<E> c){
    if(c.isEmpty()){
       throw new IllegalArgumentException("컬렉션이 비었습니다.");
    }
        
    E result = null;
    for (E e : c){
        if(result == null || e.compareTo(result) > 0){
            result = Objects.requireNonNull(e);
        }
    }
        
    return result;
}

💡**item31 한정적 와일드 카드를 사용해 API의 유연성을 높여라**

제네릭은 기본적을 불공변이다. List와 List은 서로 아무런 관계가 없는 타입이라는 뜻이다. 그러나 경우에 불공변보다 조금 더 유연한 방식이 필요할 때가 있다

→ 한정적 와일드 카드를 활용해 제네릭의 유연성을 높인다.

public class Stack<E> {
	public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}
//여기에 일련의 원소들을 추가하는 메서드를 추가한다고 하자
public void pushAll(Iterable<E> src) {
	for (E e : src) {
    	push(e);
    }
}

이 코드는 문제는 없지만 완벽한 코드라 볼 수 없다. 그 이유는 만약 Stack로 선언한 후 pushAll(intVal)을 호출하면 어떻게 될까? intVal은 Integer이다.

결과 : 논리적으로 볼 때는 될 것 같았지만 컴파일 조차 되지 않는다. 그 이유는 불공변이기 때문에 Number를 상속한 Integer라도 Iterable와 Iterable 는 서로 아무런 연관이 없는 타입이기 때문이다.

//한정적 와일드 카드를 통해 해결한 코드
public void pushAll(Iterable<? extends E> iterable) {
    for (E e : iterable) {
        push(e);
    }
}

비한정적 와일드카드 적용 방법

  • <? extends E>를 통해 우리는 와일드 카드를 활용하면 상속에 유연한 제네릭 코드를 만들 수 있음을 알았다. 그러나 비한정적 와일드 카드 적용에는 일련의 규칙이 있는데 이를 알아보자.

PECS(펙스)

  • Producer-Extends Consumer-super 라는 공식으로 공급할 때는 extends를 활용하고 사용할 때는 super를 사용하는 공식이다. 예를 통해 확인해보자

Producer - extends

public static <E> Set<E> union(Set<E> s1, Set<E> s2)
  • 위의 코드를 살펴보면 s1, s2는 생산자이다. 여기서 생산자란 의미는 s1, s2를 입력으로 사용해서 무언가 작업한다는 것이다. 이 경우 E보다 하위 타입을 입력받더라도 공변이면 E로 자동 형변환해서 저장된다.
  • 제네릭에서 가능하게 해야하기 때문에 다음과 같이 코드를 변경하면 된다.
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2);

Consumer - super

//Collection<E> 타입의 dst에 Stack 원소들을 담는 메서드이다. 이를 상속에 유연한 공변 입장에서 생각해보자..
public void popAll(Collection<E> dst) {
	while (!isEmpty()) {
    	dst.add(pop());
    }
}
  • 해당 코드는 Collection 타입의 dst에 Stack 원소들을 담는 메서드이다. 이를 상속에 유연한 공변 입장에서 생각해보자..
public void popAll(Collection<? super E> dst) {
	while (!isEmpty()) {
    	dst.add(pop());
    }
}
  • Comparable은 항상 소비자라 한다. 그 이유는 E를 가져와서 대소 관계를 처리하기 때문이다.

다음과 같이 비교하면 쉬울 것 같다.

  • 공급자: 어떤 인자들을 받아서 사용하는데 상위 타입으로 정의되어 있고 하위 타입을 받아서 작업하는 다형성을 활용하는 경우
  • 소비자: 해당 타입이 데이터를 가져가는 경우(데이터를 가져가는 경우는 해당 타입과 상위타입만 가능)나, 하위타입에서 정의되지 않고 상위 타입에서 정의된 무엇인가를 활용하는 경우

💡**item32 제네릭과 가변인수를 함께 쓸 때는 신중해라**

아예 이해 못함 말하면서 이해하고 싶음

제네릭 가변인수는 제대로 적용되지 않는다.

  • 배열로 선언하든 제네릭으로 선언하든 가변인수로 선언하면 내부적으로 배열이 만들어진다.
  • 문제는 배열은 공변이고 제네릭은 불공변이다. 제네릭은 컴파일시에 타입을 체크하지만 배열은 런타임에 타입을 체크한다
  • 이러한 불일치성과 제네릭으로 선언해도 내부적으로 배열로 생성되기 때문에 잠재적으로 힙오염이나 ClassCastException을 일으킬 수 있다.

안전하게 사용하기 @SafeVarargs

  • varargs의 타입을 안전하게 사용하기 위해서 자바에서는 @SafeVarags 애너테이션을 제공해준다. 그러나 해당 어노테이션을 적용할 때는 @SupressWarnings와 마찬가지로 완전하게 안전할 때 사용해야 한다. 그런 경우를 살펴보자.

1.복사해서 사용한다.

@SafeVarargs
static <T> List<T> flatten(List<? extends T> ...lists) {
	for (List<? extends T> list: lists) {
    	result.addAll(list);
    }
    return result;
}

2. 수정없이 받아서 사용만 하고 외부에 varargs를 노출시키지 않는다.

  • 외부에 노출시키지 않고 내부적으로만 사용하며 단순히 데이터만 활용하는 경우 안전하다.

3. 재정의하는 메서드에 사용하지 않는다.

  • 재정의하는 메서드의 경우 문제가 생길 우려가 있다. 그렇기에 자바 8에서는 final 인스턴스 메서드와 정적 메서드에만 사용가능했다.
  • 그러나 자바9 부터는 private 인스턴스 메서드에도 붙일 수 있기 때문에 조심해서 사용하도록 하자.

List.of 를 사용하기

  • List.of는 가변인자를 받아서 ImmutableList를 만들어주는 좋은 정적 메서드이다. 이를 활용하면 비검사 경고나, 예외에 대해서 안전하게 사용할 수 있다.

힙 오염에 대해 얘기해보자

profile
백엔드 개발자

0개의 댓글