BottleH Blog

이펙티브자바 3판 5장 정리

    Tags

  • Java
이펙티브자바 3판 5장 정리 thumbnail

5장 제네릭

제네릭은 jdk1.5 부터 사용할 수 있다. 제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때 마다 형변환을 해야 했다. 1.5 부터는 제네릭을 사용하면 컬렉션에 담을 수 있는 타입을 컴파일러에게 알려주며, 컴파일러가 알아서 형변환 코드를 추가한다. 또한 엉뚱한 객체를 넣는 코드가 있다면 컴파일 타임에 차단해준다.

목차


아이템26. 로 타입은 사용하지 말라


26-1. raw Type의 정의

클래스와 인터페이스 선언에 타입 매개변수(ex. <E>)가 있으면 각각 제네릭 클래스, 제네릭 인터페이스라고 부른다. 이것을 제네릭 타입이라고 한다. (ex. List<E>) 제네릭 타입을 정의하면 raw type도 정의되는데, 여기서 List<E>의 raw type이란 List 이다. 즉, __타입 매개변수를 쓰지 않은 경우__를 말한다. 이는 제네릭이 도입되기 전의 코드들의 호환성을 위한 것이다.

26-2. 로 타입 비추천 예시

// Stamp 인스턴스만 취급 private final Collection stamps = ...; // 실수로 동전을 넣는다. stamps.add(new Coin(...)); // "unchecked call" 경고를 내뱉는다.

오류의 발견은 컴파일 타임에 되는게 가장 이상적이다. raw type을 사용할 경우, unchecked 경고가 나오며, 잘못된 타입을 add할 수도 있다. 위의 예시는 런타임에서 문제가 생길 것이다. (ClassCastException 등)
따라서 raw type을 쓰지말고, 제네릭 타입을 쓴다면 컴파일러의 검사력(정적언어의 장점을 활용)과 타입 불변, 안정성을 얻을 수 있다.

  • 자바와 같은 JVM언어 중에 코틀린은 제네릭을 쓰지 않으면 컬렉션을 쓰지 못하도록 아예 막아버렸다. 자바는 하위호환성 때문에 쓰지말라고 권고만 함
private final Collection<Stamp> stamps = ...;

위와 같이 매개변수화된 컬렉션 타입을 이용해 안전성을 확보하자.

26-3. 로 타입 / 와일드카드(<?>) / <Object>

List rawList = new ArrayList<String>(); // 런타임에는 아무런 타입이 남지 않기때문에 컴파일 성공 List<?> wildList = new ArrayList<String>(); // 컴파일 성공 List<Object> genericList = new ArrayList<String>(); // 컴파일 실패

만약 타입 매개변수를 신경쓰지 않고 쓰고싶다해도 raw type보단 제네릭의 와일드카드를 쓰는 것이 좋다. (ex. Set<?>) 이렇게 하면 어떤 타입도 받을 수 있으면서 안전하며 유연해진다.
raw type과 와일드카드의 차이점은 raw type은 안전하지 않고, 와일드카드는 안전하다는 것이다.

  • raw type 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다.

와일드카드와 <Object>의 차이는 구체적 인스턴스의 차이인데, 와일드카드는 제네릭 타입 매개변수에 의존성이 있는 코드가 있다면 컴파일러가 실패처리한다. <Object>는 내부에서 또 다시 형 변환해야하므로 코드가 좀 더 복잡해지며, 제네릭의 장점이 사라진다.

26-4. 로 타입을 쓰는 예외

  • class 리터럴은 raw type으로 써야한다.
    • List.class 는 되지만, List<String>.class, List<?>.class 은 허용되지 않는다.
if (o instanceof Set){ Set<?> s = (Set<?>) o; ... }
  • instanceof 연산자는 런타임에서 타입을 비교한다. 제네릭 타입은 런타임에서 소거되므로 제네릭 타입으로 비교할 수 없다.
    • o의 타입이 Set임을 확인한 다음 와일드카드 타입인 Set<?>로 형변환해야 한다.

아이템27. 비검사 경고를 제거하라


비검사 경고(ex. warning: [unchecked] unchecked ...)를 제거할수록 타입 안정성이 높아진다고 볼 수 있다.

27-1. @SuppressWarnings("unchecked") 사용하기

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

  • 반드시 가능한 한 좁은 범위에 적용 => 자칫 심각한 경고를 놓칠 수 있음.
  • 해당 애너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다.

