Java/Effective Java

[Effective Java] 5장. 제네릭

SeungbeomKim 2025. 5. 17. 18:54

[item-26] 로 타입은 사용하지 말라

 

제네릭 타입: 클래스 혹은 인터페이스 선언에 타입 매개변수가 쓰이는 경우

로 타입: 제네릭 타입에서 타입 매개변수를 사용하지 않는 것

  • ex) List, Set ..(제네릭 타입) → List<String>, Set<String>
  1. 제네릭 타입을 하나 정의하면 Raw Type도 함께 정의됩니다.
  2. Raw Type은 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 의미합니다.
  3. Raw Type은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작합니다.

 

로 타입을 절대 쓰지 말자

  • 자바 언어 차원에서 로 타입 사용을 막지는 않았지만, 절대로 사용하지 말자
  • 로 타입을 쓰면 제네릭의 안정성과 표현력을 모두 잃게 됩니다.
  • 로 타입은 호환성 때문에 만들어졌습니다.

 

로 타입을 사용

private final Collection stamps = ...;
stamps.add(new Coin(...));
// unchecked call "경고"를 호출하지만 컴파일도 되고 실행도 됩니다
  • add 메서드 호출 시, ClassCastException 오류 발생 (Runtime Exception)

 

제네릭 사용

private final Collection<Stamp> stamps = ...;
stamps.add(new Coin()); // 컴파일 오류 발생
  • 컴파일 오류가 바로 발생

 

로 타입과 Object

  • List 같은 로 타입은 사용해서는 안되지만, List <Object>처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮습니다.
  • List는 제네릭 타입에서 완전히 발을 뺀 것입니다.
  • List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에게 명확히 전달해야 합니다.
  • 매개변수로 List를 받는 메서드에 List<String>을 넘길 수 있지만, List<Object>를 받는 메서드에는 넘길 수 없습니다.
  • rawType을 사용하면 컴파일은 되지만, 실행하면 ClassCastException이 발생합니다. 하지만, 매개변수화 타입을 사용하면 컴파일조차 되지 않습니다.

 

로 타입을 사용한 경우 (list)

  • 런타임 시점에, strings.get(0) 호출 시 ClassCastException 발생
public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    
    add(strings, Integer.valueOf(0));
    String s = strings.get(0);
}

// raw type
private static void add(final List list, final Object o) {
    list.add(o);
}

 

 

List<Object> 변경 시

  • 컴파일 에러, incompatible types: List<String> cannot be converted to List<Object> 메시지 출력
public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    
    add(strings, Integer.valueOf(0));
    String s = strings.get(0);
}

// raw type
private static void add(final List<Object> list, final Object o) {
    list.add(o);
}

 

 

Raw 타입 예시 (잘못 사용 예시)

  • 동작은 하지만 안전하지 않습니다
  • 그렇기에 비한정적 와일드 카드 타입인 ?를 대신해서 사용하는 게 좋습니다.
    • 제네릭을 사용하고 싶지만, 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않을 때 사용합니다.
    • Set<E>의 비한정적 와일드카이드 타입은 Set<?>
  • Set<?>은 어떤 타입의 Set이든 참조할 있는 범용 타입이지만, 타입을 모르므로 요소는 추가할 없습니다(null 제외)

 

Example

public class TypeTest {
    private static void addtoObjList(final List<Object> list, final Object o) {
        list.add(o);
    }

    private static void addToWildList(final List<?> list, final Object o) {
        // null 외에 허용되지 않는다
        list.add(o);
    }

    private static <T> void addToGenericList(final List<T> list, final T o) {
        list.add(o);
    }


    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        String s = "string";

        // 메서드에서 Error
        addToWildList(list, s);

        // List<Object> 이므로 incompatible types 오류
        addtoObjList(list, s);
        
        // 가능
        addToGenericList(list, s);
    }
}

 

 

로 타입을 사용할 수 있는 예외사항: instanceof

  • instanceof: 부모를 상속해서 만들어진 자식 객체가 여러 타입인 경우에 특정 클래스가 맞는지 확인하기 위한 메서드

 

