26. 로 타입은 사용하지 말라
5장 들어가기 전에,
제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때마다 형변환을 해야 했다.
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
int get = (Integer) list.get(0);
String str = (String) list.get(0);
}
*런타임 예외 발생
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
위와 같은 코드는 런타임 예외가 발생한다. 런타임 에러는 프로그램 실행 중 발생하기 때문에 크리티컬한 이슈를 발생시킬 수 있을 뿐만 아니라 미리 예방을 못한다. 제일 좋은 에러는 컴파일 에러이다. 하지만 위 코드는 컴파일은 무사 통과하지만 런타임 시에 에러가 발생한다.
하지만, generic을 사용하면
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
int get = list.getFirst();
String str = (String) list.getFirst();
}
*컴파일 에러 발생
Inconvertible types; cannot cast 'java. lang. Integer' to 'java. lang. String'
컬렉션 뿐만 아니라 generic을 모두 사용할 수 있지만, 코드가 복잡해지는 단점이 존재한다.
-----------------------------------------------------
Item 26. 로 타입은 사용하지 말라.
용어 정리
타입 매개변수가 쓰인 클래스 : 제네릭 클래스
타입 매개변수가 쓰인 인터페이스 : 제네릭 인터페이스
타입 매개변수 : <E>
제네릭 클래스 + 제네릭 인터페이스 = 제네릭 타입
각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다.
매개변수화 타입 : List<String>
타입 매개변수 : String
로타입 : List
로타입을 사용하면, 런타임에야 문제를 알아차릴 수 있을 뿐더러 문제를 겪는 코드와 원인을 제공한 코드가 상당히 멀리 떨어져 있을 가능성이 크다. => 디버깅이 어렵다.
주석을 써놓아도 주석은 컴파일러가 이해하지 못하니 별 도움이 되지 못한다.
개발자가 타입을 명시하는 법.
1. 주석
2. 제네릭
주석은 개발자가 다른 개발자에게 알려주는 것이고
제네릭은 개발자가 컴파일러에게 알려주어서 기계적인 관점에서 에러 발생을 방지하는 것.
=> 제네릭을 사용하면 컴파일러 차원에서 원소를 꺼내는 모든 곳에 절대 실패하지 않음을 보장한다.
즉, 로타입은 절대 사용해서는 안된다. 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다.
그렇다면 자바 진영은 로타입을 왜 만든걸까?
=> 호환성 때문이다. 제네릭은 JDK 1.5부터 도입되었으며, 이 전에는 존재하지 않았다. 그래서 기존 코드와의 호환성을 위해 로 타입을 지원하고 제네릭 구현에는 소거 방식을 사용하기로 했다.
List는 안되고, List<Object>처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다. 차이가 무엇일까? 언뜻 보기에는 둘 다 모든 타입을 받아들일 수 있는 것처럼 보인다. 하지만 주요한 차이는 List는 제네릭과는 아예 관련이 없는 객체이고, List<Object>는 Object를 타입 매개변수로 받는 제네릭 타입이라는 것이다. 즉, List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에게 명확히 전달한 것이다. 그에 반해 List는 Object 뿐만 아니라 다른 타입들도 허용한다. 즉, List<Object>는 Object 타입만을 받아들이는 것이고, List는 그런 것 없이 그냥 모두 받아들이는 것이다.
그렇기 때문에 List는 List<String>을 받아들일 수 있지만, List<Object>는 List<String>을 받아들이지 못한다. 그렇기 때문에List와 같은 로 타입을 사용하면 타입 안전성을 잃게 된다.
-- 로타입을 사용한 코드 예시 --
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
unsafeAdd(stringList, Integer.valueOf(42));
String s = stringList.get(0); //실제 List에서 값을 꺼낼 때, unsafeAdd()에서 add된 Integer 타입이 꺼내지는데 이를 String s에 담으려고 하고 있음.
}
//아래 메서드는 독립 작용임. 즉 컴파일 단계에서 아래 메서드만 봐서는 아무 문제가 없다. 단순히 List에 object를 추가하는 메서드이다.
private static void unsafeAdd(List list, Object o) {
list.add(o); //여기서는 List를 Raw 타입으로 사용하지 말라는 컴파일러 경고만 나옴.
//또한 여기서 list는 stringList가 넘어오지만, 런타임 시에는 타입소거법으로 인해 사실 List가 넘어오는 것임.
//그래서 .add(o)여기서 런타임시 에러가 발생하지 않는다.
//하지만 이 list를 사용하는 곳에서 값을 꺼낼 때 예상치 못한 값이 나와서 런타임 예외가 발생.
}
5 Line
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at generic.unboundedwildcardtype.main(unboundedwildcardtype.java:14)
=> 타입 안전성을 잃을 뿐더러, 런타임 시에 에러가 난다.
private static void unsafeAdd(List<Object> list, Object o) {
list.add(o);
}
이렇게 수정하면, List<String>과 List<Object>는 서로 다른 타입이기에, 컴파일 시 에러가 난다.
===============================================
---------------------------------------
제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 와일드카드(?)를 사용하자. => 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 타입이다.
와일드카드 타입은 안전하고, 로 타입은 안전하지 않다.
로 타입 컬렉션에는 아무 원소나 넣을 수 있다. 와일드카드 타입은 null을 제외한 어떤 원소도 넣을 수 없다.(읽기 전용)
비한정적 와일드카드 타입 사용 -> 컴파일 에러
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Integer> integerList = List.of(1, 2, 3);
unsafeAdd(stringList, Integer.valueOf(42));
System.out.println("stringList.get(0) = " + stringList.get(0));
}
private static void unsafeAdd(List<?> list, Object o) {
list.add(o); ->
}
Required type:
capture of ?
Provided:
Object