결론: 모든 비검사 경고는 런타임에 ClassCastException을 일으킬 수 있는 잠재적 가능성을 뜻하니 최선을 다해 제거하라.

  • ClassCastException - 객체 타입변환이 적절하지 않을 때 발생함.

아이템28. 배열보다는 리스트를 사용하라


28-1. 배열과 리스트의 첫번째 주요차이

  • 배열: 공변(함께 변함)이다.
    • ex) Sub가 Super의 하위타입이면, 배열 Sub[]는 배열 Super[]의 하위 타입이다.
  • 리스트: 불공변이다.
    • ex) 서로다른 Type1, Type2가 있을 때, List<Type1>List<Type2>는 관계가 없다.
/* 문법상 허용되는 코드 - 런타임에 실패한다 */ Object[] objectArray = new Long[1]; objectArray[0] = "타입이 달라 넣을 수 없다."; //ArrayStoreException을 던진다. /* 문법상 허용되지 않는 코드 - 컴파일에 실패한다 */ List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다. ol.add("타입이 달라 넣을 수 없다.");

위의 코드처럼 배열에서는 그 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있다.

28-2. 배열과 리스트의 두번째 주요차이

  • 배열: 실체화(런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인)된다.
  • 리스트(제네릭): 타입정보가 런타임에는 소거된다. => 자바5가 제네릭으로 순조롭게 전환될 수 있도록 해줌!

28-3. 제네릭 배열을 만들지 못하게 막은 이유

타입이 안전하지 않기 때문

List<String>[] stringLists = new List<String>[1]; List<Integer> intList = List.of(42); Objects[] objects = stringLists; objects[0] = intList; String s = stringLists[0].get(0);

위와 같은 코드가 가능하다고 가정하면 List<String> 인스턴스만 담겠다고 선언한 stringLists 배열에는 지금 List<Integer> 인스턴스가 저장돼 있다.

결론 : 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거됨. 이 둘을 섞어쓰기는 쉽지 않으니 섞어쓰다가 컴파일 오류가 경고를 만나면, 배열을 리스트로 대체하는 방법을 적용해 볼 것!

아이템29. 이왕이면 제네릭 타입으로 만들라


제네릭 배열 생성을 제거하는 방법은 2가지가 있다.

29-1. 제네릭 배열 생성을 금지하는 제약을 대놓고 우회

  1. Object배열 생성
  2. 제네릭배열로 형변환 => 컴파일러가 경고를 내보냄
  3. 타입안전성을 해치지 않는지 스스로 증명
  4. 증명 후, @SuppressWarnings 애너테이션으로 해당 경고를 숨김

29-2. elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것

ex)

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

29-3. 예제코드

public class Stack { // => Stack<E> private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; // => @SuppressWarnings("unchecked") public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; // => (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { // => push(E e) ensureCapacity(); elements[size++] = e; } private void ensureCapacity() { if (elements.length == size) { elements = Arrays.copyOf(elements, 2 * size + 1); } } public Object pop() { // => E pop() if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; // => E result = elements[--size]; elements[size] = null; return result; } }

결론: 첫 번째 방법이 가독성, 편리성 등으로 인해 많이 쓰이지만 힙 오염을 일으킬 수 있어 두번째 방법을 사용하기도 한다.

아이템30. 이왕이면 제네릭 메서드로 만들라


클래스와 마찬가지로, 메서드도 제네릭으로 만들 수 있다.

30-1. 참고예제

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

30-2. 제네릭 싱글턴 팩토리 패턴

@SuppressWarnings("unchecked") public static <T> Comparator<T> reverseOrder() { return (Comparator<T>) ReverseComparator.REVERSE_ORDER; }

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

  • ex) Collections.reverseOrder, Collections.emptySet

30-3. 재귀적 타입 한정

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

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

<E extends Comparable<E>>는 "모든 타입 E는 자신과 비교할 수 있다"라는 뜻이다.

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


아이템 28에서 나왔듯이 제네릭의 매개변수화 타입(ex. E)은 불공변이다. 아이템 29의 제네릭 클래스 Stack을 예로 들어보자. 아래 메소드가 추가되었다.

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

이는 불공변 때문에 에러가 발생한다. Iterable<Number>가 넘어와야 하는데 Iterable<Integer>가 넘어왔기 때문이다.

31-1. 한정적 와일드카드 타입을 사용하자

