https://sundaland.tistory.com/163
[ ▶ 타입 소거 (Type Erasure) ]
제너릭은 컴파일 타임에 더 엄격한 타입 체크를제공하고 제너릭 프로그래밍을 지원하기 위해 자바 언어에 도입되었다.
제너릭을 구현하기 위해 자바 컴파일러는 타입 소거를 다음에 적용한다.
- 제너릭 타입에 제한(Bounds)이 있다면 제한 타입으로, 타입 파리미터가 없다면 Object로 대체한다. 따라서 생성된 바이트코드는 일반 클래스, 인터페이스, 메서드만을 포함한다.
- 타입 안전을 유지하기 위해 필요한 경우 타입 케스트를 삽인한다.
- 확장된 제너릭 타입에서 다형성을 유지하기 위해 브리지 메서드를 생성한다.
타입 소거는 파라미터화된 타입에 대해 새 클래스가 생성되지 않도록 한다. 결과적으로 제너릭은 런타임 오버헤드를 발생시키지 않는다.
파라미터화된 타입 : 제너릭 타입을 특정 타입으로 지정한 것 (Box<String>). 또한 파라미터화된 타입이란 제너릭 타입에 구체적인 타입 아규먼트를 제공하여 특정한 타입을 지정한 것을 의미한다.
[ ▷ 제너릭 타입 소거 ]
타입 소거 과정에서 자바 컴파일러는 모든 타입 파라미터를 지우고, 각 타입 파라미터를 제한(Bounds)이 있는 경우 첫 번째 제한 타입으로, 제한이 없는 경우엔 Object로 대체한다.
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
위 코드에서 타입 파라미터 T를 제한이 없기 때문에 자바 컴파일러에선 이를 Object를 바꾼다.
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
# 우리는 코드가 이렇게 변경되는 것을 실제로 볼 수 없다. 이렇게 된다고 예상한 것이다.
아래 코드는 타입 파라미터 T를 Comparable로 변경한다.
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
& 키워드를 사용한 다중 타입 파라미터인 경우엔 앞서 말했듯이 첫 번째 타입 파라미터로 T를 변경한다.
public class MultiBoundExample<T extends Number & Comparable<T>> {
private T value;
public MultiBoundExample(T value) {
this.value = value;
}
public void show() {
System.out.println("Value: " + value);
}
public T getValue() {
return value;
}
}
public class MultiBoundExample {
private Number value;
public MultiBoundExample(Number value) {
this.value = value;
}
public void show() {
System.out.println("Value: " + value);
}
public Number getValue() {
return value;
}
}
[ ▷ 제너릭 메서드 소거 ]
자바 컴파일러는 제너릭 메서드 또한 아규먼트에서 타입 파라미터를 소거한다.
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
타입 파라미터에서 했듯이 제한이 없으므로 이를 Object로 바꾼다.
public static int count(Object[] anArray, Object elem) {
int cnt = 0;
for (Object e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
다음 클래스 3개가 정의되어 있다고 가정한다.
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
다른 모양을 그리는 제너릭 메서드를 작성한다.
public static <T extends Shape> void draw(T shape) { /* ... */ }
자바 컴파일러는 T를 Shape로 대체한다.
public static void draw(Shape shape) { /* ... */ }
# 정규 클래스가 제너릭 클래스를 상속하기 위해서는 파라미터화 되어야한다.
떄떄로 타입 소거로 인해 예상하지 못한 상황이 발생할 수 있다. 다음 예시는 그 상황이 어떻게 발생하는지 보여준다.
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Causes a ClassCastException to be thrown.
Integer x = mn.data;
위의 코드를 컴파일러가 타입 소거하면 다음과 같은 코드가 될 것이라고 예상할 수 있다.
MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
// Note: This statement could instead be the following:
// Node n = (Node)mn;
// However, the compiler doesn't generate a cast because
// it isn't required.
n.setData("Hello"); // Causes a ClassCastException to be thrown.
Integer x = (Integer)mn.data;
컴파일 오류가 발생한다.
[ ▷ 브리지 메서드 ]
파라미터화된 클래스 [public class MyNode extends Node<Integer>]를 확장하거나 파라미터된 인터페이스를 구현하는 클래스 또는 인터페이스를 컴파일할 때 컴파일러는 타입 소거 프로세스의 일부로 브리지 메서드라고 하는 합성 메서드를 생성한다.
위의 코드에서 타입 소거가 된다면 Node와 MyNode 클래스는 다음과 같이 된다.
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
타입 소거 후 메서드 시그니처가 일치하지 않기 때문에 Node.setData(T)는 Node.setData(Object)가 된다.
결과적으로 상속을 하였음에도 MyNode.setData(Integer) 메서드는 Node.setData(Object) 메서드를 재정이(Override)하지 않는다.
이 문제를 해결하고 타입 소거 후 제너릭 타입의 다형성을 유지하기 위해 자바 컴파일러는 하위 타입 지정이 예상대로 작동하는지 확인하는 브리즈 메서드를 생성한다.
class MyNode extends Node {
// Bridge method generated by the compiler
//
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
# setData(Object data) 메서드가 브리지 메서드이다. 우리가 실제 코드로 보는 것을 불가능하지만, 디버깅때 컴파일러에 의해 생성된 것을 확인 할 수 있다.
브리지 메서드 MyNode.setData(Object)는 원래의 MyNode.setData(integer) 메서드에 위임한다. 결과적으로 n.setData("Hello") 명령문은 MyNode.setData(Object) 메서드를 호출하고 "Hello"를 Integer로 캐스팅할 수 없기 때문에
컴파일 오류 (ClassCastExecption)이 발생한다.
[ ▷ 비검증/비구체화 타입 (Non-Reifiable) ]
타입 소거는 비검증 타입을 갖는 varargs 메서드의 가변 인자 (Formal Parameter)에 영향을 미친다.
비검증 구체화 타입은 런타임에 타임 정보를 완전히 사용할 수 있는 타입이다.
- 기본 (Primitive) 타입
- Non-Generics 타입
- Raw 타입
- unbounded 와일드카드 <?> 호출
비검증 타입은 타입 소거 (제한되지 않은 unbounded 와일드카드로 정의되지 않은 일반 타입 호출)에 의해 컴파일 타임에 정보가 제거된 유형이다.
비검증 타입은 런타임에 모든 정보를 사용할 수 없다. 비검증 타입의 예로 List<String> 및 List<Number>가 있다. 자바 가상 머신은 런타임 시 이러한 타입 간의 차이를 구분하지 못한다. 제너릭에 대한 제한에서 볼 수 있듯이 비검증 타입을 사용할 수 없는 특정 상황이 있다.
# 자바 구버전에서는 instanceof 연산자의 피연산자로 비검증 타입을 사용할 수 없었다.
1) 배열의 엘리먼트로 비검증 타입은 사용할 수 없다.
public class Example {
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[5]; // 컴파일 에러!
// 배열의 요소로 비검증 가능한 타입 사용 불가
}
}
위 코드에서 stringLists는 List<String> 타입의 배열이다. 그러나 new List<String>[5] 표현식은 컴파일 에러를 유발한다.
비검증 타입인 List<String>은 배열의 요소로 사용할 수 없기 때문이다.
import java.util.ArrayList;
import java.util.List;
public class NonReifiableExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
// 컴파일러는 이 코드를 컴파일할 때 제네릭 타입을 검사하지만,
// 런타임에는 제네릭 타입 정보가 소거됩니다.
System.out.println("stringList.getClass(): " + stringList.getClass());
System.out.println("numberList.getClass(): " + numberList.getClass());
// 두 클래스의 런타임 타입을 비교
boolean sameClass = stringList.getClass() == numberList.getClass();
System.out.println("Are stringList and numberList of the same class at runtime? " + sameClass);
}
}
[ ▷ 힙 오염 (Heap Pollution) ]
파라미터된 타입의 변수가 해당 파라미터된 타입이 아닌 객체를 참조할 때 힙 오염이 발생한다.
이 상황은 프로그램이 컴파일 타임에 확인되지 않는 (Unchecked) 경고를 발생시키는 일부 작업을 수행한 경우에 발생한다.
Unchecked 경고는 컴파일 시단(컴파일 시간 타입 검사 규칙의 제한 내) 또는 런타임에 파라미터화된 타입 (캐스트 또는 메서드 호출)과 관련된 작업의 정확성을 확인할 수 없는 경우 생성된다.
예를 들어 raw 타입과 파라미터화된 타입을 혼합하거나 Unchecked 캐스트를 수행할 때 힙 오염이 발생한다.
일반적인 상황에서 모든 코드가 동시에 컴파일되면 컴파일러는 잠재적인 힙 오염에 주의를 환기 시키기 위해 Unchecked 경고를 발생시킨다.
코드 섹션을 별도로 컴파일하면 힙 오염의 잠재적 위험을 감지하기 어렵다. 코드가 경고 없이 컴파일되는지 확인하면 힙 오염이 발생하지 않는다.
import java.util.ArrayList;
import java.util.List;
public class HeapPollutionExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Object> objectList = (List<Object>) stringList; // 비검증 가능한 타입의 형변환
objectList.add(10); // 잘못된 유형의 객체를 추가
String value = stringList.get(0); // ClassCastException 발생: 잘못된 유형의 객체를 가져옴
}
}
위 코드에서 stringList는 List<String> 타입의 리스트이다. 그러나 objectList에 타입 캐스팅을 통해 할당할 때 비검증 가능한 타입이 된다.
unboundedList에 add 메서드를 사용하여 잘못된 타입의 객체인 Integer 클래스 객체를 추가하면 힙 오염이 발생한다.
마지막으로 stringList에서 첫 번쨰 엘리먼트를 가져오려고 하면 컴파일 에러가 발생한다. 이는 힙 오염으로 잘못된 타입의 객체를 가져오려고 하였기 때문이다.
이 예시에서 힙 오염이 발생한 이유는 비검증 가능한 타입인 와일드카드 (List<?>)를 사용했기 떄문이다. 와일드카드 타입은 특정한 타입의 객체를 추가할 수 없으므로, 잘못된 타입의 객체를 추가하는 시도가 힙 오염을 발생시켰다.
아래의 코드도 와일드카드 사용으로 인하여 힙 오염이 발생하였다.
import java.util.ArrayList;
import java.util.List;
public class HeapPollutionExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<List<?>> nestedList = new ArrayList<>();
nestedList.add(stringList); // 비검증 가능한 타입을 포함하는 리스트 추가
List<Integer> integerList = new ArrayList<>();
nestedList.add(integerList); // 비검증 가능한 타입을 포함하는 리스트 추가
List<?> list1 = nestedList.get(0);
List<?> list2 = nestedList.get(1);
list1.add(10); // 힙 오염 발생: 잘못된 타입의 객체를 추가
list2.add("Hello"); // 힙 오염 발생: 잘못된 타입의 객체를 추가
String value = stringList.get(0); // 컴파일 에러: 잘못된 타입의 객체를 가져오려고 함
Integer number = integerList.get(0); // 컴파일 에러: 잘못된 타입의 객체를 가져오려고 함
}
}
[ ▷ 비검증 가능한 foraml 파라미터를 사용하는 Varargs 메서드의 잠재적인 취약점 ]
(Potenial Vulnerabilitieds of Varargs Methods With Non-Reifiable Formal Parameters)
varargs 입력 파라미터를 포함하는 제너릭 메서드는 힙 오염을 일으킬 수 있다.
public class ArrayBuilder {
public static <T> void addToList (List<T> listArg, T... elements) {
for (T x : elements) {
listArg.add(x);
}
}
public static void faultyMethod(List<String>... l) {
Object[] objectArray = l; // Valid
objectArray[0] = Arrays.asList(42);
String s = l[0].get(0); // ClassCastException thrown here
}
}
public class HeapPollutionExample {
public static void main(String[] args) {
List<String> stringListA = new ArrayList<String>();
List<String> stringListB = new ArrayList<String>();
ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
List<List<String>> listOfStringLists =
new ArrayList<List<String>>();
ArrayBuilder.addToList(listOfStringLists,
stringListA, stringListB);
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
}
}
ArrayBuilder.addToList 메서드에서 컴파일 오류가 발생한다.
컴파일러는 varargs 메서드를 만나면 varargs foraml 파라미터로 배열을 반환한다. 하지만 자바 프로그래밍 언어는 파라미터된 타임의 배열 작성을 허용하지 않는다. 메서드 ArrayBuilder.addToList에서 컴파일러는 varargs foraml 파라미터 T... 엘리먼트를 formal 파라미터 T[ ] 요소인 배열로 변환한다.
그러나 타입 소거로 인해 컴파일러는 varargs formal 파라미터를 Object[ ] 요소로 반환한다. 결과적으로 더미 오염의 가능성이 있다.
다음 코드 명령문은 varargs formal 파라미터 l을 Object 배열 objectArgs에 할당한다.
Object[] objectArray = l;
이 명령문으 잠재거으로 힙 오염을 일으킬 수 있다. varargs formal 파라미터 l의 파라미터화된 타입과 일치하는 값을 변수 objectArray에 할당할 수 있으므로, l에 할당할 수 있습니다.
그러나 컴파일러는 이 코드 명령문에서 확인되지 않는 경고를 생성하지 않는다. 컴파일러는 varargs foraml 파라미터 List<String>... l을 formal 파라미터 List [ ] l로 변환할떄 이미 경고를 생성했기 때문이다.
이 명령문은 유효하기 때문에 변수 l은 Object[ ]의 서브타입인 List[ ] 타입을 가진다
결과적으로 컴파일러는 다음 코드 명령문에 표시된 대로 objectArray 배열의 엘리먼트에 모든 타입의 List 객체를 할당하는 경우 경고 또는 오류를 발행하지 않는다.
objectArray[0] = Array.asList(42);
이 코드 명령문은 Integer 타입의 객체 하나를 포함하는 List 객체와 함꼐 objectArray 배열의 첫 번쨰 엘리먼트에 할당한다.
다음 명령문으로 아래의 코드가 호출될 때 자바 가상 머신은 런타임 오류를 발생시킨다.
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
// ClassCastException thrown here
String s = l[0].get(0);
변수 l의 첫 번째 배열 엘리먼트에 저장된 객체의 타입은 List<Integer>이지만 이 코드 명령문은 List<String> 타입의 객체를 예상했기 때문이다.
[ ▷ 비검증 기본 파라미터를 사용하여 Varargs 메서드의 경고 방지 ]
(Prevent Warnings form Varargs Methods with Non-Reifiable Formal Parameters)
파라미터화된 타입의 파라미터가 있는 varags 메서드를 선언하고 메서드 본문이 varagrs 기본 파라미터의 부적절한 처리로 인해 ClassCatException 또는 기타 유사한 예외를 throw 하지 않는지 확인하면 컴파일러가 다음과 같은 경고를 방지할 수 있다. 정적 및 비생성자 메서드 선언에 다음 어노테이션을 추가하여 이러한 종류의 varargs 메서드를 생성한다.
@SafeVarargs
@SafeVarargs 어노테이션은 메서드 contract의 문서화된 부분이다. 해당 어노이테이션은 메서드의 구현이 varagrs 기본 파라미터를 부적절하게 처리하지 않을 것이라고 주장한다.
바람직하진 않지만 메서드 선언에 다음을 추가하여 이러한 경고를 억제할 수 도 있다.
@SuppressWarnings({"unchecked", "varargs"})
하지만 이 방법은 메서드 호출 사이트에서 생성된 경고를 억제하지 않는다.
[ ▷ 제너릭 인스턴스 타입은 기본 타입과 같이 사용할 수 없다 ]
(Cannot Instantiate Generic Types with Primitive Types)
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
// ...
}
Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error
Pair 객체를 생성할 때, 타입 파라미터 K 또는 V를 기본 타입으로 대체할 수 없다.
타입 파라미터는 오직 non-primitive 타입으로만 대체할 수 있다.
Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));
# 자바 컴파일러 8부터는 8을 Integer.valueOf(8)으로, a를 Character(a)로 오토박싱한다.
[ ▷ 타입 파라미터로 인스턴스를 생성할 수 없다. ]
(Cannont Create Instances of Type Parameters)
타입 파라미터의 인스턴스는 생성할 수 없다. 아래의 코드는 컴파일 타임 에러를 발생시킨다.
public static <E> void append(List<E> list) {
E elem = new E(); // compile-time error
list.add(elem);
}
리플렉션을 통해 타입 파라미터의 객체를 생성하여 해결할 수 있다.
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
아래 코드와 같이 append 메서드를 호출할 수 있다.
List<String> ls = new ArrayList<>();
append(ls, String.class);
[ ▷ Cannot Declare Static Fields Whose Types are Type Parameters ]
클래스의 static 필드는 클래스의 모든 비정적 객체에 의해 공유되는 클래스 레벨의 변수이다. 따라서 타입 파라미터의 정적 필드는 허용되지 않는다.
public class MobileDevice<T> {
private static T os;
// ...
}
만약 타입 파라미어의 정적 필드가 허용된다면 다음 코드는 혼란스러워진다.
MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();
static 필드가 OS가 핸드폰, 호출기, 컴퓨터와 공유되는 상태이다, 그렇다면 OS의 실제 타입은 무엇일까? 이것이 동시에 핸드폰이자 호출기이자 컴퓨터일 수는 없기 때문이다.
그렇기에 타입 파라미터의 static 필드는 생성할 수 없다.
[ ▷ Cannot Use Casts or instanceof with Parameterized Types ]
자바 컴파일러가 제너릭 코드에서 모든 타입 파라미터를 지우기 때문에, 런타임에 어떤 파라미터화된 타입이 사용되고 있는지 확일할 수 없다.
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
// ...
}
}
S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }
런타임은 타입 파라미터를 추적하지 않기 때문에 ArrayList<Integer>와 ArrayList<String>의 차이를 알 수 없다. 할 수 있는 최대한은 unnounded 와일드카드를 사용하여 리스트가 ArrayList 인지 확인하는 것이다.
public static void rtti(List<?> list) {
if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type
// ...
}
}
일반적으로 unbounded 와일드카드로 파라미터화되지 않은 이상, 파라미터화된 타입으로 캐스팅할 수 없다.
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // compile-time error
그러나 어떤 경우에는 컴파일러가 타입 파라미터가 항상 유효하다는 것을 알고 캐스트를 허용한다.
List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1; // OK
[ ▷ Cannot Create Arrays of Parameterized Types ]
파라미터화된 타입의 배열은 생성할 수 없다.
아래 코드는 컴파일 오류가 발생한다.
List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time error
다음 코드는 다른 타입들이 배열에 삽입될 때 발생하는 상황을 보여준다.
Object[] strings = new String[2];
strings[0] = "hi"; // OK
strings[1] = 100; // An ArrayStoreException is thrown.
같은 것을 제너릭 리스트로 시도한다면 문제가 발생한다.
Object[] stringLists = new List<String>[2]; // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>(); // OK
stringLists[1] = new ArrayList<Integer>(); // An ArrayStoreException should be thrown,
// but the runtime can't detect it.
파라미터화된 리시트의 배열이 허용된다면, 이전 코드는 ArrayStoreException을 발생시키지 못할 것이다.
[ ▷ Cannot Create, Catch, or Throw Objects of Parameterized Types ]
제너릭 클래스는 Throwable 클래스를 직접적이거나 간접적으로 확장할 수 없다.
아래의 코드는 이런 이유로 컴파일 오류가 발생한다.
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // compile-time error
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
메서드는 인스턴스화된 타입 파라미터를 잡을 수 없다.
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
하지만 throws 구문에서는 타입 파라미터를 사용할 수 있다.
class Parser<T extends Exception> {
public void parse(File file) throws T { // OK
// ...
}
}
[ ▷ Cannot Overload a Method Where the Formal Parameter Types of Each Overload Erase to the Same Raw Type ]
각 오버로드의 formal 파라미터 타입이 동일한 raw 타입으로 소거되는 경우, 메서드를 오버로드 할 수 없다.
클래스는 타입 소거 후에 동일한 시그니처를 가질 두 개의 오버로드된 메서드를 가질 수 없다.
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}
이 오버로드들은 모두 같은 클래스 파일 표현을 공유하기 때문에 컴파일 에러를 발생시킨다.
'자바 튜토리얼' 카테고리의 다른 글
JDBC API (0) | 2024.08.02 |
---|---|
Collections [1] (0) | 2024.07.19 |
제너릭 (Generics) [3] (0) | 2024.07.17 |
제너릭 (Generics) [2] (0) | 2024.07.16 |
제너릭 (Generics) [1] (0) | 2024.07.16 |