public static void main(String[] args) {
List<String> stringList = List.of("아", "에", "이");
List<Integer> integerList = List.of(1, 2, 3);
printList(stringList);
printList(integerList);
}
private static void printList(List<?> list) {
for (Object o : list) {
System.out.print(o + " ");
}
System.out.println();
}
-----------------------------------------------------
로 타입 써야 하는 예외들
1. class 리터럴에는 로 타입을 써야 한다. (자바 명세)
2. instanceof 연산자는 <?> 이외의 매개변수화 타입에는 적용할 수 없다.(java 16 이상부터는 가능) <?>는 아무런 역할 없이 코드만 지저분 해지므로 로 타입을 쓰는 편이 깔끔하다.
instanceof 연산자로 Set 임을 확인했다면 와일드카드 타입인 Set<?>로 형변환 해야 한다. -> 이후 로직에서 잘못된 값 추가 방지 위해
ArrayList<String> list = new ArrayList<>();
if (list instanceof ArrayList<String>) {}
java 8 에서는 Illegal generic type for instanceof 컴파일 에러
java 17 에서는 Condition 'list instanceof ArrayList<String>' is always 'true' 경고만 나오고 컴파일 됨.
java 16부터 변경되었다고 함.
정리
1. 로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안된다.
2. Set<Object>는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이다.
3. Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다. <- 무슨 타입 객체를 저장할 수 있지?
4. Set<Object>와 Set<?>는 안전하지만, 로 타입은 안전하지 않다.
'Effectivie Java' 카테고리의 다른 글
39. 명명 패턴보다 애너테이션을 사용하라 (0) | 2025.01.26 |
---|---|
28. 배열보다는 리스트를 사용하라 (0) | 2025.01.19 |
20. 추상 클래스보다는 인터페이스를 우선하라. (0) | 2025.01.13 |
18. 상속보다는 컴포지션을 사용하라 (0) | 2025.01.05 |
10. equals는 일반 규약을 지켜 재정의하라 (0) | 2024.12.29 |