이펙티브 자바 - 5장

JeongJun Min·2024년 10월 31일

JAVA

목록 보기
4/7
post-thumbnail

제네릭

자바 5부터 지원하는 제네릭은 컬렉션에서 담을 수 있는 타입을 컴파일러에 알려주어 알아서 형변환 코드를 추가할 수 있게 된다. 하지만 코드가 복잡해 질 수 있다.

로 타입은 사용하지 말라

제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라 한다.

각각의 제네릭 타입은 일련의 매개변수화 타입(parameterized type)을 정의한다.

ex) List String인 리스트를 뜻하는 매개변수화 타입이다.

제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함계 정의된다.

로 타입(raw type)
제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.
ex) List의 로 타입은 List이다.

로 타입을 쓰면 제네릭이 안겨주는 안정성과 표현력을 모두 잃게 된다.

private final Collection stamps = ...;

stamps.add(new Coin(...));

stamp 대신 Coin을 넣어도 아무 오류 없이 컴파일 되고 실행된다.

컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하여 절대 싶패하지 않음을 보장한다.

List처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다.

🤔 로 타입인 List와 List의 차이는 무엇일까?

List는 제네릭 타입에서 완전히 발을 뺀 것이고, List는 모든 타입을 허용한다는 의사를 컴파일러에 명확히 전달한 것이다.

비한정적 와일드카드 타입(unbounded wildcard type)을 사용하는 게 좋다.

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

Set<E> → Set<?>

와일드카드 타입은 안전하고, 로 타입 컬렉션은 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다. 반면, Collection<?>에는 null 외에는 어떤 원소도 넣을 수 없다.

로 타입을 쓰는 예외

  • class 리터럴에는 로 타입을 써야 한다.
  • instanceof 연산자는 로 타입이든 비한정적 와일드카드 타입이든 완전히 똑같이 동작한다. 코드만 지저분하게 만드는 비한정적 와일드카드보다 로 타입을 쓰는 편이 깔끔하다.

비검사 경고를 제거하라

제네릭을 사용하기 시작하면 수많은 컴파일러 경고를 보게 될 것이다.

할 수 있는 한 모든 비검사 경고를 제거하라.

그러면 그 코드는 타입 안정성이 보장된다.

그러나 경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면 @SuppressWarnings(”unchecked”) 애너테이션을 달아 경고를 숨기자.

단, 타입 안정함을 검증하지 않은 채 경고를 숨기면 스스로에게 잘못된 보안 인식을 심어주는 꼴이다.

@SuppressWarnings 애너테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다. 하지만 항상 가능한 좁은 범위에 적용하자.

해당 애너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다.


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

배열은 공변(함께 변함) 제네릭은 불공변(서로 다름)

공변
자기 자신과 자식 객체로 타입 변환을 허용해주는 것이다.

불공변
두 개의 타입은 전혀 관련이 없다.

배열은 런타임에도 자신이 담기로 한 원소 타입을 인지하고 확인하지만, 리스트(제네릭)는 타입 정보가 런타임에는 소거 되며, 원소 타입은 컴파일시에만 검사한다. 즉, 런타임시에는 타입을 알 수 없다.

위 두 차이로 인해 배열과 제네릭은 함께 어우러지지 못한다. 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없으며, 아래와 같이 사용하려고 하면 제네릭 배열 생성 오류를 발생시킨다.

배열은 구체화(reify)가 되고, 제네릭은 비구체화(non-reify)가 된다.

구체화 타입(reifiable type)
자신의 타입 정보를 런타임에도 알고 있는 것이다.

비구체화 타입(non-reifiable type)
런타임 시에 소거(erasure)되기 떄문에 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다. ex) E, List, List

List에는 문자열만 넣을 수 있으나, List에는 어떤 객체도 넣을 수 있다. 이 둘은 서로 하는 일을 바꾼다면 제대로 수행하지 못한다.

배열이든 리스트이던 Integer용 저장소에 String을 넣을 순 없으나, 전자는 런타임에 실수를 알 수 있고, 후자는 컴파일타임에 알 수 있다. 후자가 당연히 좋으니, 배열보다는 리스트를 사용하자.

다만, 성능적인 측면에서는 배열이 앞설 수 있다.


이왕이면 제네릭 타입으로 만들어라

다음은 일반 클래스를 제네릭 클래스로 만드는 방법이다.

  • 클래스 선언에 타입 매개변수를 추가
  • 일반 타입(ex. Object)를 타입 매개변수로 교체
  • 비검사(unchecked) 경고 해결해주기

