제네릭을 사용하는 코드의 이점
- 컴파일 시 강력한 유형 검사: Java 컴파일러는 제네릭 코드에 강력한 유형 검사를 적용하고 코드가 유형 안전을 위반하는 경우 오류를 발생시킨다. 컴파일 타임 오류를 수정하는 것은 찾기 어려운 런타임 오류를 수정하는 것보다 쉽다.
- 형 변환 제거
제너릭 타입 파라미터 네이밍 컨벤션
규칙에 따라 유형 매개변수 이름은 대문자로 된 단일 문자다. 이는 변수 명명 규칙과 뚜렷한 대조를 이루며, 이 규칙이 없으면 유형 변수와 일반 클래스 또는 인터페이스 이름을 구분하기 어려울 수 있다.
가장 일반적으로 사용되는 유형 매개변수 이름은 다음과 같다.
E: 엘리먼트(Java 컬렉션 프레임워크에서 광범위하게 사용됨)K: 키N: 숫자T: 유형V: 값S,U,V등: 2, 3, 4번째 유형
일반 유형 호출 및 인스턴스화
일반 타입 호출은 일반적인 메서드 호출과 비슷하다고 생각할 수 있지만, 메서드에 인수를 전달하는 대신 타입 인자를 클래스 자체에 전달한다.
타입 매개변수와 타입 인자 용어: 많은 개발자가 '타입 매개변수'와 '타입 인자'라는 용어를 혼용하여 사용하지만 이 용어는 동일하지 않다. 코딩할 때 매개변수화된 타입을 생성하기 위해 타입 인수를 제공한다. 따라서 Foo<T>의 T는 타입 매개변수이고 Foo<String> Foo의 String은 타입 인자다.
제네릭 타입의 호출은 일반적으로 매개변수화된 타입으로 알려져 있다. 이 클래스를 인스턴스화하려면 평소와 같이 new 키워드를 사용하되 클래스 이름과 괄호 사이에 <Integer>를 배치한다.
다이아몬드
Java SE 7 이상에서는 컴파일러가 컨텍스트에서 유형 인수를 확인하거나 유추할 수 있는 한, 제네릭 클래스의 생성자를 호출하는 데 필요한 유형 인수를 빈 유형 인수 집합(<>)으로 대체할 수 있다. 이 꺾쇠괄호 쌍인 <>은 비공식적으로 다이아몬드라고 한다.
로우 타입
로우 타입은 유형 인수가 없는 제네릭 클래스 또는 인터페이스의 이름이다.
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
Box<T>의 매개변수화된 유형을 만들려면 형식적 유형 매개변수 T에 대한 실제 유형 인수를 제공한다.
Box<Integer> intBox = new Box<>();
실제 타입 인수가 생략된 경우, Box<T>의 원시 타입을 생성한다.
Box rawBox = new Box();
Box는 일반 유형 Box<T>의 로우 타입이다. 그러나 제네릭이 아닌 클래스 또는 인터페이스 유형은 로우 타입이 아니다.
로우 타입이 레거시 코드에 표시되는 이유는 JDK 5.0 이전에는 많은 API 클래스(예: Collections 클래스)가 제네릭이 아니었기 때문이다. 로우 타입을 사용하면 기본적으로 제네릭 이전 동작이 발생한다. 이전 버전과의 호환성을 위해 매개변수화된 유형을 원시 유형에 할당하는 것은 허용된다. 그러나 매개변수화된 유형에 로우 타입을 할당하면 경고가 표시된다.
Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion
로우 타입을 사용하여 해당 제네릭 유형에 정의된 제네릭 메서드를 호출하는 경우에도 경고가 표시된다.
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)
이 경고는 로우 타입이 일반 유형 검사를 우회하여 안전하지 않은 코드의 포착을 런타임으로 연기한다는 것을 보여준다. 따라서 원시 유형을 사용하지 않아야 한다.
바운드 타입 매개변수
매개변수화된 타입에서 타입 인수로 사용할 수 있는 타입을 제한하고 싶은 경우가 있을 수 있다. 예를 들어 숫자에 대해 연산하는 메서드는 Number 또는 그 하위 클래스의 인스턴스만 허용하고 싶을 수 있다. 이것이 바로 바운드 타입 매개변수의 용도다.
바운드 타입 매개변수를 선언하려면 타입 매개변수의 이름과 extends 키워드, 그리고 그 상한(이 예에서는 Number)을 나열한다. 이 문맥에서 extends는 일반적인 의미로 extends(클래스에서처럼) 또는 implements(인터페이스에서처럼)이라는 의미로 사용된다.
멀티 바운드 타입
타입 매개변수에는 여러 바운드가 있을 수 있다.
<T extends B1 & B2 & B3>
바운드가 여러 개인 타입 변수는 바운드에 나열된 모든 타입의 하위 타입이다. 바운드 중 하나가 클래스인 경우 클래스를 먼저 지정해야 한다.
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
class D <T extends B & A & C> { /* ... */ } // compile-time error
제너릭, 상속 그리고 서브타입
유형이 호환되는 경우 한 유형의 객체를 다른 유형의 객체에 할당할 수 있다. 예를 들어, 객체는 정수의 상위 유형 중 하나이므로 정수를 객체에 할당할 수 있다.
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
객체 지향 용어로는 이를 is a 관계라고 한다. Integer는 객체의 일종이므로 대입이 허용된다. 그러나 Integer도 일종의 숫자이므로 다음 코드도 유효하다.
public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
제네릭도 마찬가지다. 제네릭 타입 호출을 수행하여 타입 인자로 Number를 전달할 수 있으며, 인수가 Number와 호환되는 경우 이후의 add 호출이 허용된다.
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
public void boxTest(Box<Number> n) { /* ... */ }
시그니처를 보면 유형이 Box<Number>인 단일 인수를 받는다는 것을 알 수 있다. 하지만 이것이 무엇을 의미할까? 예상할 수 있는 것처럼 Box<Integer>나 Box<Double>을 전달할 수 있을까? 대답은 "아니오"이다. Box<Integer>와 Box<Double>은 Box<Number>의 하위 유형이 아니기 때문이다.
이는 제네릭을 사용한 프로그래밍에 관해 흔히 오해하는 부분이지만, 반드시 알아두어야 할 중요한 개념이다. Integer가 Number의 하위 유형이긴 하지만 Box<Integer>는 Box<Number>의 하위 유형이 아니다.
제네릭 클래스 및 하위 유형
제네릭 클래스나 인터페이스를 확장하거나 구현하여 하위 유형화할 수 있다. 한 클래스 또는 인터페이스의 형 매개변수와 다른 클래스의 형 매개변수 사이의 관계는 extends 및 implements 절에 의해 결정된다.
Collections 클래스를 예로 들어보면, ArrayList<E>는 List<E>를 구현하고 List<E>는 Collection<E>를 확장한다. 따라서 ArrayList<String>은 Collection<String>의 하위 유형인 List<String>의 하위 유형이다. 타입 인수를 변경하지 않는 한, 타입 간의 서브타입 관계는 유지된다.

