개발자 피터
Peter's Dev Blog
개발자 피터
전체 방문자
오늘
어제
  • 분류 전체보기 (77)
    • 🧑🏻‍💻 Develop (13)
      • Devops (3)
      • Elasticsearch (3)
      • Design Pattern (1)
      • SQL (4)
      • Architecture (1)
      • APM (1)
    • 💻 Service (7)
      • E-ROOM (3)
      • Briefing (4)
    • 💡 Problem Solving (43)
      • Baekjoon (40)
      • Programmers (2)
    • 📚 Reading (12)
      • Tech (9)
      • Self-Help (3)
    • 💬 Etc (1)
    • 📈 Retrospective (1)

블로그 메뉴

  • 🌟 깃허브
  • 🏷️ 태그 클라우드
  • 📝 방명록

공지사항

인기 글

태그

  • elasticsearch
  • briefing
  • boj
  • java
  • MySQL
  • E-ROOM
  • SQL
  • 독서
  • 백준
  • 브루트포스
  • 그리디
  • 문자열
  • 백트래킹
  • 구현
  • Programmers

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
개발자 피터

Peter's Dev Blog

📚 Reading/Tech

이펙티브 자바 - 5장. 제네릭

2023. 1. 24. 15:15
728x90
반응형

5장 - 제네릭


  • 5장 용어 정리
한글 용어 영문 용어 예
매개변수화 타입 parameterized type List<String>
실제 타입 매개 변수 actual type parameter String
제네릭 타입 generic type List<E>
정규 타입 매개 변수 formal type parameter E
비한정적 와일드카드 타입 unbounded wildcard type List<?>
로 타입 raw type List
한정적 타입 매개변수 bounded type bound <T extends Number>
재귀적 타입 한정 recursive type bound <T extends Comparable<T>>
한정적 와일드카드 타입 bounded wildcard type List<? extends Number>
제네릭 메서드 generic method static <E> List<E> asList(E[] a)
타입 토큰 type token String.class

💡 로 타입은 사용하지 말라

“로 타입을 사용하면 런타임에 예외가 일어날 수 있으므로 사용하면 안 됨”

1. raw type 사용의 문제점

  • 로 타입을 사용하면 제네릭이 안겨주는 타입 안정성과 표현력을 모두 잃게 됨.
  • 문제가 되는 경우 예시 코드
    • 아래의 예시를 통해 로 타입 사용시 런타임 예외가 일어날 수 있음을 확인
// Stamp 인스턴스만 취급한다.
private final Collection stamps = ...;

// stamps에 도장(Stamp) 대신 동전(Coin)을 넣어도 아무 오류 없이 컴파일되고 실행 됨
stamps.add(new Coin(...));

// 컬렉션에서 이 동전을 꺼낼 때, 런타임 예외가 발생
for (Iterator i = stamps.iterator(); i.hasNext();) {
        Stamp stamp = (Stamp) i.next(); // ClassCastException을 던진다.
        stamp.cancel();
}

 

2. 비한정적 와일드 카드 : ?

  • 제네릭의 하위 타입 규칙
    • 몇 가지 예시
    • String은 Object의 하위 타입인가? : 하위타입이다.
    • List<String>은 List의 하위 타입인가? : 하위타입이다.
    • List<String>은 List<Object>의 하위 타입인가? : 아니다. 상하관계가 없다.

→ 매개변수화 타입간에는 상하관계가 없다.

→ 따라서 아래 코드는 컴파일되지 않음.

public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();

        // List<String>이 List<Object>의 하위타입이 아님.
        unsafeAdd(strings, "test");
    }

    private static void unsafeAdd(List<Object> list, Object o) {
        list.add(o);
    }
}
  • 그러면 위와 같은 상황을 어떻게 해결하는가? : 비한정적 와일드 카드를 사용한다.
public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();

        // List<String>이 List<Object>의 하위타입이 아님.
        unsafeAdd(strings, "test");
    }
    // Object를 ?로 바꿔주었음.
    private static void unsafeAdd(List<?> list, Object o) {
        list.add(o);
    }
}
  • 비한정적 와일드카드 타입(?)은 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 때 사용.

 

