티스토리 뷰
(Effective Java) 규칙26. 로 타입은 사용하지 말라
제네릭 타입
-
클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰일때 이것을 제네릭 클래스, 제네릭 인터페이스라고 한다.
- 제네릭 클래스, 인터페이스를 통틀어 제네릭 타입이라 함
-
List
인터페이스는 원소의 타입을 나타내는 타입 매개변수E
를 받음- 이 인터페이스는
List<E>
지만, 짧게List
라고 자주 씀
- 이 인터페이스는
제네릭 타입의 특징
-
각각의 제네릭 타입은 일련의 매개변수화 타입 (parameterized type)을 정의 한다.
- 먼저 클래스(혹은 인터페이스) 이름이 나오고, 이어서 꺽쇠괄호 안에 실제 타입 매개변수들을 나열함
List<String>
은 원소의 타입이String
인 리스트를 뜻하는 매개변수화 타입임String
이 정규(formal) 타입 매개변수E
에 해당하는 실제 (actual) 타입 매개변수임
-
제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다.
- 로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말함
List<String>
의 로 타입은List
임- 로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 적용되기 전 코드와 호환하기 위한 궁여지책이라 할 수 있음
제네릭이 도입 되기 전
-
제네릭은 지원하기 전에는 컬렉션을 다음과 같이 선언했다. (자바 9에서도 여전히 동작하지만 좋은 예라고 볼 수 없음)
//Stamp 인스턴스만 취급한다. private final Collection stamps = ...;
-
위 코드를 사용하면 실수로 도장(
Stamp
) 대신 동전(Coin
)을 넣어도 아무 오류 없이 컴파일되고 실행된다.//실수로 동전을 넣는다. stamps.add(new Coin(...)); // "unchecked call" 경고를 내뱉는다.
- 컬렉션에서 이 동전을 다시 꺼내기 전에는 오류를 알아채지 못함
for(Iterator i = stamps.iterator(); i.hasNext();) { Stamp stamp = (Stamp) i.next(); // ClassCastException을 던진다. stamp.cancel(); }
-
이렇게 되면 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 상당히 떨어져 있을 가능성이 커진다.
ClassCastException
이 발생하면stamps
에 동전을 넣은 지점을 찾기 위해 코드 전체를 훑어봐야 할 수도 있음- 오류는 가능한 한 발생 즉시, 이상적으로는 컴파일할 때 발견하는 게 좋음
제네릭이 도입된 후
-
제네릭을 활용하면 이 정보가 주석이 아닌 타입 선언 자체에 녹아든다.
private final Collection<Stamp> stamps = ...;
- 이렇게 선언하면
stamps
에는Stamps
의 인스턴스만 넣어야 함을 컴파일러가 인지하게 됨
- 이렇게 선언하면
-
stamps
에 엉뚱한 타입의 인스턴스를 넣으려 하면 컴파일 오류가 발생하며 무엇이 잘못됐는지를 정확히 알려준다.Test.java:9: error: imcompatible types: Coin cannot be converted to Stamp stamps.add(new Coin()); ^
-
컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하여 절대 실패하지 않음을 보장한다.
로 타입을 사용하는 이유
-
로 타입을 만들어놓은 이유는 호환성 때문이다.
- 자바가 제네릭을 받아들이기까지 거의 10년이 걸린 탓에 제네릭 없이 짠 코드가 이미 세상을 뒤덮어 버림
- 기존 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와도 맞물려 돌아가게 해야만 했음
- 마이그레이션 호환성을 위해서 로 타입을 지원하고 제네릭 구현하는 소거(enasure; 규칙 28) 방식을 사용하기로 함
-
List
같은 로 타입은 사용해서는 안되나List<Object>
처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다.List
는 제네릭 타입에서 완전히 발을 뺀 것이고,List<Object>
는 모든 타입을 허용한다는 의사를 컴파일러에 전달한 것
-
List
를 받는 메서드에List<String>
은 넘길 수 있지만,List<Object>
를 받는 메서드에는 넘길 수 없다.- 제네릭의 하위 타입 규칙 때문으로
List<String>
은List
의 하위 타입이지만,List<Object>
의 하위 타입은 아님 List<Object>
같은 매개변수화 타입을 사용할 때와 달리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); }
- 이 코드는 컴파일되지만 로 타입인
List
를 사용하여 다음과 같은 경고가 발생함
Test.java:10: warning: [unchecked] unchecked call to add(E) as a member of the raw type List list.add(o); ^
- 이 프로그램을 이대로 실행하면
strings.get(0)
의 결과를 형변환하려 할 때ClassCastException
을 던짐Integer
를String
으로 변환하려 시도한 것임- 이 경우는 컴파일러의 경고를 무시하여 대가를 치른 것임
- 제네릭의 하위 타입 규칙 때문으로
매개변수화 타입 사용
-
이제 로 타입인
List
를 매개변수화 타입인List<Object>
로 바꾼다음 다시 컴파일 해보면 오류 메시지가 발생하면서 컴파일 조차 되지 않는다.Test.java:5: error: incompatible types: List<String> cannot be converted to List<Object> unsafeAdd(strings, Integer.valueOf(42)); ^
-
원소의 타입을 몰라도 되는 로 타입을 쓰고 싶어질 수 있다.
//잘못된 예 - 모르는 타입의 원소도 받는 로 타입을 사용함 static int numElementsInCommon(Set s1, Set s2) { int result = 0; for (Object o1 : s1) if (s2.contains(o1)) result++; return result; }
비한정적 와일드카드 타입
-
위 메서드는 동작은 하지만 로 타입을 사용해 안전하지 않음
- 따라서 비한정 와일드카드 타입(unbounded wildcard type)을 대신 사용하는 게 좋음
- 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표(?)를 사용할 것
- 제네릭 타입인
Set<E>
의 비한정적 와일드카드 타입은Set<?>
임 (어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화Set
타입)
-
비한정적 와일드카드 타입을 사용해
numElementsInCommon
을 다시 선언한 모습이다.static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }
-
비한정적 와일드카드 타입인
Set<?>
는 안전하고 로 타입인Set
은 안전하지 않다.- 로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉬움
- 반면,
Collection<?>
에는 (null
외에는) 어떤 원소도 넣을 수 없음//다른 원소를 넣으려하면 컴파일시 다음과 같은 오류 메시지를 보게 될 것이다. WildCard.java.13: error: incompatible types: String cannot be converted to CAP#1 c.add("verboten"); ^ where CAP#1 is a fresh type-variable: CAP#1 extends Object from capture of ?
- 어떤 원소도
Collection<?>
에 넣지 못하게 했으며 컬렉션에서 꺼낼 수 있는 객체의 타입도 전혀 알 수 없게함 - 이러한 제약을 받아들일 수 없다면 제네릭 메서드(규칙 30)나 한정적 와일드카드 타입(규칙31)을 사용할 것
- 어떤 원소도
로 타입을 쓰지 말하는 규칙의 예외
class 리터럴에는 로 타입을 써야 한다
-
자바 명세는
class
리터럴에 매개변수화 타입을 사용하지 못하게 했다. (배열과 기본 타입은 허용) -
예를 들어
List.class
,String[].class
,int.class
는 허용하고List<String>.class
와List<?>.class
는 허용하지 않는다.
instanceOf 연산자
-
런타임에는 제네릭 타입 정보가 지워지므로
instanceof
연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다. -
로 타입이든 비한정적 와일드카드 타입이든
instanceof
는 완전히 똑같이 동작한다. -
비한정적 와일드카드 타입의 꺽쇠괄호와 물음표는 아무런 역할 없이 코드만 지저분하게 만드므로 로 타입을 쓰는 편이 깔끔하다.
-
제네릭 타입에
instanceof
를 사용하는 올바른 예if (o instanceof Set) { //로 타입 Set<?> s = (Set<?>) o; ... }
o
의 타입이Set
임을 확인한 다음 와일드카드 타입인Set<?>
로 형변환해야 함- 이는 검사 형변환(checked cast)이므로 컴파일러 경고가 뜨지 않음
결론
-
로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안된다.
- 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐임
-
Set<Object>
는 어떤 타입도 저장할 수 있는 매개변수화 타입이고, Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다.- 그리고 이들의 로 타입인
Set
은 제네릭 타입 시스템에 속하지 않음 Set<Object>
와Set<?>
은 안전하지만, 로 타입인Set
은 안전하지 않음
- 그리고 이들의 로 타입인
참고
끝으로
이 글이 도움이 되었다면, 하단의 Google 광고 👎👎👎 한번씩 클릭 부탁 드립니다. 🙏🙏🙏
광고 클릭은 많은 힘이 됩니다!
'프로그래밍 > EffectiveJava' 카테고리의 다른 글
(이펙티브 자바) 규칙24. 멤버 클래스는 되도록 static으로 만들라 (0) | 2020.03.17 |
---|---|
(이펙티브 자바) 규칙25. 톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2020.03.15 |
(이펙티브 자바) 규칙63. 문자열 연결은 느리니 주의하라 (0) | 2020.03.12 |
(이펙티브 자바) 규칙64. 객체는 인터페이스를 사용해 참조하라 (0) | 2020.03.11 |
(이펙티브 자바) 규칙26-1. 제네릭 관련 용어 정리 (0) | 2020.03.10 |
- Total
- Today
- Yesterday
- java
- 텐트
- 배낭여행
- windows
- JavaFX 종료
- JavaFX
- effective java
- effectivejava
- Java UI
- 스프링부트
- 방통대 과제물
- JavaFX 테이블뷰
- 자전거 여행
- 일본 배낭여행
- 배낭 여행
- 자바
- 자전거
- 인텔리제이
- TableView
- 이펙티브
- springboot
- 일본 여행
- git
- 일본여행
- JavaFX Window Close
- 일본 자전거 여행
- 이펙티브 자바
- JavaFX Table View
- intelij
- 이펙티브자바
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |