Effective Java를 읽고 정리한 정리본입니다.

📌 Item 26 : 로 타입은 사용하지 말라

🫧 제네릭 (Generics)

: 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법

제네릭 클래스와 제네릭 인터페이스를 통틀어 지칭하는 말

여기서 제네릭 클래스 혹은 제네릭 인터페이스란 클래스와 인터페이스 선언에 타입 매개변수가 쓰이는 클래스 혹은 인터페이스를 지칭한다.

예를 들면 List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받는다. (List)

class FruitBox<T> {
    List<T> fruits = new ArrayList<>();

    public void add(T fruit) {
        fruits.add(fruit);
    }
}

이 예시 코드에서는 FruitBox 클래스명 옆에 기호로 제네릭을 붙여준 것을 알 수 있다.

여기서 의문이 생길 수 있다. 아까 전 설명에는 List 인터페이스가 타입 매개변수 E를 받는다고 하였는데, 예시에서는 T로 표현하였다.

이는 타입 파라미터 기호 네이밍에 따라 달라지는 것으로, 정해진 식별자 기호가 있는 것은 아니나, 관습적으로 기호를 정해 사용한다.

✨ 타입 파라미터 기호 네이밍

타입 설명
<T> 타입(Type)
<E> 요소(Element), 예를 들어 List
<K> 키(Key), 예를 들어 Map<K, V>
<V> 리턴 값 또는 매핑된 값(Variable)
<N> 숫자(Number)
<S, U, V> 2번째, 3번째, 4번째에 선언된 타입

🫧 매개변수화 타입

각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다.

List<String> lst = new ArrayList<String>();

아까 전 타입 파라미터 기호를 사용해 제네릭을 정의했다면, 이제는 이것을 실체화할 차례이다.

예시와 같이 List은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다.

여기서 String이 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수다.

🫧 로 타입 (raw type)

: 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 의미한다.

제네릭 타입을 하나 정의하면 그에 딸린 로 타입도 함께 정의된다. 제네릭 지원 전 만들어진 코드들은 모두 로 타입으로 작성되었기 때문에 호환성을 위해 로 타입이 여전히 존재한다고 할 수 있다.

ex) List의 로 타입은 List

여기서 문제가 발생한다. 로 타입은 제네릭의 이점을 전혀 살리지 못한다.

제네릭의 의미 자체가 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법인데, 데이터 타입 자체를 빼 버렸으니 다양한 문제가 발생하는 것이다.

제네릭으로 사용했을 시 컴파일러에서 오류를 잡는 반면, 로 타입으로 사용할 시 런타임 시점에 오류가 터지므로 로 타입은 사용해서는 안 된다.

✨ 로 타입과 Object 매개변수화 타입

List와 같은 로 타입은 사용해서는 안 되나, List처럼 임의의 객체를 허용하는 매개변수화 타입은 괜찮다.

둘이 비슷하다고 느낄 수 있으나, 엄연히 다르다. List는 제네릭 타입에서 완전히 발을 뺀 것이고, List는 모든 타입을 허용한다는 의미이다.

예를 들면 다음과 같다.

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

🫧 비한정적 와일드카드 타입

: 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않은 경우 사용

  • <?>를 사용해서 나타낸다.
    -> ex) Set의 비한정적 와일드카드 타입은 Set<?>이다.

그렇다면 로 타입과 비한정적 와일드카드 타입의 차이는 무엇일까?

✨ 로 타입과 비한정적 와일드카드 타입

로 타입 컬렉션에는 아무 원소나 넣을 수 있으므로 타입 불변식을 훼손하기 쉬운 방면, 비한정적 와일드카드 타입은 null 외 어떤 원소도 넣을 수 없다.

🫧 로 타입이 사용되는 예외 상황

그렇다면 이러한 로 타입은 기존 코드와의 호환성을 위해 남겨두는 것 외에는 전부 사용하지 않아야 할까?

답은 아니다.

로 타입이 불완전하고 위험한 것은 맞으나, 몇 가지 예외 상황에서는 로 타입을 사용하곤 한다.

다음은 로 타입이 사용되는 예외 상황이다.

  1. class 리터럴
  2. instanceof 연산자

✨ 클래스 리터럴

자바 명세에서는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다.

List.class, String[].class, int.class (O)
List.class, List<?>.class (X)

✨ instanceof 연산

런타임 시 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.

이것은 제네릭 구현 시 사용하는 “소거 방식” 중 하나로, 해당 내용은 뒤에 자세하게 설명되어 있다.

소거 방식 사용에 따라 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작하므로, 아무런 역할 없이 지저분한 코드를 추가할 바에는 로 타입을 쓰는 편이 깔끔하다.

🫧 참고 자료

자바 제네릭(Generics) 개념 & 문법 정복하기_Inpa Dev 👨‍💻:티스토리