3. raw type을 사용해야하는 경우

  • raw type을 사용해야 하는 예외 경우가 2가지 있음
  1. class 리터럴에는 로 타입을 써야 함.
    • ex) List.class, String[].class, int.class 가능 / List<String>.class, List<?>.class 불가능
  2. instanceof 연산자
    • 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없음.
    • 비한정적 와일드 카드 타입이든 로 타입이든 instanceof는 완전히 똑같이 동작함. 그러니 깔끔한 로 타입을 사용하자
    • ex) if(o instanceof Set) { ... }

 


💡 비검사 경고를 제거하라

“비검사 경고는 중요하니 무시하지 말자”

1. 비검사 경고

  • 비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입 경고, 비검사 변환 경고 등이 있음
  • 비검사 경고 예시
// 타입 매개변수를 명시하지 않음 : 비검사 변환 경고
Set<Lark> exaltation = new HashSet();

// 다이아몬드 연산자(<>)로 해결 : 자바7부터 타입 매개변수를 추론해 줌
Set<Lark> exaltation = new HashSet<>();
  • 할 수 있는 한 모든 비검사 경고를 제거하자.
    • 모두 제거한다면 그 코드는 타입 안정성 보장 → 런타임에 ClassCastException 발생 절대 X

 

2. @SuppressWarnings 어노테이션

  • 경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면, @SuppressWarnings(”unchecked”) 어노테이션을 달아 경고를 숨기자.
  • @SuppressWarnings 어노테이션은 항상 가능한 한 좁은 범위에 적용하자
    • 변수 선언, 아주 짧은 메서드, 생성자 등
    • 절대로 클래스 전체에 적용해서는 안 됨
  • @SuppressWarnings(”unchecked”) 어노테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 함.

 


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

1. 배열 vs 리스트

  배열 리스트(제네릭)
문법 String[] List<String>
상하관계 Sub가 Super의 하위 타입일 때,
Sub[]도 Super[]의 하위 타입

→공변(covariant)
Type1이 Type2의 하위 타입일 때,
List<Type1>는 List<Type2>의 하위 타입도 상위 타입도 아님 (상하관계 없음).

→ 불공변(invariant)
타입 안전 컴파일 : 타입 안전 X
런타임 : 타입 안전 O

→ 컴파일 시점에 타입 안전성을 보장 받지 못하여 런타임에 예외 발생 가능
컴파일 : 타입 안전 O
런타임 : 타입 안전 X

→ 컴파일 시점에 타입 안전성을 보장 받음
실체화 실체화

→ 런타임에도 자신이 담기로 한 원소의 타입을 인지 & 확인
실체화 불가(소거 매커니즘)

→ 런타임에는 원소의 타입 정보를 알 수 없음(컴파일 시점에 타입 정보 소거), 비한정적 와일드 카드 타입은 예외.

2. 배열보다는 리스트

  • 공변일 때 발생할 수 있는 문제
// 배열은 공변이므로 정상적으로 컴파일 됨
Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다." // 런타임에 ArrayStoreException을 던진다
// 제네릭은 불공변이므로 컴파일이 실패 함
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입
ol.add("타입이 달라 넣을 수 없다.");

→ 이렇듯 배열은 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일 시점에 실수를 바로 잡을 수 있음.

  • 주요 차이들로 인해 둘을 섞어 쓰기는 어려움. 리스트를 사용하자.
  • 둘을 섞어 쓰다가 오류나 경고를 만나면, 배열을 리스트로 대체해보자.

 


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

“클라이언트에서 직접 형변환해야하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다”

1. 제네릭 타입

  • 타입 매개변수를 이용해 제네릭으로 만든 스택
// E[]를 이용한 제네릭 스택
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
    // 따라서 타입 안전성을 보장하지만,
    // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) new Object[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);
    }
}
  • 제네릭 타입의 장점 : 클라이언트에서 직접 형변환하지 않아도 됨.

 

2. 한정적 타입 매개변수

  • 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다.
    • Stack<Object>, Stack<String> 등 어떤 참조 타입으로도 Stack을 만들 수 있음.
    • 단, Stack<int>처럼 기본 타입은 사용할 수 없음.
  • 한정적 타입 매개변수 : 하위 타입만 받을 수 있음
    • ex) <E extends Delayed> : java.util.concurrent.Delayed의 하위 타입만 받을 수 있음.

 


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