ex 1) piece 객체가 Empty라는 클래스 타입인지를 확인하는 메서드

if (piece instanceof Empty) {
    return;
}

 

 

ex 2) o 타입이 set 클래스 타입인지 확인하는 메서드

// o의 타입이 Set인지 확인한 다음, 와일드카드 타입으로 형변환해야 합니다.
// 여기서 로 타입인 Set이 아닌 와일드카드 타입으로 변환함에 주의!
if( o instanceof Set) {
    Set<?> s = (Set<?>) o;
}

 

다형성 적용 vs instanceof

// 다형성
public abstract class Piece {
    public abstract int calculate(int point);
}

public class King extends Piece {
    public int calculate(int point) {
        return point + 10;
    }
}

public class Pawn extends Piece {
    public int calculate(int point) {
        return point + 1;
    }
}

public class Empty extends Piece {
    public int calculate(int point) {
        return point;
    }
}

public class Point {
  public int calculate(Piece p, int point) {
    return p.calculate(point);
  }
}

// instanceof
public class Point {
    public int calculate(Piece p, int point) {
        if(p instanceof King) {
            return point + 10;
        } else if(p instanceof Pawn) {
            return point + 1;
        } else if(p instanceof Empty) {
            return point;
        }
    }
}
  • instanceof 대신 다형성을 권장합니다.
    • instanceof 사용 시, 캡슐화를 깨뜨릴 뿐만 아니라, OCP, SRP 원칙에 위배됩니다.

 

[item-27] 비검사 경고를 제거하라

  • 비검사 경고 (unchecked warnings)를 제거하면 런타임에 형변환 관련 예외(ClassCastException)가 발생할 일이 없으며 코드의 올바른 동작도 기대할 수 있게 합니다.
  • @SuppressWarnings(”unchecked”) 어노테이션을 붙여 경고를 숨기는게 좋습니다.
  • @SuppressWarnings: 컴파일 경고를 사용하지 않도록 설정해 주는 어노테이션
    • unused, serial, all, deprecation, null, unchecked ..

 

[item-28] 배열보다는 리스트를 사용하라

 

배열 vs 제네릭

 

배열

  • 공변(covariant): Sub 클래스가 Super라는 클래스의 하위 타입이라면, 배열 Sub[]는 배열 Super[]의 상위 타입이 되는 것

제네릭

  • 불공변(invariant): 서로 다른 Type1, Type2가 있을 때, List<Type1>, List<Type2>는 서로 상, 하위 타입이 될 수 없는 것
Object[] array = new Long[1];
array[0] = "String"; // 컴파일은 되지만, 런타임 시에 오류가 발생한다.

List<Object> list = new ArrayList<Long>();
list.add("String"); // 컴파일조차 되지 않는다.

 

배열은 런타임 시점에도 자신이 담기로 한 원소의 타입인지를 확인합니다. 반면, 제네릭은 타입 정보가 런타임 시점에는 소거됩니다. 이를 통해 런타임 시점에 ClassCastException을 만나지 않고, 컴파일 시점에 오류를 잡을 수 있습니다.

 

배열에서는 위와 같은 실수를 런타임 시점 (애플리케이션 실행 시점)에 알 수 있지만, 리스트는 코드를 실행하기 전에 알 수 있습니다. 그래서 더욱 안전합니다.

 

 

[item-29] 이왕이면 제네릭 타입으로 만들라

  • 클라이언트에서 직접적으로 형변환을 해야 하는 타입보다는 제네릭 타입이 더 안전하고 사용 기하기에도 편리합니다.
  • 그러므로 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하는 것이 좋습니다.
  • 과정
    1. 클래스 선언에 타입 매개변수를 추가합니다. (class {name}<T>)
    2. 일반 타입을 타입 매개변수로 바꾸면 됩니다. (T {variable name})

 

클라이언트에서 형 변환을 해야 하는 경우

  • 타입 안정성이 없으며, 실수로 잘못된 형 변환 시 ClassCastException 예외 발생
class Box {
    
    private Object item;

    public void set(Object item) {
        this.item = item;
    }

    public Object get() {
        return item;
    }
}

