언어/Effective Java

이펙티브 자바 Item 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라

now0204 2024. 6. 5. 10:28

 

가변인수 ...은 메서드를 사용하는 클라이언트에서 파라미터를 몇개 보낼지 선택하는 것 

아래와 같이 가변인수와 제네릭을 함께 사용하면 타입 안정성 문제가 발생한다.

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
    }

 

  • 제네릭은 성격상 배열과 궁합이 별로다.
  • 제네릭 타입의 배열을 정의한는 걸 컴파일러가 막아준다.
  • 그러나 가변인자는 내부적으로 제네릭의 배열이 만들어지는 경우가있다.
  • 가변인자와 제네릭을 같이 사용하면 제네릭 타입의 배열이라고 부른다.
  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
 }

 

  • List의 배열을 Object에 할당한다. 배열은 공변이므로, List의 배열을 Object에 할당할 수 있다.
  • objects[0] = intList처럼 Object배열에 List<Integer>의 원소를 할당할 수 있다.
  • stringLists[0].get(0)코드 실행시 에러가 발생하는데, 해당 코드는 컴파일 시 String 캐스팅 코드를 넣어주어야한다. 
  • stringLists[0]은 List<String>타입이기 때문이다. 
  • 이와 같은 경우 런타임에 타입 안정성이 깨지게된다. 따라서 제네릭을 사용한 가변인자를 권하지 않는다.
 @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;
    }

 

  • @SageVarargs는 가변인자를 안전하게 사용하고 있다는 뜻이다. 

 


 

가변인자를 안전하게 사용하는 방법 

 

1. 제네릭 가변인자에 아무것도 넣지 않는다.

 @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;
    }
  • 파라미터를 가공하지않고, 다시 제네릭 타입의 list에 넣고 있다. 아주 안전하다. 

2. 제네릭 가변인자를 절대로 밖으로 노출하지 않는다.

static <T> T[] toArray(T... args) {
        return args;
    }
  • 이런거 하지 말라는 것!! 
  • 제네릭 배열을 그대로 리턴하는데, 타입을 Object로 판단한다. 
  • 타입이 안정적이지 않을 수 있다! 이와 같은 상황에서는 @SafeVarargs를 사용하지마라 
@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;
    }
    
    위 코드도 아래와 같이 수정하여, 타입 안정성을 노리자 
    
    public class FlattenWithList {
    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;
    }
  • flatten은 값을 받아 어딘가에 넣어주는 프로듀서이기 때문에 <? extends T>를 사용한다.
    • T와 T하위타입은 T타입에 들어갈 수 있다! 
  • 가변인자 대신 List를 사용하는것이 코드가 약간 복잡해질 수 있다는 단점이 있지만, 장점이 더 많다! 
  • 가변인자 대신 List 혹은 List<List<? extendsT>>를 사용하는 것이 더 좋은 방법이다.

참고자료 

https://javabom.tistory.com/88

 

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

1. 가변인수와 제네릭을 함께 사용할 때의 헛점 가변인수 메서드를 호출하면 가변인수를 담기위한 배열이 자동으로 하나 만들어진다. 내부로 감춰야했을 배열을 클라이언트에 노출해서 문제가

javabom.tistory.com

 

https://pro-dev.tistory.com/156

 

이펙티브 자바 아이템 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 - 핵심 정리

아이템 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 - 핵심 정리 이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다. 가변인수 ... 는 매서드를 사

pro-dev.tistory.com