제네릭 타입 삭제
제네릭은 컴파일 시 보다 엄격한 타입 검사를 제공하고 제네릭 프로그래밍을 지원하기 위해 Java 언어에 도입되었습니다. 제네릭을 구현하기 위해 Java 컴파일러는 타입 지우기를 적용합니다:
- 제네릭 타입의 모든 타입 매개변수를 해당 바운드로 바꾸거나 타입 매개변수가 바운딩되지 않은 경우 Object로 바꿉니다. 따라서 생성된 바이트코드에는 일반 클래스, 인터페이스 및 메서드만 포함됩니다.
- 필요한 경우 타입 안전을 유지하기 위해 타입 형 변환을 삽입합니다.
- 확장된 제네릭 타입의 다형성을 보존하기 위해 브리지 메서드를 생성합니다.
타입 삭제는 매개변수화된 타입에 대해 새 클래스가 생성되지 않도록 보장하므로 제네릭은 런타임 오버헤드가 발생하지 않습니다.
타입 삭제 프로세스 중에 Java 컴파일러는 모든 타입 매개변수를 지우고 타입 매개변수가 바인딩된 경우 첫 번째 바인딩으로, 바인딩되지 않은 경우 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는 바인딩되지 않으므로 Java 컴파일러는 이를 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; }
// ...
}다음 예제에서 일반 Node 클래스는 바운드 타입 매개변수를 사용합니다:
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; }
// ...
}Java 컴파일러는 바인딩된 타입 매개변수 T를 첫 번째 바인딩된 클래스인 Comparable로 대체합니다:
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; }
// ...
}
일반 메서드 삭제
Java 컴파일러는 제네릭 메서드 인수의 타입 매개변수도 지웁니다. 다음 제네릭 메서드를 살펴봅시다:
// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}T는 바인딩되지 않았으므로 Java 컴파일러는 이를 Object로 대체합니다:
public static int count(Object[] anArray, Object elem) {
int cnt = 0;
for (Object e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}다음과 같은 클래스가 정의되어 있다고 가정합니다:
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }일반적인 메서드를 작성하여 다양한 모양을 그릴 수 있습니다:
public static <T extends Shape> void draw(T shape) { /* ... */ }Java 컴파일러는 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 = (MyNode)mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Causes a ClassCastException to be thrown.
Integer x = (String)mn.data; 다음 섹션에서는 n.setData("Hello"); 문에서 ClassCastException이 발생하는 이유에 대해 설명합니다.
브리지 방법
매개변수화된 클래스를 확장하거나 매개변수화된 인터페이스를 구현하는 클래스 또는 인터페이스를 컴파일할 때 컴파일러는 타입 지우기 프로세스의 일부로 브리지 메서드라고 하는 합성 메서드를 생성해야 할 수 있습니다. 일반적으로는 브리지 메서드에 대해 걱정할 필요가 없지만 스택 추적에 나타나면 당황할 수 있습니다.
타입 삭제 후에는 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) 메서드를 재정의하지 않습니다.
이 문제를 해결하고 타입 삭제 후에도 제네릭 타입의 다형성을 유지하기 위해 Java 컴파일러는 서브타입이 예상대로 작동하도록 하는 브리지 메서드를 생성합니다.
MyNode 클래스의 경우 컴파일러는 setData()에 대해 다음과 같은 브리지 메서드를 생성합니다:
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);
}
// ...
}브리지 메서드 MyNode.setData(object)는 원래 MyNode.setData(Integer) 메서드로 위임합니다. 결과적으로 n.setData("Hello"); 문은 MyNode.setData(Object) 메서드를 호출하고, "Hello"를 Integer로 캐스팅할 수 없기 때문에 ClassCastException이 throw됩니다.
Non-Reifiable 타입(재정의 할 수 없는 타입)
컴파일러가 타입 매개변수 및 타입 인자와 관련된 정보를 제거하는 프로세스에 대해 설명했습니다. 타입 삭제는 가변 인자(varargs라고도 함)의 형식 매개변수가 재정의할 수 없는 타입을 가진 가변 인자(varargs) 메서드와 관련된 결과를 가져옵니다. varargs 메서드에 대한 자세한 내용은 메서드 또는 생성자에 정보 전달 시 임의의 인자 수 섹션을 참조하십시오.
이 페이지에서는 다음 주제를 다룹니다:
- 재정의 할 수 없는 타입
- Heap Pollution
- 재정의할 수 없는 형식적 매개변수가 있는 Varargs 메서드의 잠재적 취약성
- 재정의할 수 없는 형식적 매개변수가 있는 Varargs 메서드의 경고 예방하기
재정의할 수 없는 타입은 컴파일 타임에 타입 삭제에 의해 정보가 제거된 타입으로, 바인딩 되지 않은 와일드카드인 일반 타입의 호출입니다. 재정의할 수 없는 타입은 런타임에 모든 정보를 사용할 수 없습니다. 재정의 불가능한 타입의 예로는 List<String>과 List<Number>가 있으며, JVM은 런타임에 이러한 타입의 차이를 구분할 수 없습니다. 제네릭에 대한 제한 섹션에서 살펴본 바와 같이 instanceof 표현식이나 배열의 요소와 같이 재정의할 수 없는 타입을 사용할 수 없는 특정 상황이 있습니다.
Heap Pollution
힙 오염 은 매개변수화된 타입의 변수가 해당 매개변수화된 타입이 아닌 객체를 참조할 때 발생합니다. 이 상황은 프로그램이 컴파일 타임에 확인되지 않은 경고를 발생시키는 일부 연산을 수행한 경우에 발생합니다. 컴파일 타임(컴파일 타임 타입 검사 규칙의 제한 내에서) 또는 런타임에 매개변수화된 타입과 관련된 연산(예: 형 변환 또는 메서드 호출)의 정확성을 확인할 수 없는 경우 확인되지 않은 경고가 생성됩니다. 예를 들어, 원시 타입과 매개변수화된 타입을 혼합하거나 확인되지 않은 형변환을 수행할 때 힙 오염이 발생합니다.
일반적인 상황에서 모든 코드가 동시에 컴파일되면 컴파일러는 잠재적인 힙 오염에 대한 주의를 환기시키기 위해 확인되지 않은 경고를 발행합니다. 코드의 섹션을 개별적으로 컴파일하면 힙 오염의 잠재적 위험을 감지하기 어렵습니다. 코드가 경고 없이 컴파일되는지 확인하면 힙 오염이 발생하지 않을 수 있습니다.
재정의할 수 없는 형식적 매개변수가 있는 vararg 메서드의 잠재적 취약성
vararg 입력 매개변수를 포함하는 일반 메서드는 힙 오염을 일으킬 수 있습니다.
다음 ArrayBuilder 클래스를 생각해 보세요:
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
}
}다음 예제인 HeapPollutionExample은 ArrayBuiler 클래스를 사용합니다:
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() 메서드의 정의에 의해 다음과 같은 경고가 생성됩니다:
warning: [varargs] Possible heap pollution from parameterized vararg type T컴파일러는 varargs 메서드를 만나면 varargs 형식 매개변수를 배열로 변환합니다. 그러나 Java 프로그래밍 언어는 매개변수화된 타입의 배열을 생성하는 것을 허용하지 않습니다. 컴파일러는 ArrayBuilder.addToList() 메서드에서 varargs 형식 매개변수 T... 요소를 배열인 형식 매개변수 T[] 요소로 변환합니다. 그러나 타입 삭제 때문에 컴파일러는 varargs 형식 매개변수를 Object[] 요소로 변환합니다. 따라서 힙 오염이 발생할 가능성이 있습니다.
다음 문은 varargs 형식 매개변수 l을 Object 배열 objectArgs에 할당합니다:
Object[] objectArray = l;이 문은 잠재적으로 힙 오염을 일으킬 수 있습니다. varargs 형식 매개변수 l의 매개변수화된 타입과 일치하는 값은 변수 objectArray에 할당될 수 있으므로 l에 할당될 수 있습니다. 그러나 컴파일러는 이 문에서 확인되지 않은 경고를 생성하지 않습니다. 컴파일러는 이미 varargs 형식 매개변수 List<String>... l을 형식 매개변수 List[] l로 변환할 때 경고를 생성했습니다. 이 문은 유효합니다. 변수 l은 Object[]의 하위 유형인 List[] 타입을 갖기 때문입니다.
따라서 컴파일러는 이 문과 같이 어떤 타입의 List 객체를 objectArray 배열의 배열 구성 요소에 할당해도 경고나 오류를 발생시키지 않습니다:
objectArray[0] = Arrays.asList(42);이 문은 Integer 타입의 객체 하나를 포함하는 List 객체가 있는 objectArray 배열의 첫 번째 배열 구성 요소에 할당합니다.
다음 문과 함께 ArrayBuilder.faultyMethod()를 호출한다고 가정해 보겠습니다:
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));런타임에 JVM은 다음 문에서 ClassCastException을 던집니다:
// ClassCastException thrown here
String s = l[0].get(0);변수 l의 첫 번째 배열 구성 요소에 저장된 객체는 List<Integer> 타입을 갖지만 이 문은 List<String> 타입의 객체를 기대하고 있습니다.
재정의할 수 없는 형식 매개변수가 있는 Varargs 메서드의 경고 방지
매개변수화된 타입의 매개변수가 있는 varargs 메서드를 선언하고 메서드 본문에서 varargs 형식 매개변수의 부적절한 처리로 인해 ClassCastException 또는 기타 유사한 예외가 발생하지 않도록 하는 경우, 정적이고 생성자가 아닌 메서드 선언에 다음 주석을 추가하여 컴파일러가 이러한 종류의 varargs 메서드에 대해 생성하는 경고를 방지할 수 있습니다:
@SafeVarargs이 어노테이션은 메서드 컨트랙트의 문서화된 부분으로, 메서드의 구현이 varargs 형식 매개변수를 부적절하게 처리하지 않을 것임을 보증합니다(@SafeVarargs).
덜 바람직하지만 메서드 선언에 다음을 추가하여 이러한 경고를 억제하는 것도 가능합니다:
@SuppressWarnings({"unchecked", "varargs"})그러나 이 접근 방식은 메서드의 호출 사이트에서 생성된 경고를 억제하지 않습니다. @SuppressWarnings 구문에 익숙하지 않은 경우 어노테이션 섹션을 참조하세요.