-
[Effective Java] 로 타입은 사용하지 말라Java 2023. 2. 2. 11:04
제네릭
제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때 마다 형변환을 해야 했다.
반면, 제네릭을 사용하면 컬렉션이 담을 수 있는 타입을 컴파일러에게 알려주게 된다.
그래서 컴파일러는 알아서 형변환 코드를 추가할 수 있게 되고, 엉뚱한 타입의 객체를 넣으려는 시도를 컴파일 과정에서 차단하여 더 안전하고 명확한 프로그램을 만들어 준다.
제네릭 클래스, 제네릭 인터페이스
- 클래스와 인터페이스 선언에 타입 매개변수가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.
public class Box<T> { // T는 타입을 의미한다. private T data; public void set(T data) { this.data = data; } public T get() { return data; } }
- List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받는다.
- 그래서 이 인터페이스의 완전한 이름은 List< E>지만, 짧게 그냥 List라고도 자주 쓴다.
- 제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입(generic type)이라 한다.
- 각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다.
- 먼저 클래스(혹은 인터페이스) 이름이 나오고, 이어서 꺾쇠괄호 안에 실제 타입 매개변수들을 나열한다.
- List< String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다.
- 여기서 String이 정규타입 매개변수 E에 해당하는 실제 타입 매개변수다.
로타입
- 로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.
- List< E>의 로타입은 List다.
컬렉션 타입의 로 타입 - 따라하지 말 것
//Stamp 인스턴스만 취급한다. private final Collections stamps = ...;
이 코드를 사용하면 실수로 도장(Stamp) 대신 동전(Coin)을 넣어도 아무 오류없이 컴파일되고 실행된다.
//실수로 동전을 넣는다. stamps.add(New Coin(...));
컬렉션에서 이 동전을 다시 꺼내기 전에는 오류를 알아채지 못한다.
반복자의 로 타입 - 따라하지 말 것
for (Iterator i = stamps.iterator(); i.hasNext(); ){ Stamp stamp = (Stamp) i.next(); // ClassCastException을 던진다. }
- 오류는 가능한 한 발생 즉시, 이상적으로는 컴파일할 때 발견하는 것이 좋다.
- 위의 예에서는 오류가 발생하고 한참 뒤인 런타임에야 알아챌 수 있는데, 이렇게 되면 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 물리적으로 상당히 떨어져 있을 가능성이 커진다.
- ClassCastException이 발생하면 stamps에 동전을 넣은 지점을 찾기 위해 코드 전체를 훑어봐야 할 수도 있다.
제네릭을 활용하면 이 정보가 주석이 아닌 타입 선언 자체에 녹아든다.
매개변수화된 컬렉션 타입 - 타입 안정성 확보
private final Collection<Stamp> stamps = ...;
- 이렇게 선언하면 컴파일러는 stamps에는 Stamp의 인스턴스만 넣어야 함을 컴파일러가 인지하게 된다.
- 따라서 아무런 경고 없이 컴파일된다면 의도대로 동작할 것임을 보장한다.
- 이제 stamps에 엉뚱한 타입의 인스턴스를 넣으려 하면 컴파일 오류가 발생하며 무엇이 잘못됐는지를 정확히 알려준다.
Test.java:9: error: incompatible types: Coin cannot be converted to Stamp stamps.add(new Coin());
컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하여 절대 실패하지 않음을 보장한다.
로타입(타입 매개변수가 없는 제네릭 타입)을 쓰는 걸 언어 차원에서 막아 놓지는 않았지만 절대로 써서는 안 된다. 로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다.
쓰면 안되는 로 타입을 애초에 왜 만들었을까?
호환성 때문이다.
- 자바가 제네릭(자바 5부터 사용)을 받아들이기까지 거의 10년이 걸린 탓에 제네릭 없이 짠 코드가 이미 세상을 뒤덮어 버렸다.
- 기존 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와도 맞물려 돌아가게 해야만 했다.
- 로 타입을 사용하는 매개변수화 타입의 인스턴스를 넘겨도 동작해야만 했다.
List< Object> 처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다.
로 타입인 List와 매개변수화 타입인 List< Object>의 차이는 무엇일까?
- List는 제네릭 타입에서 완전히 발을 뺀 것이고, List< Object>는 모든 타입을 허용한다는 의사를 컴파일러에 명확히 전달한 것이다.
- 매개변수로 List를 받는 메서드에 List< String>을 넘길 수 있지만, List< Object>를 받는 메서드에는 넘길 수 없다.
- 이는 제네릭의 하위 타입 규칙 때문이다.
- 즉, List< String>은 로 타입인 List의 하위 타입이지만, List< Object>의 하위 타입은 아니다(아이템 28).
=> 그 결과, List< Object>같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입을 사용하면 타입 안정성을 잃게 된다.
unsafeAdd 메서드가 로 타입(List)를 사용 한 경우
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); } /* 경고 발생 Test.java.10: warning: [uncheked] call to add(E) as a member of the raw type List */
- 로 타입인 List를 사용하여 경고는 발생하지만, 컴파일은 성공한다.
- 하지만, 컴파일러의 경고를 무시하고 프로그램을 실행하면, strings.get(0)의 결과를 형변환 하려 할 때, Integer를 String으로 변환하려 시도하게 되고, ClassCastException을 던진다.
List< Object>로 변경한 경우
- 오류 메시지 출력되며 컴파일조차 되지 않는다.
Test.java:5 error: incompatible types: List< String> cannot be converted to List< Object>
2개의 집합(Set)을 받아 공통 원소를 반환하는 메서드를 작성할 경우
잘못된 예 - 모르는 타입의 원소도 받는 로 타입을 사용했다.
static int numElementsInCommon(Set s1, Set s2){ int result = 0; for(Object o1 : s1) if(s2.contaions(o1)) result++; return result; }
- 이 메서드는 동작은 하지만 로 타입을 사용해 안전하지 않다.
- 따라서 비한정적 와일드카드 타입을 대신 사용하는 게 좋다.
- 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표(?)를 사용하자.
- Set<?> 는 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입이다.
비한정적 와일드카드 타입 사용 - 타입 안전하며 유연하다.
static int numElementsInCommon(Set<?> s1, Set<?> s2){...}
비한정적 와일드카드 타입인 Set<?>와 로 타입인 Set의 차이는 무엇일까?
- 와일드카드 타입은 안전하고, 로 타입은 안전하지 않다.
- 로 타입 켤렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다.
반면, Collection<?>에는 (null외에는) 어떤 원소도 넣을 수 없다.
- 와일드카드는 정해지지 않은 unknown type이기 때문에 Collection<?>로 선언함으로써 모든 타입에 대해 호출이 가능해졌다.
- 여기서 중요한 것은 와일드카드가 any type이 아닌 unknown type이라는 점이다.
- 컬렉션의 add로 값을 추가하려면 제네릭 타입인 E 또는 E의 자식을 넣어줘야 한다.
- 와일드 카드의 경우 add로 넘겨주는 파라미터가 unknown 타입의 자식이여야 하는데, 정해지지 않았으므로 어떠한 타입을 대표하는지 알 수 없어서 자식 여부를 검사할 수 없는 것이다.
- 반면에 get으로 값을 꺼내는 작업은 와일드카드로 선언되어 있어도 문제가 없다.
- 왜냐하면 값을 꺼낸 결과가 unknown 타입이여도 우리는 해당 타입이 어떤 타입의 자식인지 확인이 필요하지 않으며, 심지어 적어도 Objet의 타입임을 보장할 수 있기 때문이다.
로 타입을 쓰지 말라는 규칙에도 예외가 몇 개 있다.
class 리터럴에는 로 타입을 써야 한다.
자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다.
- 허용: List.class, String[].class, int.class
- 허용 X: List< String>.class, List<?>.class
instanceof 연산자
- 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 의외의 매개변수화 타입에는 적용할 수 없다.
if (o instance of Set){ Set<?> s = (Set<?>) o; }
- o의 타입이 Set임을 확인한 다음 와일드 카드 타입인 Set<?>로 형변환해야 한다.
핵심 정리
로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안된다.
Set< Object>는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고, Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드 카드 타입이다.
Set< Object>와 Set<?>는 안전하지만, 로 타입인 Set은 안전하지 않다.
'Java' 카테고리의 다른 글
[Effective Java] int 상수 대신 열거 타입을 사용하라 (1) 2023.02.02 [Effective Java] 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) 2023.02.02 [Effective Java] 톱레벨 클래스는 한 파일에 하나만 담으라 (0) 2023.02.02 [Effective Java] 추상 클래스보다는 인터페이스를 우선하라 (0) 2023.02.02 [Effective Java] 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) 2023.02.02 - 클래스와 인터페이스 선언에 타입 매개변수가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.