“클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다
제네릭 메서드가 더 안전하며 사용하기 쉽다”

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;
}
  • 단순한 제네릭 메서드라면 이 이 정도로 충분함
  • 현재 이 메서드는 집합 3개(입력 2개, 반환 1개)의 타입이 모두 같아야 하는데, 한정적 와일드카드 타입을 사용하여 더 유연하게 개선할 수 있음.
  • 제네릭 메서드의 장점 : 클라이언트에서 입력 매개변수와 반환 값을 명시적으로 형변환하지 않아도 됨.

 

2. 재귀적 타입 한정

  • 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있음
  • 주로 타입의 자연적 순서를 정하는 Comparable 인터페이스와 함께 쓰임
// 컬렉션에서 최댓값을 반환한다. - 재귀적 타입 한정 사용
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;
}
  • <E extends Comparable<E>> : “모든 타입 E는 자신과 비교할 수 있다.”

 


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

“PECS공식으로 API를 유연하게 만들자”

1. PECS 공식

  • PECS : producer-extends, consumer-super
  • 매개변수화 타입 T가 생산자라면 <? extends T>를 사용(하위타입으로 제한)하고, 소비자라면 <? super T>를 사용(상위타입으로 제한)하라.
  • produce-extends 예시
    • 생성자로 넘겨지는 choices 컬렉션은 T 타입의 값을 생산하기만 하니(그리고 나중을 위해 저장해둔다), T를 확장하는 와일드카드 타입을 사용해 선언해야 함.
    • 이렇게 하면 Chooser<Number>의 생성자에 List<Integer>를 넘길 수 있음.
    public class Chooser<T> {
        private final List<T> choiceList;
        private final Random rnd = new Random();

        // T 생산자 매개변수에 와일드카드 타입 적용
        public Chooser(Collection<? extends T> choices) {
            choiceList = new ArrayList<>(choices);
        }

        public T choose() {
            return choiceList.get(rnd.nextInt(choiceList.size()));
        }

        public static void main(String[] args) {
            List<Integer> intList = List.of(1, 2, 3, 4, 5, 6);
            Chooser<Number> chooser = new Chooser<>(intList);
            for (int i = 0; i < 10; i++) {
                Number choice = chooser.choose();
                System.out.println(choice);
            }
        }
    }
  • consumer-super 예시
    • 스택의 popAll의 dst 매개변수는 Stack으로 부터 E 인스턴스를 소비하므로 dst의 적절한 타입은 Collection<? super E> dst임.
      // E 소비자(consumer) 매개변수에 와일드카드 타입 적용
      public void popAll(Collection<? super E> dst) {
       while (!isEmpty())
           dst.add(pop());
      }

2. 비한정적 타입 매개변수 vs 비한정적 와일드카드

  • 기본 규칙 : 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라.
  • 아래 예시에서는 간단한 두 번째 선언이 더 나음 (public API에 적합)
    // swap 메서드의 두 가지 선언
    public static <E> void swap(List<E> list, int i, int j);
    public static void swap(List<?> list, int i, int j);

 


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

“제네릭과 가변인수를 혼용하면 타입 안정성이 깨진다”

1. 가변 인수(...)

  • 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해줌.
  • 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 생성됨.

 

2. 제네릭과 가변인수의 혼용

  • 제네릭과 가변인수를 혼용했을 때의 문제
    • 힙 오염(heap pollution) : 매개변수화 타입의 변수가 타입이 다른 객체 참조했을 때 발생
    • 아래 예시처럼 타입 안정성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않음.
    // 제네릭과 varargs를 혼용하면 타입 안전성이 깨진다!
    static void dangerous(List<String>... stringLists) {
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList; // 힙 오염 발생
        String s = stringLists[0].get(0); // ClassCastException
    }

 

