본문 바로가기
Dev Books/Effective Java

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

by Thumper 2024. 5. 31.

로 타입은 사용하지 말라

용어정리

  • 제네릭 클래스 혹은 제네릭 인터페이스
    • 클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰인다.
    • Example.class
  • 제네릭 타입(generic type)
    • 제네릭 클래스와 제네릭 인터페이스를 통틀어 이르는 말.
    • Example
  • 매개변수화 타입(parameterized type)
    • 각각의 제네릭 타입은 매개변수화 타입을 정의한다.
    • Example<String>
  • 타입 매개변수(Type parameter)
    • 제네릭 선언에 사용된 매개변수를 말한다.
    • <T>
  • formal 타입
    • 특수문자나 키워드를 제외하고는 문자 형식에는 크게 상관없으며 대부분 한 글자로 대문자로 표현한다.
    • 콤마를 이용하여 여러 개를 선언할 수 있다.
    • T,E 같은 문자들을 Formal Type Parameter라고 부른다.
    • Example<E>
  • actual 타입
    • 실제 타입 매개변수(타입 매개변수)
    • Example<String>
  • 로 타입(raw type)
    • 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않았을 때를 말한다.
    • 로타입은 제네릭 전후 코드의 호환을 위한 것이며, 동작하지만 좋은 예는 아니다.
    • List

로 타입이란?

일단 이 책 전반에서 즐기차게 이야기하듯, 오류는 가능한 컴파일할 때 발견하는 것이 좋다.

로 타입 단점: 런타임 에러 시점에서 실수했다는 걸 발견할 수 있다.

Raw Type을 사용하면 컴파일은 되지만, 실행하면 ClassCastException 발생한다.

image



아래 예제를 보자,
로 타입의 사용으로 인해 컴파일 타임에는 문제가 없으나, 실제 런타임에는 ClassCastException이 터진다.

Integer값을 String값으로 캐스팅하려 했기 때문이다.

package effectivejava.chapter5.item26;
import java.util.*;

public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0); // 컴파일러가 자동으로 형변환 코드를 넣어준다.
    }

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

로 타입 장점: 제네릭을 활용하면 이 정보가 주석이 아닌 타입 선언 자체에 녹아든다.

컴파일러가 타입선언에 대해 인지하고 있기 때문에, 아무런 경고 없이 컴파일되면 의도대로 동작할 것임을 보장한다.

public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings, "42");   // 컴파일 타임에 에러를 체크해주어서 바꿀 수 있다.
        String s = strings.get(0);
    }

    private static <T> void unsafeAdd(List<T> list, T o) { // 제네릭 선언
        list.add(o);
    }
}

로 타입을 사용하지 말자.

제네릭이 안겨주는 안정성과 표현력을 모두 잃게된다.

로 타입이 만들어진 이유

  • 제네릭이 없던 이전에서, 기존 코드와 제네릭을 사용하는 새로운 코드와 맛물려 돌아가게 해야만 했다.
  • 호환성을 위해 로 타입을 지원하고 제네릭 구현에는 소거(erasure, ITEM 28)방식을 사용하기로 했다.

로 타입인 List와 매개변수화 타입인 List<Object>의 차이점

  • 로 타입 List
    • 제네릭 타입에서 완전히 발을 뺀 것이다.
  • 매개변수화 타입 List<Object>
    • 모든 타입을 허용한다는 의사를 컴파일러에 명확히 전달한 것이다.

예시 코드와 함께 확인해보자.

image



image

매개변수로 List를 받는 메서드에 List을 넘길 수 있지만, List

를 받는 메서드에는 넘길 수 없다.
제네릭의 하위 타입 규칙 때문이다.


List은 Raw Type인 List의 하위 타입이지만 List의 하위 타입은 아니다.
위의 예와 같이 List 같은 Raw Type을 사용하면 타입 안정성을 잃게 된다.


로 타입 예시 (잘못된 사용 예시)

image

 

  • 모르는 타입도 받을 수 있는 Raw Type을 사용한 예시이다.
  • 동작은 하지만 안전하지 않다.

비한정적 와일드카드 타입

위 코드처럼 raw 타입을 사용하면 안전하지 않다.

제네릭을 사용하고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않을 때 ?(물음표)를 사용하자.
제네릭 타입인 Set<E>의 비한정적 와일드카드 타입은 Set<?>다.
이것이 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입이다.

예시: Set<?>

image

 

  • 는 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입이다.
  • ?를 사용하면 타입이 안전하고 유연하다.

Set<?>와 Set 차이

  • 와일드카드 타입은 안전하다.
  • 로 타입은 안전하지 않다.
  • 로 타입 컬렉션에는 아무 원소나 넣을 수 있기에 타입 불변식을 훼손하기 쉽다.
  • Set<?>에는 null 외에는 어떤 원소도 넣을 수 없다.

아래 예시코드와 함께 보자.

image




Collection<?>에는 null외에는 넣을 수 없다. 다른 원소를 넣으려 하면, 컴파일할 때 다음의 오류 메시지를 보게 된다.

즉, 컬렉션의 타입 불변식을 훼손하지 못하게 막았다. 이러한 제약을 받아들일 수 없다면 제네릭 메서드(ITEM 30)나 한정적 와일드카드 타입(ITEM 31)을 사용하면 된다.

로 타입의 규칙예외

class 리터럴에는 로 타입을 써야 한다.

class 리터럴에 매개변수화 타입을 사용하지 못하게 했다. (배열과 기본 타입은 허용한다.)

  • 허용 : List.class, String[].class, int.class
  • 불가 : List.class, List<?>.class

Instanceof 연산자는 로타입을 사용한다.

  • 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.
  • 로 타입이든 비한정 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다.
  • 비한정적 와일드카드 타입의 꺽쇠괄호와 물음표는 아무런 역할 없이 코드만 지저분하게 만드므로, 차라리 로 타입을 쓰는 편이 깔끔하다.

로 타입을 써도 좋은 예시 코드를 살펴보자.

image


o의 타입이 Set 임을 확인한 다음, 와일드카드 타입인 Set<?>로 형 변환해야 한다.
이는 형변환(checked cast)이므로 컴파일러 경고가 뜨지 않는다.

용어정리

4장 전반에 사용할 용어도 미리 소개한다. (책 참고하기)

image

✅ 핵심 정리

먼저 핵심 포인트를 집고 내용을 정리하고자 한다.

  • 로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 절대 사용하지 말자.
  • Set는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이다.
  • Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다.

댓글