개요
이번 아이템에서도 생소한 용어인 '로 타입'이 등장했다. 로 타입이 뭐길래 사용하지 말라는 걸까?
로 타입
제네릭 타입에서 타입 매개변수(괄호 < > 안에 들어가는 타입 값)를 사용하지 않았을 때의 타입을 말한다. List<E>의 로 타입은 List, ArrayList<E>의 로 타입은 ArrayList 이다.
List list = ... // 로 타입!
로 타입의 사용 예
아래는 로 타입을 사용하는 예이다. 컴파일 시 오류가 발생하지 않으나, 런타임 시 타입 오류가 발생한다. Coin을 Money로 캐스팅하려하니 에러가 나는 건 당연하지만 중요한건 이 에러가 컴파일에는 발생하지 않는다는 점이다. 이런 케스팅 에러는 런타임보다 컴파일 타임에 발견되는 것이 좋다.
List list = new ArrayList();
Coin coin = new Coin();
list.add(coin); // 실수로 Coin 인스턴스를 추가하였다.
Money getCoin = (Money) list.get(0); // 런타임 시 ClassCastException이 발생한다.
컴파일 타임에 발견되는 캐스팅 에러
타입 매개변수를 사용한다면 컴파일 타임에에 컴파일러가 오류를 캐치하게 된다. 그럼 변수 초기화 시 타입 매개변수를 사용하면 무조건 해당 변수는 타입 안전성을 갖게되는걸까? 그건 아니다.
List<Money> list = new ArrayList(); // 타입 매개변수 사용
Coin coin = new Coin();
list.add(coin); // 컴파일 에러 발생!
Money getCoin = list.get(0);
메서드 파라미터에 사용되는 로 타입
앞에서 제공한 코드 중간에 unsafeAdd 메서드가 추가되었고, 로 타입으로 list 값을 받고 있다. 이때 list.add(o) 부분에서 과연 컴파일 또는 런타임 에러가 발생할까?
List<Money> list = new ArrayList();
Coin coin = new Coin();
unsafeAdd(list, coin);
Money getCoin = list.get(0);
...
public static void unsafeAdd(List list, Object o){
list.add(o); // 예외가 발생할까요?
}
list 에 대한 타입 매개변수를 Money로 했으니 당연히 list.add(o) 부분에서 컴파일 예러가 발생한다고 생각할 수 있지만 그렇지 않다. 심지어 list.add(o) 시 런타임 예외도 발생하지 않는다. Coin 타입의 인스턴스가 Money 타입의 list에 잘 들어간다. 대신 list.get(0) 을 통해 값을 조회할 때 ClassCastException이 발생한다.
list 변수의 타입 안전성이 unsafeAdd 메서드에 추가한 '로 타입' 매개변수에 의해 파괴되는 순간이다.
로 타입을 사용하면 안되는 이유
💡 제네릭 타입이 제공하는 타입 안정성과 표현력을 굳이 버리는 꼴이다.
제네릭 타입에서 공식적으로 발을 뺀 타입이다.
로 타입 완전 별로인데 그냥 자바 진영에서 제명하면 안돼?
로 타입을 폐기할 수 없는 이유는 개발자들이 제네릭을 받아들이는데 오랜 시간이 걸렸고, 이미 '로 타입'으로 작성된 코드들이 너무 많았기 때문에, 그 코드와의 호환성을 위해 남겨두고 있는 것이라고 한다.
모든 타입을 허용하려면 로타입 말고 Object 타입으로 사용하자
모든 타입을 허용하는 변수를 정의할때는 정해진 타입이 없으니 로 타입으로 쓸 수 있지만, 제네릭 타입에서 발을 뺀 로 타입을 쓰는 것 자체가 모순이다. 이때는 로 타입 대신 Object 타입을 사용하자.
Coin coin = new Coin();
Money money = new Money();
List<Object> list = new ArrayList<>(); // 로 타입 대신 Object 타입
list.add(coin);
list.add(money);
Coin getCoin = (Coin) list.get(0);
Money getMoney = (Money) list.get(1);
메서드 파라미터는 타입 매개변수를 Object 타입으로 만들 수 없다.
모든 타입을 허용하는 메서드를 만들고, 여러 타입 매개변수를 가진 리스트에서 이를 재사용하도록 만들 수 있을까? 실제로 Object 타입을 가진 list를 매개변수로 받는 add 메서드를 구현했더니 컴파일 에러가 발생한다. 컴파일 에러가 발생하는 이유는 List<Object> 타입과 List<Coin> 타입이 다르기 때문이다.
List<Money> list = new ArrayList<>();
Coin coin = new Coin();
objectAdd(list, coin); // 컴파일 에러
Coin getCoin = (Coin) list.get(0); // 컴파일 에러
...
public static void objectAdd(List<Object> list, Object o){
list.add(o);
}
비한정 와일드카드 타입 활용
컴파일 에러를 해결하기 위해 비한정 와일드카드 타입을 쓸 수도 있다. 하지만 와일드 카드를 사용할 경우 타입 안전성을 지키기 위해 null 외에 아무 값도 넣지 못하게 된다. 즉, 타입 안전성을 훼손하는 모든 작업은 할 수 없는 것이다. 단, get과 같은 작업은 타입 안전성을 훼손하지 않으므로 가능하다.
List<Money> list = new ArrayList<>();
Coin coin = new Coin();
objectAdd(list, coin);
...
public static void objectAdd(List<?> list, Object o){
list.add(o); // 컴파일 에러
list.get(0); // 컴파일 에러는 발생하지 않음.
}
만약 모든 타입에 대해 타입 안전성을 훼손하지 않는 비지니스 로직을 처리해야할 경우 비한정 와일드카드 타입을 활용하겠지만, 그게 아니라면 굳이 모든 타입을 허용하는 메서드를 만들 필요는 없다고 생각한다.
로 타입을 사용하는 예외 상황
로 타입을 사용해야 하는 상황도 있다.
1. class 리터럴
class 타입이 들어가야하는 자바 문법에 List.class 와 같은 로 타입은 허용하지만, List<String>.class와 같은 매개변수화 타입은 허용하지 않는다.
2. instanceof 연산자
instanceof 연산자는 비한정적 와일드카드 타입과 로 타입 말고는 적용할 수 없다.
정리
로 타입을 사용하면 런 타임에 예외가 일어날 수 있으니 사용하면 안된다. 로 타입은 제네릭 도입에 따른 호환성을 위해 제공될 뿐이다.
'공부 > Effective Java' 카테고리의 다른 글
[Effective Java] Item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 / feat. 매개변수화 타입 (1) | 2023.11.02 |
---|---|
[Effective Java] Item 27. 비검사 경고를 제거하라 (1) | 2023.10.11 |
[Effective Java] Item 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라 (0) | 2023.10.03 |
[Effective Java] Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라. (0) | 2023.10.02 |
[Effective Java] Item 15. 클래스와 멤버의 접근 권한을 최소화하라 / 정보은닉 (0) | 2023.09.20 |