Java

[Effective Java] 5-2. 제네릭: 권고 사항

noahkim_ 2024. 12. 28. 07:27

조슈아 블로크 님의 "Effective Java" 책을 정리한 포스팅 입니다.

 

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

배열은 공변

Object[] objArray = new Long[1];
objArray[0] = "hello"; // 런타임에 ArrayStoreException 발생
  • 구현 타입으로 변경됨
  • 잘못된 타입이 할당될 경우, 런타임에 예외 발생 

 

제네릭은 불공변

List<Object> ol = new ArrayList<Long>(); // 호환 X. 컴파일 타임에 오류 발생!
ol.add("hello");
  • 매개변수 타입과 관련없이 제네릭 타입은 서로 다른 타입
  • 일치하지 않을 경우, 컴파일 타임에 오류

 

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

  • 클래스 선언에 매개변수 타입을 추가하기

 

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

타입 매개변수 목록 메서드의 제한자와 반환 타입 사이에 옴

public <E> set<E> union(Set<E> s1, Set<E> s2) {
	// ...
}

 

제네릭 싱글턴

  • 제네릭 타입과 싱글턴 패턴을 결합한 개념
  • 불변 객체를 제네릭 타입으로 활용하여 여러 타입에 대하여 재사용할 수 있도록 설계된 패턴

 

 

항등함수를 담은 클래스
public class GenericSingleton {
    // 항등 함수 (불변 객체)
    private static final UnaryOperator<Object> IDENTITY_FUNCTION = t -> t;

    // 타입 안전한 싱글턴 인스턴스 제공
    @SuppressWarnings("unchecked")
    public static <T> UnaryOperator<T> identityFunction() {
        return (UnaryOperator<T>) IDENTITY_FUNCTION;
    }

    public static void main(String[] args) {
        // String 타입 항등 함수
        UnaryOperator<String> stringIdentity = GenericSingleton.identityFunction();
        System.out.println(stringIdentity.apply("Hello")); // 출력: Hello

        // Integer 타입 항등 함수
        UnaryOperator<Integer> intIdentity = GenericSingleton.identityFunction();
        System.out.println(intIdentity.apply(42)); // 출력: 42
    }
}
  • 불변 객체를 여러 타입으로 활용

 

재귀적 타입 한정
public class RecursiveTypeBound {
    // 최대 값을 반환하는 제네릭 메서드
    public static <T extends Comparable<T>> T max(List<T> list) {
        if (list.isEmpty()) throw new IllegalArgumentException("List is empty");

        T max = list.get(0);
        for (T item : list) {
            if (item.compareTo(max) > 0) max = item;
        }
        
        return max;
    }

    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 3, 2, 5, 4);
        System.out.println("Max Integer: " + max(intList)); // 출력: Max Integer: 5

        List<String> strList = Arrays.asList("apple", "banana", "cherry");
        System.out.println("Max String: " + max(strList)); // 출력: Max String: cherry
    }
}
  • 타입 매개변수가 자기 사진을 상위 타입으로 제한하는 방식
  • Comparable과 함께 쓰임
    • 타입 T는 반드시 Comparable<T>를 구현해야 하며, 자기 자신과 비교할 수 있어야 함

 

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

제네릭 타입은 불공변

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

numberStack.pushAll(integers); // 컴파일 오류!
  • Stack<Number>의 pushAll(integer...)일 경우 컴파일 오류 발생
  • Stack<Number>와 Stack<Integer>와는 아무 상관 없음
  • 제네릭 타입이 타입 안전성을 보장하기 위함

 

한정적 와일드카드

  • 제네릭 타입의 유연성을 극대화 하는 방법
  • 특정 상위 타입이나 하위 타입으로 제한된 타입 매개변수를 지정할 수 있음

 

<? extends T>
public class Stack<E> {
    private List<E> elements = new ArrayList<>();

    public void push(E element) {
        elements.add(element);
    }

    // <? extends E> 사용: 하위 타입 허용
    public void pushAll(Iterable<? extends E> src) {
        for (E e : src) {
            push(e);
        }
    }
}

// 사용
public static void main(String[] args) {
    Stack<Number> stack = new Stack<>();
    List<Integer> integers = Arrays.asList(1, 2, 3);
    stack.pushAll(integers); // 가능: Integer는 Number의 하위 타입
}
  • 타입 매개변수의 상위타입 지정할 수 있음
  • 타입 매개변수가 생산자일 경우 유용 (읽기)

 

<? super T>
public class Stack<E> {
    private List<E> elements = new ArrayList<>();

    public E pop() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() - 1);
    }

    // <? super E> 사용: 상위 타입 허용
    public void popAll(Collection<? super E> dst) {
        while (!elements.isEmpty()) {
            dst.add(pop());
        }
    }
}

// 사용
public static void main(String[] args) {
    Stack<Integer> stack = new Stack<>();
    stack.push(1);
    stack.push(2);

    List<Number> numbers = new ArrayList<>();
    stack.popAll(numbers); // 가능: Number는 Integer의 상위 타입
}
  • 타입 매개변수의 하위타입을 지정할 수 있음
  • 타입 매개변수가 소비자일 경우 유용 (쓰기)