상한 경계 와일드카드
상한 경계 와일드카드를 사용하면 변수에 대한 제한을 완화할 수 있다. 예를 들어, List<Integer>, List<Double>, List<Number>에 모두 작동하는 메서드를 작성하고 싶다면, 상한 경계 와일드카드를 사용하여 이를 달성할 수 있다.
상한 경계 와일드카드를 선언하려면, 와일드카드 문자(?) 뒤에 extends 키워드를 쓰고, 그 뒤에 상한 타입을 지정한다. 이때 extends는 일반적인 의미로 사용되며, 클래스에서는 extends을, 인터페이스에서는 implements을 의미한다.
Number 및 Number의 하위 타입들(예: Integer, Double, Float)로 이루어진 리스트에 대해 작동하는 메서드를 작성하려면, List<? extends Number>로 지정해야 한다. List<Number>는 Number 타입의 리스트만 일치시키므로 List<? extends Number>보다 더 제한적이다. 반면에 List<? extends Number>는 Number 또는 그 하위 클래스 타입의 리스트에 모두 일치한다.
제한 없는 와일드 카드
제한이 없는 와일드카드 타입은 와일드카드 문자 ?를 사용하여 지정하며, 예를 들어 List<?>와 같이 사용한다. 이는 "알 수 없는 타입의 리스트"라고 불린다. 제한 없는 와일드카드는 다음 두 가지 경우에 유용하게 사용된다.
Object클래스에서 제공하는 기능만으로 메서드를 구현할 수 있을 때- 제네릭 클래스의 타입 매개변수(
T)에 의존하지 않는 메서드를 사용할 때
예를 들어,List.size()나List.clear()와 같은 메서드는 타입 매개변수와 무관하게 동작한다. 실제로Class<?>는 매우 자주 사용되는데, 이는Class<T>의 대부분의 메서드가 타입T에 의존하지 않기 때문이다.
하한 경계 와일드카드
하한 경계 와일드카드는 알 수 없는 타입을 특정 타입 또는 그 상위 타입으로 제한한다. 하한 경계 와일드카드는 와일드카드 문자(?) 뒤에 super 키워드를 붙이고, 그 뒤에 하한 타입을 명시하여 표현한다. <? super A>
예를 들어, Integer 객체를 리스트에 추가하는 메서드를 작성하고 싶다고 가정해 보자. 유연성을 극대화하기 위해, 이 메서드는 List<Integer>, List<Number>, List<Object>, 즉 Integer 값을 담을 수 있는 어떤 리스트에도 작동해야 한다.
Integer와 그 상위 타입들(예: Number, Object)로 이루어진 리스트에 대해 작동하는 메서드를 작성하려면 List<? super Integer>로 지정해야 한다. List<Integer>는 오직 Integer 타입 리스트만 일치시키기 때문에 List<? super Integer>보다 더 제한적이다. 반면에 List<? super Integer>는 Integer의 상위 타입이 될 수 있는 어떤 리스트와도 일치한다.
와일드카드와 서브타이핑
제네릭 클래스나 인터페이스는 해당 타입들 간에 관계가 있다고 해서 자동으로 서로 관련이 있는 것이 아니다. 그러나 와일드카드를 사용하면 제네릭 클래스나 인터페이스 간에 관계를 만들어낼 수 있다.
class A { /* ... */ }
class B extends A { /* ... */ }
클래스 B가 클래스 A를 상속하는 B는 A의 서브타입이다. 하지만 이는 제네릭 타입에는 적용되지 않는다.
List<B> lb = new ArrayList<>();
List<A> la = lb; // 컴파일 타임 오류
Integer가 Number의 서브타입이라는 점을 고려했을 때, List<Integer>와 List<Number> 사이에는 어떤 관계가 있을까?
공통 부모 제네릭 리스트
비록 Integer는 Number의 서브타입이지만, List<Integer>는 List<Number>의 서브타입이 아니며, 실제로 이 둘은 아무런 관련이 없다. List<Number>와 List<Integer>의 공통 부모는 List<?>다.