한정적 와일드카드 타입을 사용하면 해결할 수 있다. E의 Iterable이 아닌 E의 하위타입의 Iterable(Iterable<? extends E>로 만들면 된다.

✔ PECS(producer-extends, consumer-super)

  • 매개변수화 타입 T가 생산자라면, <? extends T>를 사용하고, 소비자라면 <? super T>를 사용해라.

❗ 주의사항: 반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다. 클라이언트 코드에서도 와일드카드 타입을 써야하기 때문이다.

31-2. 타입 매개번수와 와일드카드

타입 매개변수와 와일드카드에는 공통된 부분이 있어서, 메소드를 정의할 때 어느 것을 사용해도 괜찮을 때가 많다.

public static <E> void swap(List<E> list, int i, int j); public static void swap(List<?> list, int i, int j);

두번째 방법의 장단점

  • 장점: 신경써야할 타입 매개변수가 없음

  • 단점: 직관적으로 구현한 코드가 컴파일 되지 않음.

    • public static void swap(List<?> list, int i, int j){ list.set(i, list.set(j, list.get(i))); }
    • List<?>에는 null 외에는 어떤 값도 넣을 수 없기 때문

결론: 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다.

아이템32. 제네릭과 가변인수를 함께 쓸 때는 신중해라


가변인자는 제네릭과 함께 자바5에 추가되었으나, 이 둘을 혼용하면 타입 안정성이 깨질 수 있다. 가변인자를 받는 메소드를 호출하면 호출시점에 배열이 생긴다.

  • 아이템 28에서 애초에 만들 수 없다던 제네릭 배열이 만들어짐
  • 가변인자가 실무에서 매우 유용하기 때문에 모순이지만 수용했다.

가변인자: 매개변수의 개수를 동적으로 지정해주는 기능

@SafeVarargs @SuppressWarnings("varargs") public static <T> List<T> asList(T... a) { return new ArrayList<>(a); }

32-1. @SafeVarargs

자바7부터 도입된 @SafeVarargs를 사용하면 제네릭 가변인수와 관련된 경고를 숨길 수 있다. @SafeVarargs는 제네릭 가변인자 메소드 작성자가 그 메소드가 타입 안전함을 보장하는 장치다.

✔ 검증방법

  • 메서드가 이 배열에 아무것도 저장하지 않는다.(매개변수들을 덮어쓰지 않는다.)
  • 그 배열의 참조가 밖으로 노출되지 않는다.(신뢰할 수 없는 코드가 배열에 접근할 수 없다.)

결론: 32-1의 두가지 조건을 만족하면 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달라. 더불어, @SafeVarargs 애너테이션은 재정의할 수 없는 메서드에만 달아야 한다.

아이템33. 타입 안전 이종 컨테이너를 고려하라


33-1. 타입 안전 이종 컨테이너란?

Set<E>, Map<K,V> 처럼 클래스 레벨에서 매개변수화 할 수 있는 타입의 수는 제한적이다.
타입의 수에 제약없이 유연하게 필요한 경우, 특정 타입 외에 다양한 타입을 지원해야하는 경우가 있을 수 있다. 이 때 클래스 대신 키를 매개변수화한 다음 키 타입을 제공해주면 된다. 이것을 타입 안전 이종(heterogeneous) 컨테이너 패턴 이라고 한다.

타입토큰: 컴파일타임 타입 정보와 런타임 타입 정보를 위해 메소드에서 주고 받는 class 리터럴
ex) Integer.class는 class 리터럴이며 타입토큰은 Class<Integer>

// 타입 안전 이종 컨테이너 패턴 public class Favorites{ public <T> void putFavorite(Class<T> type, T instance); public <T> T getFavorite(Class<T> type); }

33-2. 타입 안전 이종 컨테이너의 제약

  1. 악의적인 클라이언트가 Class 객체를 (제네릭이 아닌) 로 타입으로 넘기면 타입 안전성이 쉽게 깨진다.
  2. 실체화 불가 타입에는 사용할 수 없다.
    • ex) List<String>은 사용할 수 없다. List<String>.class 라는 리터럴을 얻을 수 없기 때문이다.

33-3. 슈퍼 타입 토큰

33-2.의 2번의 완벽한 우회로는 없다. 하지만 슈퍼 타입 토큰으로 해결하려는 시도는 있다. 슈퍼 타입을 토큰으로 사용한다는 의미이며, 상속과 Reflection을 조합해서 List<String>.class 같이 사용할 수 없는 class 리터럴을 타입 토큰으로 사용하는 것과 같은 효과를 낼 수 있다.

  • 스프링 프레임워크에서는 이것을 클래스로 구현을 해두었다.(ParameterizedTypeReference)
Written by@BottleH
Back-End Developer

GitHub