Box box = new Box();
box.set("item");
String message = (String) box.get(); // 형변환 필요

 

제네릭을 활용한 개선

  • 컴파일 타임에 타입을 체크하고, 형변환이 필요 없을 뿐만 아니라 타입 안정성까지 확보할 수 있게 됩니다.
class Box<T> {
    private T item;

    public void set(T item) {
        this.item = item;
    }

    public T get() {
        return item;
    }
}

Box<String> box = new Box<>();
box.set("item");
String message = box.get(); // 형변환 불필요

 

[item-30] 이왕이면 제네릭 메서드로 만들라

 

ex) 타입 매개변수 목록 <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;
}

 

  • 위 코드는 경고 없이 컴파일되며, 타입 안전하며 쓰기도 쉽습니다.
  • 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로는 매개변수화 할 수 있습니다.
  • 이를 통해 불변 객체를 여러 타입으로 만들 수 있습니다.

 

재귀적 타입한정 적용

puiblic static <E extends Comparable<E>> E max(Collection<E> c);

 

  • E로 받을 타입은 오직 Comparable <E>를 구현한 타입만 가능하다는 뜻입니다.
  • 즉, Comparable을 구현한 타입만 가능하다는 뜻입니다.

 

[item-31] 한정적 와일드카드를 사용해 API의 유연성을 높여라

  • 제네릭은 불공변이기 때문에 하위 타입 객체를 추가하는 경우에 문제가 발생하기 쉽습니다.
  • 이러한 경우에는 매개변수에 한정적 와일드 카드를 적용함으로써 하위 객체 또는 상위 객체까지 연산을 적용할 수 있습니다.
  • 반환 값에 한정적 와일드카드를 적용하는 것은 오히려 유연성을 깨뜨립니다.
Iterable<? extends E> src;        // E의 하위 타입
Iterable<? super E> src;        // E의 상위 타입

 

 

PECS (Producer-Extends-Consumer-Super)

  • 매개변수 타입이 생산자를 나타내면 <? extends T>를 사용합니다.
  • 매개변수 타입이 소비자를 나타내면 <? super T>를 사용합니다.

 

<? extends T> (stack: push)

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

 

<? super T> (stack: pop)

public void popAll(Collection<? super E> dst) {
    while(!isEmpty()) {
        dst.add(pop());
    }
}

 

Advanced

public class Union {
    public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
        Set<E> result = new HashSet<>(s1);
        result.addAll(s2);
        return result;
    }

    public static void main(String[] args) {
        // Set.of 메서드는 java 9 이상부터 지원
        Set<Double> doubleSet = Set.of(1.0, 2.1);
        Set<Integer> integerSet = Set.of(1, 2);
        Set<Number> unionSet = union(doubleSet, integerSet);
    }
}

 

  • 재귀적 한정 타입을 적용한 부분 Comparable은 E 인스턴스를 소비하는 소비자이므로 super가 적용해야 합니다.
// 변경 전
public static <E extends Comparable<E>> E max(Collection<E> collection)

// 변경 후(PECS 공식 2번 적용)
public static <E extends Comparable<? super E>> E max(Collection<? extends E> collection)

 

타입 매개변수가 한 번만 나오는 경우

  • 와일드카드로 대체하는 것이 좋습니다.
public static <E> void swap(List<E> list, int i, int j); (X)
public static void swap(List<?> list, int i, int j); (O)

 

 

[item-32] 제네릭과 가변인수를 함께 쓸 때는 신중하라

  • 가변인수 메서드를 호출하면, 가변인수를 담기 위한 배열이 자동으로 생성
  • 가변인수(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
    }

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("Hi there");
        dangerous(stringList);
    }

 

제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드 대표적 예시

  • Arrays.list(T… a), EnumSet.of(E first, E… set)
@SafeVarargs // 제네릭 가변인수 관련 컴파일 경고를 숨기기 위한 어노테이션
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

@SafeVarargs
public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) {
    EnumSet<E> result = noneOf(first.getDeclaringClass());
    result.add(first);
    for (E e : rest)
        result.add(e);
    return result;
}