제네릭 타입 안에서 리스트를 사용하는 것이 항상 가능한 것도, 좋은 것도 아니다. 자박 리스트를 기본 타입으로 제공하지 않아, ArrayList와 같은 제네릭 타입도 결국 기본 타입인 배열을 사용해 구현해야하며, HashMap의 경우 성능을 높일 목적으로 배열을 사용하기도 한다.

대부분 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않으며, Stack<Object>, Stack<int[]>, Stack<List<String>>, Stack 등 어떤 참조 타입으로도 생성할 수 있다. 단, 기본타입은 사용할 수 없다. ex) Stack - (X)


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

클래스와 마찬가지로 메소드도 제네릭이 가능하다면 사용하자. 사용자 측에서 형변환하는 것보다 훨씬 안전하고 유연해진다.

제네릭 싱글턴 팩터리

떄때로 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때가 있는데, 이때는 제네릭 싱글톤 팩토리를 만들면 된다. ex) Collections.reverseOrders, Collections.emptySet

@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
    return (UnaryOperator<T>) IDENTITY_FN;
}

재귀적 타입 한정(recursive type bound)

자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다.

public static <E extends Comparable<E>> E max(Collection<E> c)

위와 같이 타입 매개변수를 한정적으로 기술해주는 방식이다. 이를 통해 모든 타입 E는 자신과 비교할 수 있다라는 것을 나타낸다.


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

매개변수화 타입은 불공변이다. 즉 서로 다른 타입 Iterable<Integer>Iterable<Number>가 있을 때 Iterable<Integer>Iterable<Number>의 하위 타입도 상위 타입도 아니다.

한정적 와일드카드 타입을 사용하자

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

이처럼 <? extends T> 를 통해 매개변수화 타입을 사용하여 해결할 수 있다.

참조형 매개변수의 자료형을 T와 T의 자손 타입에 대해서만 가능하도록 제한

PECS: produce-extends, consumer-super

위 공식은 어떤 와일드카드를 써야 하는지 기억하는데 있어서 도움이 된다.

즉, 매개변수화 타입이 T가 생상자라면 <? extends T> 를 사용하고, 소비자라면 <? super T>를 사용하면 된다.

max 메서드도 한정적 와일드카드를 이용해 다듬을 수 있다. 입력 매개변수(c)는 E인스턴스를 생상하므로 extends이고, 타입 매개변수는 E 인스턴스를 소비하므로 super이다.

어떤 인터페이스를 직접 구현한 클래스를 확장한 타입을 지원하기 위해 한정적 와일드카드가 필요하다.

한정적 와일드카드를 씀으로써 계층구조를 유연하게 이용할 수 있다.


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

가변인수 메서드를 호출하면 가변인수를 담기 위해 배열이 생성되기 떄문에, 실체화 불가 타입인 제네릭 매개변수화 타입이 포함되면 안전하지 않다.

컴파일 오류는 발생하지 않지만, 인수를 건네 호출하게 되면 ClassCastException이 발생한다.

해당 코드 부분에 컴파일러가 생성한 형변환 코드가 숨어 있기 때문이다.

제네릭 가변인수 배열을 안전하게 사용하는 방법

varargs 매개변수 배열이 순수하게 인수들을 전달한다면 그 메서드는 안전하다.

  • 메서드가 가변인수 메서드가 호출될 때 생성되는 varargs 매개변수 배열에 아무것도 저장하지 않아야 한다.
  • 배열의 참조가 신뢰할 수 없는 곳에 노출되지 않아야 한다.

타입 안전 이종 컨테이너를 고려하라

제네릭에서 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.

타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)이란

  • 컨테이너 대신 키를 타입 매개변수화 한다.
  • 컨테이너에서 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공한다.
  • 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해준다.

주의사항

  • 타입 토큰을 로 타입(raw type)으로 넘길 경우 타입 안정성이 깨진다.
    • 동적 형변환을 통해 런타임 타입 안정성을 확보할 수 있다.
  • 실체화가 불가능한 타입은 넣을 수 없다.
    • String, String , List (x)

슈퍼 타입 토큰

런타임에 파라미터 타입에 대한 정보가 남아있도록 구현하는 것


https://dahye-jeong.gitbook.io/java/java/effective_java/2021-05-30-careful-when-using-generic-and-varargs

profile
개발계발

0개의 댓글