로 타입은 사용하지 말라
작성자: 프람
작성 일시: 2024_05_06
내용: Effective java 3/E 5장 아이템26
이펙티브 자바 Effective Java 3/E - 예스24
자바 플랫폼 모범 사례 완벽 가이드 - Java 7, 8, 9 대응자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브
m.yes24.com
인사
안녕하세요🤗 프람입니다.
다시 이펙티브 자바 내용으로 돌아왔어요.
짧은? 연휴간 잘 지내셨나요??
저는 잘 지내지 못했어요😭
우.테.코 미션 진행하랴, 테스트에 대한 학습하랴 좀 바쁘게 지내다 보니 기운이 많이 없습니다.
그래도 이번 내용 알차게 잘 정리해보겠습니다🔥
로(Raw) 타입이 뭔데?
본격적으로 제네릭을 다루는 5장까지 왔습니다.
꽤 빠르게 진행되고 있어 놀라운데요.
제네릭의 첫 번째 아이템은 로타입은 사용하지 말라입니다.
그래서 로 타입(Raw Type)이 뭔데?
이름이 좀 생소할 수 있는데요.
책에서 설명하길, 제너릭 타입에서 타입 매개변수를 전혀 사용하지 않았은 타입이라고 합니다.
다소 이해하기 난해하지만, 코드를 보면 쉽게 이해가 가능하죠
private final Collection stamp = ....;
위 코드와 같이 제네릭 타입(Collection<T>)에 타입매개변수(<T>)를 명시해주지 않은 것을 말해요.
참 쉽죠??
그래서 로 타입의 문제가 뭔데?
로 타입의 문제는 간단합니다. 우리가 지난 번에 알아본 '아이템 14. Comparable 구현을 고려해라'를 예시로 들어 볼게요.
(Comparable을 설명하고자 하는 것은 아닙니 내용을 모르셔도 됩니다)
1번. 로 타입의 Comparable을 구현한 Point class🤮
class Point implements Comparable {
private int x;
private int y;
@Override
public int compareTo(Object o) {
Point p = (Point) o;
int result = Integer.compare(x, p.x);
if(result == 0) {
return Integer.compare(y, p.y);
}
return result;
}
}
2번. 제너릭 타입의 Comparable을 구현한 Point class🤗
class Point implements Comparable<Point> {
private int x;
private int y;
@Override
public int compareTo(Point p) {
int result = Integer.compare(x, p.x);
if(result == 0) {
return Integer.compare(y, p.y);
}
return result;
}
}
자, 예시 코드를 보니 무엇이 문제인지 단번에 눈치를 채신 분도 있을실거에요.
제가 조금의 설명 덧붙히자면, 1번 코드의 경우 로타입의 Comparable 인터페이스를 구현하고 있어
컴파일러가 비교 대상의 compareTo를 타입을 알지 못해요. 따라서 Object 타입을 매개변수로 받아와
내부적으로 Point로 형변환을 해주고 있습니다.
이렇게 된다면 아주 큰 문제가 생긱게 되는데요.
compareTo 매개변수로 Point가 아닌 Integer 타입의 참조가 들어왔다고 가정해봅시다.
코드 작성자는 해당 사실(버그)를 언제 찾을 수 있을까요?
바로.. 바로.. 런타임에 버그가 잡히게 됩니다.
반면, 2번 코드의 경우에 다른 타입이 매개변수로 들어오게 된다면, 컴파일 타입에 컴파일러가
오류를 잡아주조~~ Fail fast의 측면에서 2번 코드가 더 올바르다 볼 수 있겠습니다.
그런데, 여기서 궁금증 하나 생기더군요.
왜? 제네릭 타입을 명시하지 않으면, 하필 왜 Object 타입으로 받아야하는가 인거죠
내부적으로 바이트 코드를 뜯어보면, 컴파일러가 모든 제너릭 타입에 대해
자동으로 Object타입으로 타입 캐스팅을 진행하기 때문이에요. 버전 하위 호환성을 위해 말이죠
이걸 전문 용어로 "소거"라고 합니다 ㅋㅋ
눈으로 살펴봅시다. 아래 제네릭 Point 클래스가 있습니다.
이 코드를 byte 코드로 뜯어보면, 아래와 같이 컴파일러가 자동으로 Object 타입 캐스팅 해줌을 알 수 있습니다.
궁금증! 그럼 List와 List<Object>는 다른거야?
결론부터 말씀드리자면, 네! 달라요.
해당 개념을 알기 위해 선수 지식부터 알아봅시다.
이 역시 코드로 한번 살펴 봅시다.
Integer a = 11;
Number n = a;
이 코드는 문제가 없습니다.
상속 관계에서는 공변 타입(아이템 28에서 자세히 다룰 예정)을 보장해주죠.
하지만, 아래와 같이
제네릭 타입끼리는 공변하지 않습니다.
List<Integer> iList = new ArrayList<>();
List<Number> nList = iList //컴파일 에러 발생
왜 그런지에 대한 오라클의 설명을 인용해보겠습니다.
Integer는 Number의 서브 타입은 맞으나, nList 는 Number객체를 원소로 가지는 리스트이다,
반면, iList는 Integer객체를 원소로 가지는 리스트이다. 이 사이에서는 어떠한 관계도 존재하지 않는다.
Because Integer is a subtype of Number, and numList is a list of Number objects, a relationship now exists between intList (a list of Integer objects) and numList.
말이 좀 어렵지만, 와이들 카드를 사용하라고 하네요🤔
그림을 통해 제너릭의 공변 관계를 볼까요?
참~~ 복잡하다. 그죠?
List<Integer> iList = new ArrayList<>();
List<? extends Number> nList = iList;
결론적으로 어렇게 만들어야 한다는 거죠!!
자 그럼, 본론으로 돌아와서 List와 List<Object>는 왜 다른건데??
List는 그림으로 보자면 List<?>와 비슷해요.(완전 같은건 아님 뒤에서 자세하게 다루겠습니다)
어떠한 값이 들어와도 컴파일러는 상관하지 않는다느 것이죠.
반면 List<Object>는 컴파일러가 "오호! 주인님께서 Object 타입을 주시겠군!"이라고 타입에 대한 정보를 알고 있어요.
물론 컴파일 에러는 나지 않겠지만,
아래와 다른 하위 타입이 들어 가게 되면
컴파일러는 머리가 어지러워지면서 경고를 합니다.
여기까지 잘 따라 오셨나요??
히자만 진짜 큰 차이점이 있습니다.
List: 근본적으로 나 제너릭에서 발뺄게~~~
List<Object>: 난 제네릭을 담고 있지만 모든 타입 다 받을게~~~
의 차이입니다.
위에서 설명한 제네릭 서브타이밍 규칙을 이해하셨다면,
쉽게 이해가 가능할거에요!
이 역시 코드로 살펴 보죠
List<String> sList = new ArrayList<>();
List<Integer> iList = new ArrayList<>();
List list;
list = sList; // 가능
list = iList; // 가능
List<Object> oList;
oList = sList; // 불가능 컴파일 에러!
oList = iList; // 불가능 컴파일 에러!
무슨 차이인지 아시겠나요???
List는 컴파일러가 아에 제네릭이 없다 판단해버리고 List<String> 제네릭타입이든 List<Integer> 제네릭 타입이든 다 허용해주지만
List<Object>는 제네릭 서브타이핑규칙에 의해 List<Object> != List<String> 으로 인식하기 때문에, 컴파일 자체가 안되는 것이죠.
여기서 또 꼬리를 무는 궁금증이 있습니다.
그럼 매개변수로 넓은 범위의 제네릭 인스턴스를 받으려면 List 타입으로 받으면 되겠네???
이 질문은 아래 주제를 통해 이해봅시다.
List와 List<?>의 차이
Set(제목의 List와 같은 로타입)을 사용한 예시🤮
static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1: s1) {
if (s2.contains(o1)) result++;
}
return result;
}
이 코드를 보면 어떤가요? 글 처음에 설명해준 comparable구현과 같이 안전하지 못하다는 느낌이 팍팍 드실겁니다.
이럴때 사용할 수 있는 것이 비한적정적 와일드 카드입니다(<?>)
static int numElementsInCommon(Set<?> s1, Set<?> s2) {
int result = 0;
for (Object o1: s1) {
if (s2.contains(o1)) result++;
}
return result;
}
이게 이게 무슨 차이가 있는지 결론만 말씀드리자면
와일드 카드는 로타입과 달리 타입 안정성을 보장해준다는 겁니다.
위 사진과 같이 와일드카드로 들어오는 값은 불변식을 보장해줍니다.
ㅎㅎ 매우 큰차이가 있다는 걸 알겠죠??
결론
제네릭 객체를 사용할 때는 절대 로 타입 쓰지마라!!!!