요즘에 이펙티브 자바를 읽고 있다. 그런데 이펙티브 자바는 설명이 함축적인 경우가 많아서 읽기가 쉽지 않은 책이다.
그래서 이펙티브 자바 책과 함께 백기선님의 이펙티브 자바 완벽 공략 강의를 보면서 기선님과 함께 스터디하는 느낌으로 책을 읽어 나가고 있다.
아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라.
아이템 32 파트를 강의를 수강하다가 아래와 같은 질문 글을 발견했다.
아이템 32. 핵심 정리 12:00 이 부분에서 질문이 있습니다.
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
제가 자바 기초가 약해서 이해를 못하는걸까요? ..
bytecode로 확인하면
pickTwo 메소드를 반환하게 된다면 Object(추상적) 형식으로 반환 해서 String(구체적) 형식으로 변환(타입 케스트) 해서 문제(ClasscastException)가 발생된다고 해주셨는데요
그러니깐
결론은 이것인데
Object obj = new Object();
obj = "test";// 추상적인 Object 에 담음String string = (String) obj;// 구체적인 String 에 담음
해당 원인이라면 해당 소스는 에러가 발생해야 되는것이 아닌가요?....
자바 기초가 약해서 그런지 조금 이해가 안되는 부분이 있어서 질문 드립니다.
이펙티브 자바 책이랑 강의에서 Object[] → String[]으로 타입 캐스팅이 Object가 String의 하위 타입이 아니라고 간단하게 설명해주고 넘어가서 좀더 자세하게 코드를 분석해보기로 했다.(그리고 질문에 대한 답변 글도 남겼다.)
이 질문을 이해하기 앞서 이펙티브 자바에서 사용된 예제 코드는 다음과 같다.
public class PickTwo {
static <T> T[] toArray(T... args){
return args;
}
static <T> T[] pickTwo(T a, T b, T c){
switch(ThreadLocalRandom.current().nextInt(3)){
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError(); // 도달할 수 없다.
}
public static void main(String[] args) {
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
System.out.println(Arrays.toString(attributes));
}
}
/*
Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String; ([Ljava.lang.Object; and [Ljava.lang.String; are in module java.base of loader 'bootstrap')
위와 같은 타입 에러 발생
*/
이 예제 코드는 제네릭과 가변 인수가 함께 사용 되었을 때, 런타임 에러인 ClassCastException
가 발생할 수 있는 상황을 설명하기 위해 제공되었다.
제네릭은 ‘컴파일 시점’에 타입 안정성을 보장하기 위해 자바에서 사용하는 문법인데, 이 경우 ‘컴파일 시점’에 타입 안정성을 보장하지 못하고 '런타임 시점'에 ClassCastException
가 발생했다.
이 에러가 발생한 과정을 제대로 이해해보기 위해서는 제네릭 타입 이레이저(Type Erasure)에 대한 이해가 필요하다.
타입 이레이저는 제네릭 이전 버전(JDK 1.5 이전)에 작성된 소스 코드 간의 호환성을 위해 도입되었다.
간단하게 말하면 제네릭 타입은 컴파일 시에만 존재하고, 컴파일된 바이트 코드에서는 타입에 대한 정보가 존재하지 않는다.
여기서 실제로 코드와 함께 이레이저 과정을 살펴보자.
static <T> T[] pickTwo(T a, T b, T c){
switch(ThreadLocalRandom.current().nextInt(3)){
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError(); // 도달할 수 없다.
}
public static void main(String[] args) {
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
System.out.println(Arrays.toString(attributes));
}
먼저 <>는 지워지고, T는 실제 타입으로 치환된다.
하지만 main 함수에서 pickTwo()
의 인자로 String
이 주어지기 때문에 다음과 같이 타입 이레이저가 진행될 거라고 잘못 생각하기 쉽다.
// 아래와 같이 타입 이레이저가 진행되는게 아니다!
static String[] pickTwo(String a, String b, String c){
switch(ThreadLocalRandom.current().nextInt(3)){
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError(); // 도달할 수 없다.
}
public static void main(String[] args) {
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
System.out.println(Arrays.toString(attributes));
}
실제로 바이트 코드를 참고하면 제네릭 T 타입은 다음과 같이 Object
로 타입 이레이저가 진행되는 것을 살펴볼 수 있다.(이후에 타입 캐스팅이 필요한 시점에 컴파일러가 타입 추론을 통해 타입 캐스팅 해준다.)
public class PickTwo {
static Object[] toArray(Object... args){
return args;
}
static Object[] pickTwo(Object a, Object b, Object c){
switch(ThreadLocalRandom.current().nextInt(3)){
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError(); // 도달할 수 없다.
}
public static void main(String[] args) {
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
System.out.println(Arrays.toString(attributes));
}
}
위 코드에서 “좋은”, “빠른”, “저렴한”
은 원래 String
객체이기 때문에 String
↔ Object
로 타입 변환이 자유롭다.
따라서 다형성 덕분에 pickTwo()
내부에서 업캐스팅 되어 Object
타입으로 변환되어 사용된다.
이때 toArray()
는 내부에서 가변 인수인 Object
를 저장하기 위해서 Object[]
객체를 사용해서 저장한 후 해당 Object[]
객체를 리턴한다.
결과적으로 main 함수에서 다음과 같은 상황이 발생한다.
public static void main(String[] args) {
String[] attributes = (String[])Object[] //pickTwo("좋은", "빠른", "저렴한")이 리턴한 객체
System.out.println(Arrays.toString(attributes));
}
하지만 Object[] 객체는 그 자체가 Object[]이기 때문에 앞에서 String[]으로 다운 캐스팅 될 수 없다.
그래서 이펙티브 자바 책에서는
그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
라고 강조하고 있다.
위 예제 코드로 예를 든다면
static <T> T[] toArray(T... args){
return args;
}
여기서 제네릭과 함께 사용된 가변 인자 args
를 바로 리턴해서 외부에 노출하는 경우를 의미한다.