List<? extends Integer> intList = new ArrayList<>();
// This is OK because List<? extends Integer> is a subtype of List<? extends Number>
List<? extends Number> numList = intList;
Integer는 Number의 하위 타입이므로, numList가 Number 객체들의 리스트이고 intList가 Integer 객체들의 리스트일 때, 이 둘 사이에는 관계가 성립하게 된다. 아래 다이어그램은 상한 및 하한 경계 와일드카드를 사용하여 선언된 여러 List 클래스들 간의 관계를 보여준다.

와일드카드 캡처와 헬퍼 메서드
일부 경우 컴파일러는 와일드카드의 타입을 추론한다. 예를 들어, 리스트가 List<?>로 정의되어 있어도, 어떤 표현식을 평가할 때 컴파일러는 코드로부터 특정 타입을 추론할 수 있다. 이러한 상황을 와일드카드 캡처라고 한다. 대부분의 경우 와일드카드 캡처에 대해 신경 쓸 필요는 없지만, 컴파일 에러 메시지에 "capture of"라는 문구가 나타난다면 관련이 있을 수 있다.
import java.util.List;
public class WildcardError {
void foo(List<?> i) {
i.set(0, i.get(0)); // 컴파일 에러 발생
}
}
이 예제에서, 컴파일러는 i를 Object 타입으로 처리한다. 그러나 List.set(int, E) 메서드는 리스트에 어떤 타입의 객체가 들어오는지 명확해야 하므로, 컴파일러는 이 타입이 무엇인지 확인할 수 없어 에러가 발생한다. 이와 같은 컴파일 오류는 잘못된 타입을 변수에 할당하려고 한다고 컴파일러가 판단할 때 발생한다. Java가 제네릭을 도입한 이유도 바로 이러한 타입 안정성을 컴파일 타임에 보장하기 위함이다.
public class WildcardFixed {
void foo(List<?> i) {
fooHelper(i);
}
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0)); // 타입 추론으로 컴파일 성공
}
}
위 예제처럼 헬퍼 메서드를 만들어주면 컴파일러가 와일드카드의 타입을 추론할 수 있으므로 컴파일이 성공한다.
public class WildcardErrorBad {
void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
Number temp = l1.get(0);
l1.set(0, l2.get(0)); // 서로 다른 캡처 타입이므로 컴파일 에러
l2.set(0, temp); // Number를 넣으려 했지만 캡처 타입에 불일치
}
}
List<Integer> li = Arrays.asList(1, 2, 3);
List<Double> ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld); // 위험한 코드
List<Integer>와 List<Double> 모두 List<? extends Number>에 해당하지만, Integer 값을 Double 리스트에 넣거나 그 반대는 타입 안전하지 않기 때문에 컴파일러가 에러를 발생한다. 이와 같은 경우 문제의 본질이 잘못되었기 때문에 이를 해결할 수 있는 헬퍼 메서드는 존재하지 않는다. Integer 값들의 리스트에서 항목을 꺼내 Double 값들의 리스트에 넣으려는 것은 명백히 잘못된 동작이다.
와일드카드 사용 지침
제네릭을 사용할 때, extends, super, 또는 아무 제약도 없는 ? 를 언제 사용할지 헷갈릴 수 있다. 이를 해결하기 위해 in과 out의 관점에서 바라보면 도움이 된다.
in관점:extends는 코드 내부에서 데이터를 읽기 전용으로 사용할 때 적합하다.in변수지만Object클래스에서 정의된 메서드만 사용할 경우에는?를 사용하는 것이 적합하다.
out관점:super는 코드 내부에서 데이처를 쓰기 전용으로 사용할 때 적합하다.
이는 메서드의 반환 타입에는 적용되지 않는다. 와일드카드를 반환 타입으로 사용하는 것은 피해야 한다. 왜냐하면 이 경우 코드를 사용하는 프로그래머가 와일드카드를 처리해야 하기 때문이다.
List<? extends ...>로 정의된 리스트는 비공식적으로는 읽기 전용으로 간주될 수 있지만, 이는 엄격한 의미의 보장은 아니다.
class NaturalNumber {
private int i;
public NaturalNumber(int i) { this.i = i; }
// ...
}
class EvenNumber extends NaturalNumber {
public EvenNumber(int i) { super(i); }
// ...
}
List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35)); // 컴파일 타임 오류
List<EvenNumber>는 List<? extends NaturalNumber>의 하위 타입이므로, le를 ln에 할당할 수 있다. 그러나 ln을 사용하여 자연수를 짝수 리스트에 추가할 수는 없다.
다음 작업은 리스트에서 수행할 수 있다.
null을 추가할 수 있다.clear()를 호출할 수 있다.- 반복자를 얻어
remove()를 호출할 수 있다. - 와일드카드를 캡처하고 리스트에서 읽은 요소를 쓸 수 있다.
List<? extends NaturalNumber>로 정의된 리스트는 가장 엄격한 의미에서 읽기 전용은 아니지만, 새 요소를 저장하거나 기존 요소를 변경할 수 없기 때문에 읽기 전용처럼 느껴질 수 있다.
제너릭 타입 소거
제네릭은 컴파일 시간에 더 엄격한 타입 검사를 제공하고 제네릭 프로그래밍을 지원하기 위해 Java 언어에 도입되었다. 제네릭을 구현하기 위해 Java 컴파일러는 타입 소거를 적용한다.
- 제네릭 타입의 모든 타입 매개변수는, 타입에 지정된 상한이 있을 경우 그 상한으로, 그렇지 않은 경우에는
Object로 대체한다. 따라서 생성된 바이트코드는 일반 클래스, 인터페이스, 메서드만을 포함하게 된다. - 타입 안전성을 유지하기 위해 필요한 경우 타입 캐스트를 삽입한다.
- 확장된 제네릭 타입에서 다형성을 유지하기 위해 브리지 메서드를 생성한다.
타입 소거는 매개변수화된 타입에 대해 새로운 클래스가 생성되지 않도록 보장하므로, 제네릭은 런타임 오버헤드를 발생시키지 않는다. 타입 소거 과정에서 Java 컴파일러는 모든 타입 매개변수를 제거하고, 각 매개변수를 제한이 지정된 경우 첫 번째 제한으로, 제한이 없는 경우에는 Object로 대체한다.
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; }
// ...
}
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; }
// ...
}
상한만 남기는 경우
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; }
// ...
}
브리지 메서드
제네릭을 사용한 클래스를 상속하거나 인터페이스를 구현하는 클래스를 컴파일할 때, 컴파일러는 타입 소거 과정의 일환으로 합성 메서드를 생성해야 할 수도 있다. 이 메서드를 브리지 메서드라고 부른다. 일반적으로 개발자가 브리지 메서드에 대해 걱정할 필요는 없지만, 스택 트레이스에 등장하면 당황스러울 수 있다. 타입 소거 이후, Node와 MyNode 클래스는 다음과 같이 변환된다.
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);
}
}
타입 소거 이후, 메서드 시그니처가 일치하지 않게 된다. Node.setData(T) 메서드는 Node.setData(Object)로 바뀌고, MyNode.setData(Integer)는 이 메서드를 오버라이드하지 않게 된다. 이 문제를 해결하고 타입 소거 이후에도 제네릭 타입의 다형성을 유지하기 위해, 자바 컴파일러는 브리지 메서드를 생성한다. 이는 하위 타입 관계가 기대한 대로 작동하도록 보장한다.
MyNode 클래스에 대해, 컴파일러는 다음과 같은 setData() 브리지 메서드를 생성한다.
class MyNode extends Node {
// 컴파일러가 생성한 브리지 메서드
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이 발생한다.
소거 가능한 타입
소거 가능한 타입은 런타임에 타입 정보가 완전히 유지되는 타입이다. 예를 들어 다음이 이에 해당한다.
- 기본 타입 (
int,double등) - 제네릭이 아닌 타입
- raw 타입 (
List,Map등) - 제약이 없는 와일드카드 타입 (
List<?>등)
소거 불가능 타입
소거 불가능 타입은 컴파일 시 타입 소거에 의해 일부 또는 전부의 타입 정보가 제거된 타입이다. 즉, 구체적인 타입 인자를 가지는 제네릭 타입으로, 다음과 같은 경우가 이에 해당한다.
List<String>List<Number>Map<String, Integer>
이러한 타입은 런타임에 JVM이 그 구체적인 타입 정보를 알 수 없다.
예를 들어, JVM은 List<String>과 List<Number>의 차이를 구분할 수 없다.
비 실체화 가능한 형식 매개변수를 가진 가변 인자(varargs) 메서드의 잠재적 취약점
자바 컴파일러는 varargs 메서드를 처리할 때, 가변 인자 매개변수를 배열로 변환한다. 하지만 자바 언어는 제네릭 타입의 배열 생성을 허용하지 않는다. 즉, T... elements는 내부적으로 T[] elements로 번역되지만, 타입 소거로 인해 실제로는 Object[] elements로 처리된다. 이로 인해 힙 오염 가능성이 생긴다.
제너릭 타입의 제한
원시 타입으로 제너릭 타입을 인스턴스화 할 수 없다
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<Integer, Character> p = new Pair<>(8, 'a'); // OK
Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a')); // OK
타입 파라미터로 인스턴스를 만들 수 없다
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);
}
List<String> ls = new ArrayList<>();
append(ls, String.class);
타입이 타입 파라미터인정적 필드를 선언할 수 없다
public class MobileDevice<T> {
private static T os;
// ...
}
매개변수 유형의 정적 필드가 허용되면 다음 코드가 혼동될 수 있다.
MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();
정적 필드 OS는 스마트폰, 호출기, PC에서 공유하는데 실제 OS의 종류는 무엇일까? 스마트폰, 호출기, 태블릿PC는 동시에 스마트폰, 호출기, 태블릿PC일 수 없습니다. 따라서 파라미터 유형의 정적 필드를 만들 수 없다.
매개변수화된 유형 cast 혹은 instanceof 사용 불가하다
Java 컴파일러는 일반 코드의 모든 유형 매개 변수를 지우기 때문에 런타임에 일반 유형에 대해 어떤 매개 변수화된 유형이 사용되고 있는지 확인할 수 없다.
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
// ...
}
}
rtti() 메서드에 전달되는 매개변수화된 유형 집합은 다음과 같다.
S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }
런타임은 유형 매개변수를 추적하지 않으므로 ArrayList<Integer>와 ArrayList<String>의 차이를 구분할 수 없다. 할 수 있는 최선은 제한되지 않은 와일드카드를 사용하여 목록이 ArrayList인지 확인하는 것이다.
public static void rtti(List<?> list) {
if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type
// ...
}
}
일반적으로 매개변수화된 유형은 바인딩되지 않은 와일드카드로 매개변수화되지 않는 한 형변환할 수 없다.
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // compile-time error
그러나 컴파일러가 유형 매개변수가 항상 유효하다는 것을 알고 형 변환을 허용하는 경우도 있다.
List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1; // OK
매개변수화된 타입의 배열을 만들 수 없다
매개변수화된 유형의 배열은 만들 수 없다. 예를 들어 다음 코드는 컴파일되지 않는다.
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.
각 오버로드의 형식적 매개변수 유형이 동일한 원시 유형으로 지워지는 메서드를 오버로드할 수 없다.
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}
클래스에는 타입 삭제 후 동일한 서명을 갖는 오버로드된 메서드가 두 개 있을 수 없다. 오버로드는 모두 동일한 클래스 파일 표현을 공유하며 컴파일 타임 오류를 생성한다.
'자바' 카테고리의 다른 글
| The Stream API 정리 (2) | 2025.06.13 |
|---|---|
| The Collections Framework 정리 (3) | 2025.06.13 |
| NotNull과 NonNull (0) | 2025.06.10 |
| NullAway 정리 (1) | 2025.06.05 |
| withContext (0) | 2025.05.26 |