3. @SafeVarargs 어노테이션

  • 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치
  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달아라.
    • 이 말은 안전하지 않은 varargs 메서드는 절대 작성해서는 안된다는 뜻임.
  • 다음 두 조건을 만족하는 제네릭 varargs 메서드는 안전함
    1. varargs 매개변수 배열에 아무것도 저장하지 않는다.
    2. 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
  • 제네릭 varargs 매개변수를 안전하게 사용하는 메서드 예시
    @SafeVarargs
    static <T> List<T> flatten(List<? extends T>... lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }

 

4. 가변인수 대신 List를 사용하는 방식

  • “배열보다는 리스트를 사용하라”라는 조언에 따라 (실체는 배열인) varargs 매개변수를 List매개변수로 바꿀 수 있음
  • 제네릭 varargs 매개변수를 List로 대체한 메서드 예시
    static <T> List<T> flatten(List<List<? extends T>> lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }

 


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

“컨테이너에서 매개변수화 타입의 수가 임의의 수라면
타입 안전 이종 컨테이너 패턴을 사용하자”

1. 타입 안전 이종 컨테이너 패턴

  • 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 방식
  • 각 타입의 클래스 객체를 키 역할로 사용하는 예제를 살펴보자
용어 설명
class 리터럴 String.class
class 리터럴의 타입 Class
타입 토큰 메서드들이 주고 받는 class 리터럴
- 타입 안전 이종 컨테이너 패턴 - API 코드  
public class Favorites {
        public <T> void putFavorite(Class<T> type, T instance);
        public <T> T getFavorite(Class<T> type);
}
  • 타입 안전 이종 컨테이너 패턴 - API 구현
    • cast 메서드는 형변환 연산자의 동적 버전이다.
      • 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 검사한 다음, 맞다면 그 인수를 그대로 반환, 아니면 ClassCastException을 던짐.
public class Favorites {   
    private Map<Class<?>, Object> favorites = new HashMap<>();

    // 동적 형변환으로 런타임 타입 안전성 확보
    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), type.cast(instance));
    }
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}
  • 타입 안전 이종 컨테이너 패턴 - 클라이언트

public static void main(String[] args) {
    Favorites f = new Favorites();

    f.putFavorite(String.class, "Java");
    f.putFavorite(Integer.class, 0xcafebabe);
    f.putFavorite(Class.class, Favorites.class);

    String favoriteString = f.getFavorite(String.class);
    int favoriteInteger = f.getFavorite(Integer.class);
    Class<?> favoriteClass = f.getFavorite(Class.class);

    System.out.printf("%s %x %s%n", favoriteString,
            favoriteInteger, favoriteClass.getName());
}
  • 이 프로그램은 Java cafebabe Favorites를 출력함.

 

2. 한정적 타입 토큰

  • 한정적 타입 토큰 : 한정적 타입 매개변수나 한정적 와일드카드를 사용해 표현 가능한 타입을 제한하는 타입 토큰
  • 사용 예시 : getAnnotation 메서드
    • annotationType 인수는 어노테이션 타입을 뜻하는 한정적 타입 토큰이다.
    • 이 메서드는 토큰으로 명시한 타입의 어노테이션이 대상 요소에 달려있다면 어노테이션을 반환하고, 없다면 null을 반환함
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
728x90
반응형
저작자표시 (새창열림)

'📚 Reading > Tech' 카테고리의 다른 글

이펙티브 자바 - 7장. 람다와 스트림  (0) 2023.01.24
이펙티브 자바 - 6장. 열거 타입과 애너테이션  (0) 2023.01.24
이펙티브 자바 - 4장. 클래스와 인터페이스  (0) 2023.01.24
이펙티브 자바 - 3장. 모든 객체의 공통 메서드  (0) 2023.01.24
이펙티브 자바 - 2장. 객체 생성과 파괴  (0) 2023.01.24
    '📚 Reading/Tech' 카테고리의 다른 글
    • 이펙티브 자바 - 7장. 람다와 스트림
    • 이펙티브 자바 - 6장. 열거 타입과 애너테이션
    • 이펙티브 자바 - 4장. 클래스와 인터페이스
    • 이펙티브 자바 - 3장. 모든 객체의 공통 메서드
    개발자 피터
    개발자 피터
    Backend Engineer 🔥

    티스토리툴바