JDK 1.5에 처음 도입되었으며, 제네릭은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다.(생활코딩 )
publicclassFruit<T> {publicT fruit;}
Fruit<Apple> apple =newFruit<>();Fruit<Banana> apple =newFruit<>();
위의 예제를 보면 Fruit 인스턴스를 생성할 때, Apple, Banana 를 넣어 타입을 지정하고 있다. 즉, 클래스를 정의 할 때는 어떤 타입이 들어올지 확정하지 않고, 인스턴스를 생성할 떄 데이터 타입을 지정하는 기능이다.
왜 제네릭을 사용할까?
Generic은 클래스와 인터페이스, 메서드를 정의할 때 타입을 파라미터로 사용할 수 있게 한다. Generic 타입을 이용함으로써 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있게되었다. 타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해준다.
컴파일 시 강한 타입 체크를 할 수 있다.
컴파일 언어의 기본은 모든 에러는 컴파일이 발생할 수 있도록 하는 것이 좋다.(오류는 빨리 발견할 수록 좋다.) 런타임은 실제로 애플리케이션이 동작하고 있는 상황이기 때문에 런타임에 발생하는 에러는 항상 심각한 문제를 초래할 수 있기 때문이다.
Generic type으로 변경 후에는 java: incompatible types: java.lang.String cannot be converted to int 컴파일 오류가 발생하는 것을 확인할 수 있다.
즉, 컴파일 단계에서 오류 검출이 가능하며, 타입 안정성을 추구할 수 있게된다.
타입 변환(casting)을 제거한다.
// generic이 아닌경우List list =newArrayList();list.add("hello");String str = (String)list.get(0); // 타입 변환이 필요// genericList<String> list =newArrayList<String>();list.add("hello");String str =list.get(0); // 타입 변환을 하지 않음
Generic Type
Generic Type은 **타입을 파라미터로 가지는 클래스(class<T>)와 인터페이스(interface<T>)**를 말한다.
위의 코드에서 클래스 필드 타입을 Object로 선언한 이유는 필드에 모든 종류의 객체를 저장하고 싶어서이다. Object는 모든 자바 클래스의 최상위 조상(부모) 클래스이다. 자식 객체는 부모타입에 대입할 수 있기 때문에 모든 자바 객체는 Object타입으로 자동 타입변환되어 저장된다.
Box box =newBox();box.set("안녕"); // String 타입을 Object타입으로 자동타입 변환String str = (String) box.get(); // Object 타입을 String타입으로 강제 타입 변환
다음과 같이 get으로 가져오기위해서는 강제 타입 변환이 필요하다.
Object 타입을 사용하면 모든 종류의 자바 객체를 저장할 수 있다는 장점은 있지만, 저장할 때와 읽어올 때 타입 변환이 발생하며, 잦은 타입 변환은 전체 프로그램 성능에 좋지 못한 결과를 가져올 수 있다.
여기서 T는 클래스로 객체를 생성할 때 구체적인 타입으로 변경된다. 그렇기 때문에 저장할 때와 읽어올 때 타입 변환이 발생하지 않는다. 이와 같이 generic은 클래스 설계시 구체적은 타입을 명시하지 않고, 타입 파라미터로 대체했다가 실제 클래스가 사용될 때 구체적인 타입을 지정함으로써 타입 변환을 최소화시킨다.
타입 파라미터
타입 파라미터의 이름은 짓기 나름이지만, 일반적으로 대문자 한글자로 표현 보편적으로 자주 사용하는 타입 매개변수는 다음과 같다.
다이아몬드 <>
제네릭 타입 변수 선언과 객체 생성을 동시에 할 때 타입 파라미터에 구체적인 타입을 지정하는 코드가 중복될 수 있다. 그렇기 때문에 자바7에서 부터는 <> (다이아몬드연산자)를 제공한다. 자바 컴파일러는 타입 파라미터 부분에 <>연산자를 사용하면 타입 파라미터를 유추해서 자동으로 설정해준다.
// java7이후Box<String> box =newBox<>();
다중 타입 파라미터(Multiple Type Parameters)
Generic Type은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있다. 이 경우 각 타입 파라미터를 콤마로 구분한다.
위 경고는 raw type이 generic type 검사를 생략해 안전하지 않은 코드가 런타임시에 발견될 수도 있다는 경고 문구이다. 그러므로, raw type은 최대한 사용하지 않는 것이 좋으며, 자세한 내용은 **[effective java - ITEM 26]**에서 확인할 수 있다.
타입 매개변수 <T> 는 반드시 메서드의 수식자(public, static)와 반환형 사이에 위치되어야한다.
Generic 메소드는 다음과 같이 호출될 수 있다.
//명시적으로 구체적 타입을 지정리턴타입 변수 =<구체적타입> 메소드명(매개값);Box<Integer> box =<Integer>boxing(100);//매개값을 보고 구체적 타입을 추정리턴타입 변수 = 메소드명(매개값);Box<Integer> box =boxing(100);
Pair<Integer,String> p1 =newPair<>(1,"apple");Pair<Integer,String> p2 =newPair<>(2,"pear");boolean same = Util.<Integer, String>compare(p1, p2);
Util.<Integer, String>compare(p1, p2) 과 같이 타입을 명시적으로 썼지만, 다음과 같이 생략해도 컴파일러가 유추할 수 있다.
boolean same =Util.compare(p1, p2);
제한된 타입 파라미터(<T extends 최상위타입>)
타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 종종 있다. 숫자를 연산하는 제네릭 메소드의 매개값으로는 Number타입 또는 그 하위 클래스 타입(Integer, Double, Long, Short, ...) 의 인스턴스만 가져야한다. 이러한 이유로 제한된 타입 파라미터가 필요한 경우가 있다.
public<T extends 상위타입> 리턴타입 메소드(매개변수,...){...}
상위 타입은 클래스뿐만 인터페이스도 가능하다. 하지만 인터페이스라고 해서 implements를 사용하지 않는다.( extends 사용)
여기서 주의할 점은 함수 내에서 타입 파라미터 변수로 사용한 것은 상위 타입의 멤버(필드, 메서드)로 제한된다는 점이다. doubleValue()는 Number 클래스의 메서드이기때문에 사용할 수 있는 것이다.
Muliple Bounds
제한된 타입 파라미터를 여러개로 설정할 수 있다.
<T extends B1 & B2 & B3>
Class A { /* ... */ }interfaceB { /* ... */ }interfaceC { /* ... */ }
classD <TextendsA&B&C> { /* ... */ }
여기서 D 클래스의 타입파라미터는 A, B, C 모든 유형의 하위 클래스여야하며, 이중 한개가 클래스인 경우 반드시 먼저 선언되어야한다.
classD <TextendsB&A&C> { /* ... */ }
위와 같이 A 가 클래스이지만, 인터페이스인 B 보다 늦게 선언된다면 컴파일 오류가 발생한다.
Generic Method와 Bounded Type Parameter
제한된 타입 파라미터(Bounded Type Parameter)는 제네릭 알고리즘을 구현할때 핵심이된다.
// 두번째 인자(elem)보다 큰 값이 anArray에 몇개가 있는지 세는 메서드publicstatic<T>intcountGreaterThan(T[] anArray,T elem) {int count =0;for (T e : anArray)if (e > elem) // compiler error++count;return count;}
위 메서드는 컴파일 오류가 발생한다.
java: bad operand types for binary operator '>' first type:T second type:T
> 연산자는 기본형(int, short, double, long ...)에만 동작이 허용되기 때문이다. 즉, > 연산자는 객체간 비교에 사용할 수 없으며, Comparable 인터페이스를 사용해 해당 오류를 해결할 수 있다.
Object someObject =newObject();Integer someInteger =newInteger(10);someObject = someInteger; // OK
타입간 호환이된다면, 특정 타입의 객체를 다른 타입에 할당이 가능하다. Object 는 Integer의 상위 클래스이기 때문에 할당이 가능하다. 객체지향 이론에서는 이러한 경우를 "is a" 관계라고 부른다. "Integer is a Object" 이므로 Object에 Integer가 할당이 가능한 것이다.
또한, "Integer is a Number" 이므로 위 예제 코드 또한 잘 작동하는 것을 볼 수 있다.
이러한 규칙은 제네릭에서도 똑같이 적용된다. 제네릭 타입 호출시, 타입 인자가 "is a" 관계라면 타입 인자로 전달할 수 있는 것이다.
Box<Number> box =newBox<Number>();box.add(newInteger(10)); // OKbox.add(newDouble(10.1)); // OK
여기서 주의해야할 부분이 있다.
publicvoidboxTest(Box<Number> n) { /* ... */ }
boxTest() 메서드의 인자로 어떤 타입을 받을 수 있을까? 대부분 Box<Integer> 와 Box<Double> 이 전달 가능할거라고 생각할 것이다. 하지만, Box<Integer>와 Box<Double> 은 Box<Number> 의 서브타입이 아니기 때문에 인자값으로 전달 할 수 없다.
Generic Classes and Subtyping
제네릭 클래스 상속 또는 제네릭 인터페이스 구현시, 두 클래스간 "is a" 관계를 만들 수 있다.
예를 들어, Collection 클래스를 사용할 때, ArrayList<E> 는 List<E> 를 구현하고, List<E>는 Collection<E> 를 상속 받고 있는 것을 볼 수 있다.
위 예제에서 제네릭 메서드 addBox() 는 타입 매개변수(U)가 선언되어있다. 일반적으로 컴파일러는 해당 제네릭 메서드를 호출하는 곳을 보고 타입 파라미터를 추론할 수 있다. 제네릭 메서드 addBox 를 호출할 때, 구체적인 타입 매개변수를 주지 않아도 컴파일러가 자동으로 값을 추론할 수 있다.
제네릭 클래스의 생성자에 타입 매개변수화한 생성자 대신 아래처럼 <> 만 사용하여 선언할 수 있다.
Map<String,List<String>> myMap =newHashMap<>();
하지만, <> 연산자를 선언하지 않으면, raw 타입이므로 타입추론을 하지 않기때문에 주의해야한다
Map<String, List<String>> myMap = new HashMap(); // raw type
Type Inference and Generic Constructors of Generic and Non-Generic Classes
제네릭 클래스와 비제네릭 클래스(non-generic) 모두 제네릭 생성자를 선언할 수 있다.
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
}
new MyClass<Integer>("");
위 코드는 매개변수화된 타입 MyClass의 인스턴스를 생성한다. 제네릭 클래스인 MyClass의의 형식 매개변수 X에 대해 Integer 타입을 명시적으로 지정하고 있다. 이 제네릭 클래스의 생성자는 형식 매개 변수 T를 포함하고 있으며, 컴파일러는 이 제네릭 클래스(MyClass)의 생성자의 파라미터 타입이 String인 것을 추론할 수 있다. 자바 SE 7 이전의 컴파일러는 제네릭 메서드와 유사한 제네릭 생성자의 실제 타입 파라미터를 추론할 수 있으며, 자바 SE7 이상 컴파일러는 <> 다이아몬드 연산자를 사용하면, 제네릭 클래스의 실제 타입 파라미터를 추론할 수 있다.
MyClass<Integer> myObject = new MyClass<>(""); // JavaSE7 이후
위 예시에서 컴파일러는 제네릭 클래스 MyClass<X>의 타입 파라미터가 Integer 임을 추론할 수 있고, 이 제네릭 클래스의 생성자의 타입 파라미터 T가 String 임을 추론할 수 있다.
Target Types
타겟 타입 표현식은 자바 컴파일러가 예상하는 데이터 타입이다.
@SuppressWarnings("unchecked")
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
List<String> list = Collections.emptyList();
위 선언문은 리턴타입을 List<String> 타입으로 받고 있기때문에 List<String> 를 예상할 수 있다. 이 데이터 타입을 바로 target type이라 한다. emptyList() 메서드가 List<T> 를 반환하기때문에 컴파일러는 T가 String 이라고 추론할 수 있다.
List<String> list = Collections.<String>emptyList();
물론 위와 같이 T 타입을 명시 할 수 있지만, 문맥상 String 인것이 명백하기 때문에 적어주지 않아도 된다. 명시적으로 타입을 선언해줘야하는 경우를 살펴보자.(Type witness)
void processStringList(List<String> stringList) {
// process stringList
}
processStringList(Collections.emptyList());
위 예제는 Java SE7에서 컴파일 되지 않으며, List<Object> cannot be converted to List<String> 오류가 발생한다. 컴파일러는 T의 타입 인자가 필요하지만, 아무것도 주어지지 않았기 때문에 Object를 타입 인자로 갖게된다. Collections.emptyList() 는 List<Object> 객체를 반환하기때문에 컴파일 오류를 발생시키는 것이다. Java SE7에서는 반드시 타입값을 명시해야한다.
하지만, Java SE8 이상부터는 타겟 타입을 결정할 떄 메서드의 인자 값도 살피도록 확장되었으므로, 명시해줄 필요가 없어졌다. 즉, Collections.emptyList() 의 반환 값인 List<T>가 List<String> 인게 추론이 가능해졌기 때문에 Java SE8부터는 아래 예시도 컴파일 된다.
List<String> list = Collections.emptyList();
Wildcards (와일드카드)
제네릭에서 unkwon타입을 표현하는 ?를 일반적으로 와일드카드라고 부른다. 와일드카드는 파라미터, 필드, 직역변수 타입, 리턴타입 등 다양한 상황에서 쓰이며, 제네릭 메서드 호출, 제네릭 클래스 인스턴스 생성, 상위 타입(super type)의 타입 인자로는 사용되지 않는다.
public class Couse<T>{
private String name;
private T[] students;
public Course(String name, int capacity){
this.name = name;
// 타입 파라미터로 배열을 생성하려면 new T[n]형태가 아닌 (T[])(new T[n])의 형태로 생성해야한다.
students = (T[])(new Object[capacity]);
}
public String getName(){ return name; }
public T[] getStudents(){ return students; }
public void add(T t){
for(int i=0;i<students.length;i++){
if(students[i] == null){
students[i]=t;
break;
}
}
}
}
수강생이 될 수 있는 타입이 아래와 같다.
Person
Worker
Student
HighStudent
Course<?> : 수강생은 모든 타입(Person, Worker, Student, HightStudent)
Course<? extends Students> : 수강생는 Student와 HighStudent만 가능
Course<? super Worker> : Worker, Person만 가능
Unbounded Wildcards
List<?> 와 같이 ? 의 형태로 정의되며, 비한정적 와일드카드 타입이 사용될 수 있는 시나리오는 다음과 같다.
Object 클래스에서 제공되는 기능을 사용하여 구현할 수 있는 메서드를 작성하는 경우
타입 파라미터에 의존적이지 않은 일반 클래스의 메소드를 사용하는 경우( ex) List.clear, List.size, Class<?>)
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
위 메서드의 목표는 어떠한 타입의 리스트가 오더라도 그 요소를 출력하는 것이다. 하지만, 위 예제는 한가지 문제점이 있다.
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
java: incompatible types: java.util.List<java.lang.Integer> cannot be converted to java.util.List<java.lang.Object>
List<Object> 외의 List<Integer>, List<String>, List<Double>의 출력은 java: incompatible types: 컴파일 오류가 발생하며 실패한다. 왜나하면 List<Object>의 하위타입이 아니기 때문이다.
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
비한정적 와일드카드 타입을 사용한다면, 성공적으로 출력되는 것을 알 수 있다. 왜나하면, 어떠한 타입 A가 와도 List<A>는 List<?>의 하위 타입이기 때문이다.
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
비한정적 와일드 카드는 어떠한 타입이 와도 읽을 수 있도록, 모든 타입의 공통 조상인 Object 로 받는다.
여기서 주의할 점은 List<Object> 와 List<?>이 같지 않으며, List<Object>에는 Object 의 하위 타입은 모두 넣을 수 있지만, List<?> 에는 오직 null만 넣을 수 있다. 왜나하면 비경계 와일드 카드의 원소가 어떠한 타입이 들어오는 지 알 수 없으므로, 타입 안정성을 지키기 위해 null만 추가할 수 있다.
List<?> list = new ArrayList<>();
list.add(null); // 가능
list.add("test"); // 컴파일 오류
만약 다음과 같이 모든 타입을 넣을 수 있게 한다면, List<Integer>에 Double을 추가하는 모순 발생하게 된다. 이는 제네릭의 타입 안정성을 위반하게 되며, null 만 추가할 수 있도록 했다.
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
addDouble(ints);
}
private static void addDouble(List<?> ints){
ints.add(3.14); // List<Integer>에 Double을 추가하는 모순 발생
}
<? super T> 의 형태로, List<? super T> 와 같이 사용한다. T 혹은 T의 상위 클래스만 인자로 올 수 있다.
<? super T>에서 Get한 원소는 Object 이다.
T 하한 경계 와일드카드의 원소는 T의 상위 클래스 중 어떠한 타입도 올 수 있다. 어떠한 타입이 와도 읽을 수 있도록, T들의 공통 조상인 Object로 받는다. List<Integer>, List<Double>, List<Number>가 와도 모두 읽을 수 있다.
public static void printList(List<? super Integer> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
List<? super T> 에는 T의 하위 클래스만 삽입할 수 있다.
List<? super Integer> ints = new ArrayList<>();
ints.add(new Integer());
ints.add(new Number()); // compile error
만약 ints가 List<Integer> 일 경우 Number는 Integer의 상위 클래스 이므로 원소를 추가할 수 없다. List<Integer>, List<Number>, List<Object> 중 어떠한 리스트가 올지 ints는 알지 못한다. 하지만 그 중 어떠한 리스트가 오더라도, Integer의 하위 클래스는 원소로 추가할 수 있다.
Wildcards and Subtyping
비제네릭 클래스에서든 상위 클래스에 하위 클래스를 대입할 수 있다.
class A { /* ... */ }
class B extends A { /* ... */ }
B b = new B();
A a = b; // OK
하지만, 제네릭 클래스에서는 컴파일 오류가 발생한다.
List<B> lb = new ArrayList<>();
List<A> la = lb; //compile error
왜냐하면 List<B>는 List<A>의 하위 타입이 아니며, 아무런 관계가 없다. List<B>와 List<A>의 공통 조상은 List<?>이다. 그렇다면, B와 A의 요소를 받는 제네릭 타입을 만들고 싶을때는 상위 경계 와일드카드를 사용하면된다.
List<? extends Integer> ints = new ArrayList<>();
List<? extends Number> numbers = ints; // OK
List<? extends Integer>는 List<? extends Number>의 하위 타입이기 때문에 가능하다. 왜냐하면 Integer의 하위 타입은 Number의 하위타입이기 때문에, 아래와 같은 관계가 성립된다.
Wildcard Capture
컴파일러는 어떠한 경우에, 와일드카드의 타입을 추론한다. 예를들어, List<?> 리스트가 정의되어있을때, 컴파일러는 코드에서 특정 타입을 추론한다. 이러한 시나리오를 와일드카드 캡처라고 한다.
"capture of" 오류 문구를 제외하고는 와일드카드 캡처를 신경쓰지 않아도 된다.
public class WildcardError {
void foo(List<?> i) {
i.set(0, i.get(0)); // capture of compile error
}
}
위 예제에서는 foo() 메서드에서 List.set(int, E)를 호출할때 컴파일러는 List에 삽입되는 객체의 유형을 확인할 수 없기 때문에 오류를 발생시킨다. 이러한 유형의 오류가 발생하면 일반적으로 컴파일러는 변수에 잘못된 타입의 값을 할당하고 있다고 믿는다는 것을 의미한다. 이렇게 자바 컴파일타임에 타입안전을 추가하기위해 제네릭이 추가된 것이다.
public class WildcardFixed {
void foo(List<?> i) {
fooHelper(i);
}
// Helper method created so that the wildcard can be captured
// through type inference.
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}
이러한 경우 fooHelper() 처럼 private helper method를 만들어서 문제를 해결할 수 있다. helper 메서드 덕분에 컴파일러는 T가 캡쳐 변수인 CAP#1 임을 추론할 수 있다. 일반적으로 helper 메서드는 originalMethodNameHelper로 지정된다.
Type Erasure
제네릭은 타입의 안정성을 보장하며, 실행시간에 오버헤드가 발생하지 않도록 하기위해 추가됐다. 컴파일러는 컴파일 시점에 제네릭에 대해 원소 타입 소거(type erasure)를 한다. 즉, 컴파일 타임에만 타입 제약 조건을 정의하고, 런타임에는 타입을 제거한다는 뜻이다.
unbounded Type(<?>, <T>)은 Object로 변환
bound type(<E extends Comparable>)의 경우는 Object가 아닌 Comprarable로 변환
제네릭 타입을 사용할 수 있는 일반 클래스, 인터페이스, 메소드에만 소거 규칙을 적용
타입 안정성 보존을 위해 필요시 type casting
확장된 제네릭 타입에서 다형성을 보존하기위해 bridge method 생성
하나씩 예제를 보면서 알아볼 것이다.
unbounded Type(<?>, <T>)은 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; }
// ...
}
type erasure가 적용되면서 특정 타입으로 제한되지 않은 <T>는 다음과 같이 Object로 대체된다.
제네릭 메서드에서도 동일하다.
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
public static int count(Object[] anArray, Object elem) {
int cnt = 0;
for (Object e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
T 는 비한정적 타입이므로, 컴파일러가 Object 로 변환한다.
bound type(<E extends T>)의 경우는 Object가 아닌 T로 변환
// 컴파일 할 때 (타입 변환 전)
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; }
// ...
}
한정된 타입(bound type)에서는 컴파일 시점에 제한된 타입으로 변환된다. Comparable 로 변환된 것을 확인할 수 있다.
public static void draw(Shape shape) { /* ... */ }
여기서는 Shape 로 변환된 것을 확인할 수 있다.
확장된 제네릭 타입에서 다형성을 보존하기위해 bridge method 생성
컴파일러가 컴파일 시점에 제네릭 타입 안정성을 위해 bridge method를 생성할 수 있다. 다음 예제를 살펴보자.
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;
타입이 소거된 후에는 다음과 같이 적용되며,런타임시 ClassCastException 를 발생시키게 된다.
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;
타입 소거 후에 Node와 MyNode는 다음과 같이 변환되는 것을 볼 수 있으며, 소거 후에는 Node 시그니처 메서드가 setData(T data) 에서 setData(Object data)로 바꾸기 때문에 MyNode 의 setData(Integer data)를 overriding 할 수 없게 된다.
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);
}
}
런타임 시에는 다음과 같이 타입이 소거된 상태로 변할 것이다. (Object로 변환) 그렇게 되면, Object 로 변하게 되는 경우에 대한 불일치를 없애기 위해 컴파일러는 런타임에 해당 제네릭 타입의 타임 소거를 위한 bridge method를 생성해준다.
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);
}
// ...
}
그렇기때문에 ClassCastException 예외를 던지는 것을 알 수 있다.
Reifiable vs Non-Reifiable
Reifiable Type
런타임시에 완전하게 오브젝트 정보를 표현할 수 있는 타입을 실체화 타입(Reifiable Type)이라고 한다. 즉, 컴파일 단계에서 타입소거에 의해 지워지지 않는 타입 정보이다.
원시 타입 (ex) int, double, float, byte 등등
Number, Integer와 같은 일반 클래스와 인터페이스 타입
비한정 와일드카드(?)가 포함된 매개변수화 타입 (ex) List<?>, ArrayList<?>
Raw type (ex) List, ArrayList, Map
비한정적 와일드카드는 애초에 타입 정보를 전혀 명시하지 않았으므로, 컴파일시에 타입 소거를 한다고 해도 잃을 정보가 없기 때문에 실체화 타입이라 볼 수 있으며, 타입 소거에 의해 컴파일 시점에 Object로 변환된다.
Non-Reifiable Type
컴파일 단계에서 타입소거에 의해서 타입 정보가 제거된 타입이다. 이 경우 런타임시에 런타임시에 알 수 있는 정보가 없다.
Generic Type (ex) List<E>
Parameterized Type (ex) List<Number>, ArrayList<String>
Bounded wildcard Type (ex) List<? extends Number>, List<? super String>
Heap Pollution
힙 오염은 파라미터화 된 타입의 변수가 해당 파라미터화된 타입이 아닌 객체를 참조할 때 발생한다.
raw type과 파라미터화된 타입이 섞여 사용되는 경우
unchecked cast가 사용된 경우
Potential Vulnerabilities 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!"));
}
}
컴파일시에 addToList() 메서드 정의 부분에 다음과 같은 경고 문구가 뜬다.
Possible heap pollution from parameterized vararg type
컴파일이 가변인수(varargs) 메서드를 만나게되면 가변인수(varargs) 파라미터를 배열로 변환시킨다. 하지만, 자바는 파라미터화된 타입의 배열의 생성을 허용하지 않는다. 위 예제에서 ArrayBuilder.addToList 가변인수 매개변수 T...를 T[] 배열로 변환하는데, 타입 소거에 의해서 컴파일러는 가변인수 파라미터를 Object[]로 변환시키게되고 결과적으로 힙오염의 가능성이 생기는 것이다.
Object[] objectArray = l;
구문은 힙오염을 잠재적으로 내재하고 있는데, 컴파일러는 경고문구를 생성하지 않고 있다. 왜냐하면, 컴파일는 List<String>... l 을 형식 파라미터 List[] 로 변환할 때 이미 경고를 만들었기 때문이다. 즉, 변수 l은 Object[]의 하위 타입인 List[] 타입을 갖게되고 이 구문은 유효하게 되는 것이다.
ArrayBuilder.faultyMethod 를 호출한다면, 런타임시에 ClassCastException 오류를 발생시키는 것이다.
Prevent Warnings from Varargs Methods with Non-Reifiable Formal Parameters
파라미터화 타입의 가변인수 메서드가 ClassCastException 을 발생시키지 않고, 비슷한 오류가 발생하지 않는 다는 것이 보장되면(타입 안전성이 보장) static과 생성자가 아닌 메서드 선언부에@SafeVarargs 어노테이션을 추가해 경고 문구를 지울 수 있다. 이 어노테이션은 메서드 구현에서 varargs 형식 파라미터에 대해 부적절한 처리를하지 않았음을 나타낸다. 좋은 방법은 아니지만 메서드 선언에 다음과 같은 방법으로도 경고 문구를 억제할 수 있다.
@SuppressWarnings({"unchecked", "varargs"})
단, 이경우에는 메서드 호출 부분에서 생성되는 경고를 막아주지는 않는다.
Generic Type의 상속과 구현
제네릭 타입도 부모 클래스가 될 수 있다.
public class ChildProduct<T,M> extends Product<T,M>{...}
자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있다.
public class ChildProduct<T,M,C> extends Product<T,M>{...}
제네릭 인터페이스를 구현한 클래스도 제네릭 타입이된다.
public interface Storage<T>{
public void add(T item, int index);
public T get(int index);
}
public class StorageImpl<T> implements Storage<T>{
private T[] array;
public StorageImpl(int capacity){
this.array = (T[])(new Object[capacity]);
}
@Override
public void add(T item, int index){
array[index] = item;
}
@Override
public T get(int index){
return array[index];
}
}