2024. 9. 26. 16:38ㆍBackend/Java
배열과 제네릭의 타입 소거
일반적으로 제네릭은 컴파일 시점에만 컴파일을 보장하고, 런타임에는 타입을 Object로 통일 해버린다. (예외 경우로 extend로 bound를 제한하는 경우에는 아닐 수도 있다.) 이를 타입 소거라고 하는데, 이것 때문에 런타임에는 제네릭의 원래 타입을 알 수 없다.
이러한 타입 소거 때문에, Java에서는 제네릭 배열 생성이 금지된다.
List<User>[] lists = new ArrayList<>[100]; // <- 이러면 컴파일 오류 발생
List<User>[] lists = new ArrayList[100]; //이렇게 하면 오류는 안 생김
이러한 이유는, 배열이 런타임에 타입을 검사를 한다는게 주요한데,
만약 컴파일 단계에서 잡히지 않고, List<User>[] lists에, new ArrayList<Store>()를 넣으려고 하더라도, 제네릭은 Object로 치환되어있기 때문에 런타임에도 오류가 발생하지 않는다.
void test() {
List<String>[] list = new List[1];
Object[] object = list;
object[0] = new ArrayList<Integer>();
if(object[0] instanceof ArrayList arrayList) {
arrayList.add(1);
System.out.println("arrayList.get(0) = " + arrayList.get(0));
}
list[0].add("Hi");
System.out.println(list[0]);
}
위와 같은 코드를 실행한다면, 자바의 ArrayList가 파이썬의 List가 되는 기적을 볼 수 있다.
사실 배열이 아니라도 이런 일이 생길 수는 있지만, 자바에서 배열에 제네릭을 더 엄격하게 방지하는 이유는 위에서 언급한 배열의 특징과 관련이 있는데
배열은 런타임에 잘못된 값이 들어올 경우 ArrayStoreException을 발생시킨다.
@DisplayName("Array에 다른 값이 들어오는 경우")
@Test
void test3() {
Object[] list = new String[10];
list[0] = "hello";
list[1] = 1;
}
컴파일러에서는 다음과 같은 warning을 보여주고
Storing element of type 'java. lang. Integer' to array of 'java. lang. String' elements will produce 'ArrayStoreException'
실제 코드를 실행시키면
java.lang.ArrayStoreException: java.lang.Integer
위의 예외가 발생한다.
배열은 이렇게, 런타임에도 타입 정보를 바탕으로 배열에 잘못된 값이 들어갈 경우에 대한 예외처리를 진행하지만,
제네릭에 대해서는 제네릭 값이 타입 소거가 되기 때문에 런타임에 검사를 진행하더라도 같은 값으로 처리된다. 이 때문에 자바에서는 배열에 대해 제네릭을 금지시키는 것 같다.
그렇다면 만약 ? extends로 bound를 제한하는 경우에는 괜찮을까?
가설: 배열이 타입과 제네릭 모두를 비교할 것이다?
배열은 타입과, 제네릭을 비교하지만, 제네릭이 Object로 통일되었기 때문에 배열의 런타임 타입 체크가 무용지물이 되었을 것으로 생각했다.
이 가설에 대해 bound가 실제로 어떻게 동작하는지 알아봤다.
제네릭에서의 bound
제네릭에서 만약 <? extends Number?를 사용한다면 Object로 치환되지 않고 Upper Bound의 Number 클래스로 소거된다.
@Test
void test5() {
List<? extends String> list = new ArrayList<>();
Object objList = list;
if(objList instanceof ArrayList arrayList) {
arrayList.add(1);
System.out.println("arrayList.get(0) = " + arrayList.get(0));
}
System.out.println(list);
}
type을 소거할 때 extends의 경우 upper bound로 타입 소거가 된다.
이를 확인하기 위해 코드를 작성하고 javac로 컴파일하여 바이트코드를 찾아봤다.
public class GenericExample<T> {
private T valueT;
public GenericExample(T valueT) {
this.valueT = valueT;
}
public T getValueT() {
return valueT;
}
public static void main(String[] args) {
GenericExample<Integer> example = new GenericExample<>(1);
System.out.println(example.getValueT());
}
}
$ javap -c GenericExample
Warning: File ./GenericExample.class does not contain class GenericExample
Compiled from "GenericExample.java"
public class com.example.generic.GenericExample<T> {
public com.example.generic.GenericExample(T);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #7 // Field valueT:Ljava/lang/Object;
9: return
public T getValueT();
Code:
0: aload_0
1: getfield #7 // Field valueT:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #8 // class com/example/generic/GenericExample
3: dup
4: iconst_1
5: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
8: invokespecial #19 // Method "<init>":(Ljava/lang/Object;)V
11: astore_1
12: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
15: aload_1
16: invokevirtual #28 // Method getValueT:()Ljava/lang/Object;
19: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
22: return
}
생성자와 getValueT 메서드의 바이트코드 주석을 보면 fieldT에 대해 java/lang/Object로 처리하고 있는 것을 확인할 수 있다.
main메서드의 16번째 코드를 확인하더라도
16: invokevirtual #28 // Method getValueT:()Ljava/lang/Object;
19: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
따로 Object를 캐스팅 하지 않고 println에서 사용하는 것을 확인할 수 있다.
(지금은 Object에 있는 toString을 그대로 사용해서 그렇고, 만약 Integer 타입의 다른 메서를 사용하게 된다면 checkcast라는 바이트코드가 추가된다.)
checkcast는
26: invokevirtual #28 // Method getValueT:()Ljava/lang/Object;
29: checkcast #14 // class java/lang/Integer
32: invokevirtual #37 // Method java/lang/Integer.longValue:()J
이렇게 checkcast를 통해 캐스팅 이후 메서드를 호출하게 된다.
checkcast는 보통 자바에서 Integer a = (Integer) c;이렇게 명시적으로 캐스팅할 때 사용된다.
만약 적절하지 않은 타입으로 캐스팅하려고 하면 ClassCastException을 발생한다.
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at com.example.generic.Test.main(Test.java:10)
만약 upper bound를 준 경우에는 type 소거가 Number로 일어나게 된다
public class GenericExample<T extends Number> {
private T valueT;
public GenericExample(T valueT) {
this.valueT = valueT;
}
public T getValueT() {
return valueT;
}
public static void main(String[] args) {
GenericExample<? extends Number> example = new GenericExample<>(1);
System.out.println(example.getValueT());
}
}
$ javap -c GenericExample
Warning: File ./GenericExample.class does not contain class GenericExample
Compiled from "GenericExample.java"
public class com.example.generic.GenericExample<T extends java.lang.Number> {
public com.example.generic.GenericExample(T);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #7 // Field valueT:Ljava/lang/Number;
9: return
public T getValueT();
Code:
0: aload_0
1: getfield #7 // Field valueT:Ljava/lang/Number;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #8 // class com/example/generic/GenericExample
3: dup
4: iconst_1
5: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
8: invokespecial #19 // Method "<init>":(Ljava/lang/Number;)V
11: astore_1
12: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
15: aload_1
16: invokevirtual #28 // Method getValueT:()Ljava/lang/Number;
19: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
22: return
}
이렇게 됐을 때, 만약 upper bound를 둔다면 타입 소거가 모두 Object로 이루어지지 않기 떄문에 제네릭 배열에서도 런타임 체크가 가능하지 않을까 싶었다.
제네릭 배열의 바이트코드는?
그래서 제네릭 배열을 만들어서 바이트코드를 확인해봤다.
public static void main(String[] args) {
ArrayList<Integer>[] list = new ArrayList[1];
list[0] = new ArrayList<>();
list[0].add(1);
}
위 코드의 바이트코드를 확인해보면
1: anewarray #7 // class java/util/ArrayList
4: astore_1
5: aload_1
6: iconst_0
7: new #7 // class java/util/ArrayList
10: dup
11: invokespecial #9 // Method java/util/ArrayList."<init>":()V
14: aastore
15: aload_1
16: iconst_0
17: aaload
18: iconst_1
19: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: invokevirtual #16 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
25: pop
이렇게 anewarray에 대한 class가 단순 ArrayList의 raw타입이 사용되고 있었다.
anewarray는 참조 타입 배열을 생성할 때 사용하는 명령이다.
이를 통해 ArrayList객체를 담을 수 있는 배열을 생성하게 되고, 추가적인 제네릭에 대한 정보는 없다.
이후 18: aastore를 통해 스택의 최상단에 있는 값을 배열에 저장한다. 따라서 이 aastroe 내에서 런타임 체크가 일어나는 것이다.
aastore는 저장할 때, 배열의 레퍼런스와, 저장할 객체의 레퍼런스를 비교하여 타입 검사를 런타임에 수행한다.
그렇기 떄문에 선언 당시의 제네릭이 뭐가 들어가던지, raw타입에 대해서만 비교를 하게 되고, ArrayList<?>에서 ?에 뭐가 들어가던지 간에 같은 것으로 간주한다.
그리고, 애초에 자바에서는 제네릭 배열을 만들 수 없다.
하지만, 어떻게 선언은 가능했던걸까?
ArrayList<Integer>[] list; // 선언은 가능
이걸 왜 허용해둘까 고민 해본 결과는
- 선언은 단순히 참조 변수를 선언하는 것이기 때문에 타입 소거 문제를 크게 문제삼지 않는 것 같다.
- 하지만 배열을 실제로 생성하는 것은 타입 정보를 런타임에 유지해야 하기 때문에 제네릭 배열 생성을 허용하지 않는 것 같다.
선언까지는 이해해주지만, 실제 생성을 하려면 제네릭까지 보존해야하기 때문에 허용하지 않는다고 이해했다.
그러면 bound있는 제네릭 배열을 선언하더라도?
List<? extends Number>[] list = new List[1];
Object[] object = list;
object[0] = new ArrayList<Integer>();
if(object[0] instanceof ArrayList arrayList) {
arrayList.add("HiHi");
System.out.println("arrayList.get(0) = " + arrayList.get(0));
}
뭔가 그럴듯해 보이지만, 결국 까고 보면 List 배열에서는 <? extends Number> 의 정보가 남지 않고 raw 타입의 List로만 배열을 생성한다.
애초에 자바에서는 제네릭 타입 배열 생성을 허용하지 않는다. 그래서 List<? extends String>[] list = new List[1];가 제네릭 배열을 생성하는 것 처럼 보이지만, 결국은 List[]로 처리된다.
애초에 선언과 상관없이, 생성할 때는 new List[]로 생성해야하기 때문에 선언에서의 bound제약은 아무런 의미가 없게 된다.
결론
자바에서는 제네릭 배열의 생성을 허용하지 않는다.
제네릭을 포함하여 배열을 생성할 수 없고, raw타입으로만 생성이 가능하다.
단, 선언부에는 제네릭 타입의 배열을 선언할 수는 있다.
ObjectMapper의 TypeReference는?
작성 중
'Backend > Java' 카테고리의 다른 글
Synchronized, Heavy-weight Lock 동작 원리, ReentrantLock과의 성능 비교 (3) | 2024.10.03 |
---|---|
Condition이 동작하는 원리 (ReentrantLock + AbstractQueuedSynchronizer) (2) | 2024.09.22 |
ReentrantLock이 동작하는 원리 (AbstractQueuedSynchronizer) (1) | 2024.09.17 |