티스토리 뷰

728x90
반응형

로 타입은 사용하지 말라


  • 로 타입(raw type)이란 제네릭에서 타입 매개변수를 전혀 사용하지 않은 경우를 말합니다.
  • 이러한 로 타입은 타입 매개변수가 없기 때문에 컴파일러에서 형변환 코드를 알아서 넣어주지 못하기 때문에 실수로 의도와 다른 타입의 객체를 넣어도 오류가 발생하지 않고 컴파일되고 실행이 됩니다.
private final List users = ...;

 

🧨 문제가 발생하는 코드

  • Foods 클래스의 print 메서드에서 iterator로 순회시 Food 클래스로 형변환시 예외가 발생하게 됩니다.
public class Foods {

    private final List foods = new ArrayList();

    public void add(Object o) {
        foods.add(o);
    }

    public void print() {
        Iterator iterator = foods.iterator();

        while (iterator.hasNext()) {
            Food food = (Food) iterator.next(); // ClassCastException 발생
            System.out.println("food : " + food);
        }
    }
}

public class Food {

    private final String name;

    public Food(String name) {
        this.name = name;
    }
}

public class Weapon {

    private final String name;

    public Weapon(String name) {
        this.name = name;
    }
}

public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
        Foods foods = new Foods();
        foods.add(new Food("햄버거"));
        foods.add(new Weapon("도끼"));
        foods.print();
    }
}

 

💡 제네릭을 사용하여 예외 예방

  • 제네릭을 사용하여 Food 객체만 받을 수 있고 기존의 Weapon 객체를 add하게 되면 컴파일에서 인지해서 에러라는 것을 표시해주게 됩니다. 즉, 로 타입을 쓰게 되면 제네릭의 안전성과 표현력을 모두 포기한다는 의미입니다.
public class Foods {

    private final List<Food> foods = new ArrayList();

    public void add(Food o) {
        foods.add(o);
    }

    public void print() {
        Iterator iterator = foods.iterator();

        while (iterator.hasNext()) {
            Food food = (Food) iterator.next();
            System.out.println("food : " + food);
        }
    }
}

 

💡 로 타입이 남아 있는 이유

  • 자바가 제네릭을 받아들이기까지 거의 10년이 걸린 탓에 제네릭 없이 짠 코드가 이미 세상을 뒤덮어 버렸습니다. 그래서 기존 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와도 맞물려 돌아가게 해야만 했습니다. 그래서 마이그레이션 호환성을 위해서 로 타입을 지원하고 제네릭 구현에는 소거 방식을 사용하기로 했습니다.

💡 List와 List<Object>

  • 전자와 같은 로 타입은 위에서도 계속 안된다고 했으나 List<Object>와 같이 임의 객체를 허용하는 매개변수화 타입은 괜찮다고 합니다. 왜 일까요? 로 타입과는 다르게 List<Object>는 컴파일러에게 모든 타입을 허용한다는 것을 명시적으로 전달하는 코드이기 때문입니다. 그리고 로 타입인 List를 매개변수로 받는 메서드는 List<String>과 같은 List도 전달하는데 문제가 없지만 List<Object>를 매개변수로 받는 메서드에서는 넘길 수 없습니다.
  • 그 이유는 제네릭의 하위 타입 규칙 때문인데, List<String>은 List의 하위 타입이지만 List<Object>의 하위 타입은 아니기 때문입니다. 그 결과 List와 같은 로 타입을 매개변수로 사용하면 타입 안정성을 잃게 됩니다.

🧨 문제가 발생하는 경우(List)

  • 아래 코드를 실행하면 ClassCastException이 발생하게 됩니다. 그 이유는 static add 메서드에서는 전달받은 매개변수 list에 add할 때 타입 구분없이 추가하게 되지만 list.get(1)하는 경우 컴파일러는 자동으로 타입 매개변수로 선언된 String으로 형변환을 시도하게 되고 123은 Integer 타입이기 때문에 형변환에 예외가 발생하는 것입니다.
public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
       List<String> list = new ArrayList<>();
       add(list, "hello");
       add(list, 123);
       String s = list.get(1);
    }

    public static void add(List list, Object o) {
        list.add(o);
    }
}

 

🧨 문제가 발생하는 경우(List<Object>)

  • 아래처럼 변경을 하면 컴파일에러가 발생하게 됩니다.
  • java: incompatible types: java.util.List<java.lang.String> cannot be converted to java.util.List<java.lang.Object>
public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
       List<String> list = new ArrayList<>();
       add(list, "hello"); // 컴파일 에러 발생
       add(list, 123);     // 컴파일 에러 발생
       String s = list.get(1);
    }

    public static void add(List<Object> list, Object o) {
        list.add(o);
    }
}

 

💡문제 해결 방법(와일드 카드)

  • 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않은 경우에 로 타입이 아닌 ?를 사용하면 어떤 타입도 담을 수 있는 범용적인 매개변수화 타입이 됩니다.
public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
         HashSet<Integer> s1 = new HashSet<>(){{
             add(1);
             add(2);
             add(3);
         }};

        HashSet<Integer> s2 = new HashSet<>(){{
            add(1);
            add(4);
            add(5);
            add(6);
        }};

        long count = numElementInCommon(s1, s2);
        System.out.println(count); // 1
    }

    public static long numElementInCommon(Set<?> s1, Set<?> s2) {
        return s1.stream()
                .filter(obj -> s2.contains(obj))
                .count();
    }
}

 

💡비한정적 와일드카드 타입은 로 타입에 비해 안전합니다.

  • 아무 원소나 넣을 수 있어 타입 불변식을 훼손할 수 있는 로 타입에 비교해서 비한정적 와일드카드 타입에는 null외의 어떤 원소도 넣을 수 없습니다. 그래서 컬렉션의 타입 불변식을 훼손하지 못하게 막았습니다.

 

💡로 타입을 사용해야 하는 경우

  • 로 타입을 사용해야 하는 경우는 바로 class 리터럴에는 로 타입을 사용해야 하는데, 자바 명세에는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했습니다. (배열과 기본타입은 허용)
  • 허용되는 경우
    • List.class
    • String[].class
    • int.class
  • 허용이 안되는 경우
    • List<String>.class
    • List<?>.class
  • 또 다른 경우는 instanceof 연산자를 사용한 경우 런타임시 제네릭 타입 정보는 지워지기 때문에 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수 타입에는 적용이 불가능합니다. 또한 로 타입이나 비한정적 와일드카드 타입이나 instanceof는 동일하게 동작합니다. 그렇기 때문에 불필요한 코드 작성을 <?>로 하지 않고 로 타입으로 하는게 낫습니다.
  • instanceof에서는 로 타입을 사용해서 Set인지 확인을 했다면 내부 코드에서는 Set<?>으로 형변환을 해주는게 좋습니다.
if(o instanceof Set) {
    Set<?> s = (Set<?>) o;
}

 

 

참고 자료)

https://catsbi.oopy.io/83635cfe-1cab-43f2-a943-56a9efd83fb2

 

 

 

 

728x90
반응형