Generic
시작하기에 앞서 관련 용어를 정리해 둘것이다.
한글 | 영어 | 예 |
---|---|---|
매개변수화 타입 | parameterized type |
|
실제 타입 매개변수 | actual type parameter |
|
제네릭 타입 | generic type |
|
정규 타입 매개변수 | formal type parameter |
|
비한정적 와일드카드 타입 | unbounded wildcard type |
|
로 타입 | raw type |
|
한정적 타입 매개변수 | bounded type parameter |
|
재귀 타입 한정 | recursive type bound |
|
한정적 와일드카드 타입 | bounded wildcard type |
|
제네릭 메서드 | generic method |
|
타입 토큰 | type token |
|
Generic이란 무엇일까?
JDK 1.5에 처음 도입되었으며, 제네릭은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다.(생활코딩 )
위의 예제를 보면 Fruit
인스턴스를 생성할 때, Apple
, Banana
를 넣어 타입을 지정하고 있다. 즉, 클래스를 정의 할 때는 어떤 타입이 들어올지 확정하지 않고, 인스턴스를 생성할 떄 데이터 타입을 지정하는 기능이다.
왜 제네릭을 사용할까?
Generic은 클래스와 인터페이스, 메서드를 정의할 때 타입을 파라미터로 사용할 수 있게 한다. Generic 타입을 이용함으로써 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있게되었다. 타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해준다.
컴파일 시 강한 타입 체크를 할 수 있다.
컴파일 언어의 기본은 모든 에러는 컴파일이 발생할 수 있도록 하는 것이 좋다.(오류는 빨리 발견할 수록 좋다.) 런타임은 실제로 애플리케이션이 동작하고 있는 상황이기 때문에 런타임에 발생하는 에러는 항상 심각한 문제를 초래할 수 있기 때문이다.
위는 성공적으로 컴파일되지만, 런타임시 ClassCastException
이 발생하는 것을 보여주는 예제이다. 이때 Generic Type을 사용해 컴파일 타임에 오류를 발견할 수 있도록 할 수 있다.
Generic type으로 변경 후에는 java: incompatible types: java.lang.String cannot be converted to int
컴파일 오류가 발생하는 것을 확인할 수 있다.
즉, 컴파일 단계에서 오류 검출이 가능하며, 타입 안정성을 추구할 수 있게된다.
타입 변환(casting)을 제거한다.
Generic Type
Generic Type은 **타입을 파라미터로 가지는 클래스(class<T>
)와 인터페이스(interface<T>
)**를 말한다.
타입 파라미터는 변수명과 동일한 규칙으로 작성될 수 있지만, 일반적으로 대문자 한글자로 표현한다. Generic Type을 실제 코드에서 사용하려면 타입 파라미터에 구체적인 타입을 지정해야한다.
위의 코드에서 클래스 필드 타입을 Object로 선언한 이유는 필드에 모든 종류의 객체를 저장하고 싶어서이다. Object는 모든 자바 클래스의 최상위 조상(부모) 클래스이다. 자식 객체는 부모타입에 대입할 수 있기 때문에 모든 자바 객체는 Object타입으로 자동 타입변환되어 저장된다.
다음과 같이 get으로 가져오기위해서는 강제 타입 변환이 필요하다.
Object 타입을 사용하면 모든 종류의 자바 객체를 저장할 수 있다는 장점은 있지만, 저장할 때와 읽어올 때 타입 변환이 발생하며, 잦은 타입 변환은 전체 프로그램 성능에 좋지 못한 결과를 가져올 수 있다.
Generic을 통해서 타입변환이 발생하지 않도록 할 수 있다.
여기서 T는 클래스로 객체를 생성할 때 구체적인 타입으로 변경된다. 그렇기 때문에 저장할 때와 읽어올 때 타입 변환이 발생하지 않는다. 이와 같이 generic은 클래스 설계시 구체적은 타입을 명시하지 않고, 타입 파라미터로 대체했다가 실제 클래스가 사용될 때 구체적인 타입을 지정함으로써 타입 변환을 최소화시킨다.
타입 파라미터
타입 파라미터의 이름은 짓기 나름이지만, 일반적으로 대문자 한글자로 표현 보편적으로 자주 사용하는 타입 매개변수는 다음과 같다.
타입 매개변수 | 의미 |
---|---|
| Element |
| Key |
| Number |
| Type |
| Value |
| 2nd, 3rd, 4th types |
다이아몬드 <>
<>
제네릭 타입 변수 선언과 객체 생성을 동시에 할 때 타입 파라미터에 구체적인 타입을 지정하는 코드가 중복될 수 있다. 그렇기 때문에 자바7에서 부터는 <>
(다이아몬드연산자)를 제공한다. 자바 컴파일러는 타입 파라미터 부분에 <>
연산자를 사용하면 타입 파라미터를 유추해서 자동으로 설정해준다.
다중 타입 파라미터(Multiple Type Parameters)
Generic Type은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있다. 이 경우 각 타입 파라미터를 콤마로 구분한다.
Raw Types
Raw type은 타입 파라미터가 없는 제네릭 타입을 의미한다.
Raw type은 Java가 제네릭을 도입하기전(JDK 5.0) 이전 기존 코드와의 호환성을 보장하기 위해서 제공하고 있는 것이다.
하지만, raw type을 paratmeterized type으로 설정하면 경고문구를 볼 수 있다.
또한, raw type을 사용해, 제네릭 타입의 제네릭 메서드를 호출하는 경우에도 경고가 표시된다.
위 경고는 raw type이 generic type 검사를 생략해 안전하지 않은 코드가 런타임시에 발견될 수도 있다는 경고 문구이다. 그러므로, raw type은 최대한 사용하지 않는 것이 좋으며, 자세한 내용은 **[effective java - ITEM 26]**에서 확인할 수 있다.
Generic Method
일반 클래스의 메서드에서 타입 매개변수를 사용해 정의 가능
제네릭 메서드에서 타입 매개변수 범위는 메서드 내부로 제한된다.
제네릭 클래스의 생성자에서도 사용 가능
Generic Method는 매개타입과 리턴 타입으로 타입 파라미터를 갖는 메소드이다.
타입 매개변수 <T>
는 반드시 메서드의 수식자(public
, static
)와 반환형 사이에 위치되어야한다.
Generic 메소드는 다음과 같이 호출될 수 있다.
Util.<Integer, String>compare(p1, p2)
과 같이 타입을 명시적으로 썼지만, 다음과 같이 생략해도 컴파일러가 유추할 수 있다.
제한된 타입 파라미터(<T extends 최상위타입>
)
<T extends 최상위타입>
)타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 종종 있다. 숫자를 연산하는 제네릭 메소드의 매개값으로는 Number
타입 또는 그 하위 클래스 타입(Integer
, Double
, Long
, Short
, ...) 의 인스턴스만 가져야한다. 이러한 이유로 제한된 타입 파라미터가 필요한 경우가 있다.
상위 타입은 클래스뿐만 인터페이스도 가능하다. 하지만 인터페이스라고 해서 implements를 사용하지 않는다.( extends
사용)
타입 파라미터의 구체적 타입 : 상위타입, 상위타입의 하위 또는 구현클래스
{}
안에서의 타입 파라미터 변수 : 상위 타입의 멤버(필드, 메서드로 제한)
여기서 주의할 점은 함수 내에서 타입 파라미터 변수로 사용한 것은 상위 타입의 멤버(필드, 메서드)로 제한된다는 점이다. doubleValue()
는 Number
클래스의 메서드이기때문에 사용할 수 있는 것이다.
Muliple Bounds
제한된 타입 파라미터를 여러개로 설정할 수 있다.
여기서 D
클래스의 타입파라미터는 A
, B
, C
모든 유형의 하위 클래스여야하며, 이중 한개가 클래스인 경우 반드시 먼저 선언되어야한다.
위와 같이 A
가 클래스이지만, 인터페이스인 B
보다 늦게 선언된다면 컴파일 오류가 발생한다.
Generic Method와 Bounded Type Parameter
제한된 타입 파라미터(Bounded Type Parameter)는 제네릭 알고리즘을 구현할때 핵심이된다.
위 메서드는 컴파일 오류가 발생한다.
>
연산자는 기본형(int
, short
, double
, long
...)에만 동작이 허용되기 때문이다. 즉, >
연산자는 객체간 비교에 사용할 수 없으며, Comparable
인터페이스를 사용해 해당 오류를 해결할 수 있다.
Comparable
인터페이스로 제한된 타입을 사용해 객체를 비교할 수 있다.
Generics, Inheritance, Subtypes
타입간 호환이된다면, 특정 타입의 객체를 다른 타입에 할당이 가능하다. Object
는 Integer
의 상위 클래스이기 때문에 할당이 가능하다. 객체지향 이론에서는 이러한 경우를 "is a" 관계라고 부른다. "Integer is a Object" 이므로 Object에 Integer가 할당이 가능한 것이다.
또한, "Integer is a Number" 이므로 위 예제 코드 또한 잘 작동하는 것을 볼 수 있다.
이러한 규칙은 제네릭에서도 똑같이 적용된다. 제네릭 타입 호출시, 타입 인자가 "is a" 관계라면 타입 인자로 전달할 수 있는 것이다.
여기서 주의해야할 부분이 있다.
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>
를 상속 받고 있는 것을 볼 수 있다.
그러므로, ArrayList<String>
은 List<String>
와 Collection<String>
의 하위 타입으로 "is a"관계가 성립하게 되는 것이다.
List<E>
인터페이스를 상속받는 PayloadList
가 다음과 같이 정의되었을 때, PayloadList
는 다음과 같은 서브타입 관계가 형성될 수 있는걸 볼 수 있다.
Type Inference(타입 추론)
타입추론은 컴파일러가 각 메서드 호출과 해당 선언을 검토하고 호출을 적용할 수 있게 하는 인수를 결정하는 능력이다. 추론 알고리즘은 인수의 유형과 결과가 할당되거나 반환되는 유형을 결정하고, 가장 구체적인 타입을 찾기 위해 노력한다.
Type Inference and Generic Methods
위 예제에서 제네릭 메서드 addBox()
는 타입 매개변수(U
)가 선언되어있다. 일반적으로 컴파일러는 해당 제네릭 메서드를 호출하는 곳을 보고 타입 파라미터를 추론할 수 있다. 제네릭 메서드 addBox
를 호출할 때, 구체적인 타입 매개변수를 주지 않아도 컴파일러가 자동으로 값을 추론할 수 있다.
그러므로, 대부분 타입을 구체적으로 선언하지 않고도 사용할 수 있는 것이다.
Type Inference and Instantiation of Generic Classes
제네릭 클래스를 생성자를 통해 객체를 생성할 때 타입 인자 대신 <>
다이아몬드 연산자를 사용하면, 컴파일러는 타입인자를 유추할 수 있다.
제네릭 클래스의 생성자에 타입 매개변수화한 생성자 대신 아래처럼 <>
만 사용하여 선언할 수 있다.
하지만, <>
연산자를 선언하지 않으면, raw 타입이므로 타입추론을 하지 않기때문에 주의해야한다
Type Inference and Generic Constructors of Generic and Non-Generic Classes
제네릭 클래스와 비제네릭 클래스(non-generic) 모두 제네릭 생성자를 선언할 수 있다.
위 코드는 매개변수화된 타입 MyClass
의 인스턴스를 생성한다. 제네릭 클래스인 MyClass
의의 형식 매개변수 X
에 대해 Integer
타입을 명시적으로 지정하고 있다. 이 제네릭 클래스의 생성자는 형식 매개 변수 T
를 포함하고 있으며, 컴파일러는 이 제네릭 클래스(MyClass
)의 생성자의 파라미터 타입이 String
인 것을 추론할 수 있다. 자바 SE 7 이전의 컴파일러는 제네릭 메서드와 유사한 제네릭 생성자의 실제 타입 파라미터를 추론할 수 있으며, 자바 SE7 이상 컴파일러는 <>
다이아몬드 연산자를 사용하면, 제네릭 클래스의 실제 타입 파라미터를 추론할 수 있다.
위 예시에서 컴파일러는 제네릭 클래스 MyClass<X>
의 타입 파라미터가 Integer
임을 추론할 수 있고, 이 제네릭 클래스의 생성자의 타입 파라미터 T
가 String
임을 추론할 수 있다.
Target Types
타겟 타입 표현식은 자바 컴파일러가 예상하는 데이터 타입이다.
위 선언문은 리턴타입을 List<String>
타입으로 받고 있기때문에 List<String>
를 예상할 수 있다. 이 데이터 타입을 바로 target type이라 한다. emptyList()
메서드가 List<T>
를 반환하기때문에 컴파일러는 T
가 String
이라고 추론할 수 있다.
물론 위와 같이 T
타입을 명시 할 수 있지만, 문맥상 String
인것이 명백하기 때문에 적어주지 않아도 된다. 명시적으로 타입을 선언해줘야하는 경우를 살펴보자.(Type witness)
위 예제는 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부터는 아래 예시도 컴파일 된다.
Wildcards (와일드카드)
제네릭에서 unkwon타입을 표현하는 ?
를 일반적으로 와일드카드라고 부른다. 와일드카드는 파라미터, 필드, 직역변수 타입, 리턴타입 등 다양한 상황에서 쓰이며, 제네릭 메서드 호출, 제네릭 클래스 인스턴스 생성, 상위 타입(super type)의 타입 인자로는 사용되지 않는다.
코드 | 종류 | 설명 |
---|---|---|
| Unbounded wildcards 비한정적 와일드 카드 | 제한 없음 (타입 파라미터를 대치하는 구체적 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.) |
| Upper Bounded Wildcards 상한 경계 와일드카드 | 상위 클래스 제한 (타입 파라미터를 대치하는 구체적 타입으로 상위 타입이나 하위 타입만 올 수 있다.) |
| Lower Bounded Wildcards 하한 경계 와일드카드 | 하위 클래스 제한 (타입 파라미터를 대치하는 구체적 타입으로 하위 타입이나 상위타입이 올 수 있다.) |
수강생이 될 수 있는 타입이 아래와 같다.
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<?>
)
위 메서드의 목표는 어떠한 타입의 리스트가 오더라도 그 요소를 출력하는 것이다. 하지만, 위 예제는 한가지 문제점이 있다.
List<Object>
외의 List<Integer>
, List<String>
, List<Double>
의 출력은 java: incompatible types:
컴파일 오류가 발생하며 실패한다. 왜나하면 List<Object>
의 하위타입이 아니기 때문이다.
비한정적 와일드카드 타입을 사용한다면, 성공적으로 출력되는 것을 알 수 있다. 왜나하면, 어떠한 타입 A가 와도 List<A>
는 List<?>
의 하위 타입이기 때문이다.
비한정적 와일드카드 List<?>
에서 가져온 원소는 Object
타입이다.
비한정적 와일드 카드는 어떠한 타입이 와도 읽을 수 있도록, 모든 타입의 공통 조상인 Object
로 받는다.
여기서 주의할 점은 List<Object>
와 List<?>
이 같지 않으며, List<Object>
에는 Object
의 하위 타입은 모두 넣을 수 있지만, List<?>
에는 오직 null
만 넣을 수 있다. 왜나하면 비경계 와일드 카드의 원소가 어떠한 타입이 들어오는 지 알 수 없으므로, 타입 안정성을 지키기 위해 null
만 추가할 수 있다.
만약 다음과 같이 모든 타입을 넣을 수 있게 한다면, List<Integer>
에 Double
을 추가하는 모순 발생하게 된다. 이는 제네릭의 타입 안정성을 위반하게 되며, null
만 추가할 수 있도록 했다.
Upper Bounded Wildcards
상한 경계 와일드 카드를 사용해 변수에 대한 제한을 완화할 수 있다. 예를 들어, List<Integer>
, List<Double>
, List<Number>
에서만 동작하는 메서드를 원할 때 상한 경계 와일드 카드를 사용하면된다.
List<Number>
는 오직 List<Number>
만 올 수 있으므로, Number
및 Number
의 하위클래스가 모두 올 수 있는 List<? extends Number>
보다 더 제한적이다.
즉, <? extends T>
는 T의 하위 타입만 올 수 있다.
<? extends T>
에서 Get한 원소는 T
이다.
<? extends T>
에서 Get한 원소는 T
이다.상한 경계 와일드 카드의 원소는 T
혹은 T
의 하위 클래스이며, 원소들의 최고 공통 조상인 T
로 읽으면 어떤 타입이 오든 T
로 읽을 수 있다.
여기서 만약 하위 타입인 Integer
로 하면 컴파일 오류가 발생한다. 왜냐하면, Double
은 Number
로 값이 들어오게 되면, Integer
로 타입을 변환할 수 없기 때문이다.
List<? extends T>
에는 null만 삽입할 수 있다.
List<? extends T>
에는 null만 삽입할 수 있다.상한경계 와일드카드의 원소가 어떤 타입인지 알 수 없기 때문에 null
만 삽입할 수 있다.
Lower Bounded Wildcards
<? super T>
의 형태로, List<? super T>
와 같이 사용한다. T
혹은 T
의 상위 클래스만 인자로 올 수 있다.
<? super T>
에서 Get한 원소는 Object
이다.
<? super T>
에서 Get한 원소는 Object
이다.T
하한 경계 와일드카드의 원소는 T
의 상위 클래스 중 어떠한 타입도 올 수 있다. 어떠한 타입이 와도 읽을 수 있도록, T
들의 공통 조상인 Object
로 받는다. List<Integer>
, List<Double>
, List<Number>
가 와도 모두 읽을 수 있다.
List<? super T>
에는 T
의 하위 클래스만 삽입할 수 있다.
List<? super T>
에는 T
의 하위 클래스만 삽입할 수 있다.만약 ints가 List<Integer>
일 경우 Number
는 Integer
의 상위 클래스 이므로 원소를 추가할 수 없다. List<Integer>
, List<Number>
, List<Object>
중 어떠한 리스트가 올지 ints는 알지 못한다. 하지만 그 중 어떠한 리스트가 오더라도, Integer
의 하위 클래스는 원소로 추가할 수 있다.
Wildcards and Subtyping
비제네릭 클래스에서든 상위 클래스에 하위 클래스를 대입할 수 있다.
하지만, 제네릭 클래스에서는 컴파일 오류가 발생한다.
왜냐하면 List<B>
는 List<A>
의 하위 타입이 아니며, 아무런 관계가 없다. List<B>
와 List<A>
의 공통 조상은 List<?>
이다. 그렇다면, B
와 A
의 요소를 받는 제네릭 타입을 만들고 싶을때는 상위 경계 와일드카드를 사용하면된다.
List<? extends Integer>
는 List<? extends Number>
의 하위 타입이기 때문에 가능하다. 왜냐하면 Integer
의 하위 타입은 Number
의 하위타입이기 때문에, 아래와 같은 관계가 성립된다.
Wildcard Capture
컴파일러는 어떠한 경우에, 와일드카드의 타입을 추론한다. 예를들어, List<?>
리스트가 정의되어있을때, 컴파일러는 코드에서 특정 타입을 추론한다. 이러한 시나리오를 와일드카드 캡처라고 한다.
"capture of" 오류 문구를 제외하고는 와일드카드 캡처를 신경쓰지 않아도 된다.
위 예제에서는 foo()
메서드에서 List.set(int, E)
를 호출할때 컴파일러는 List
에 삽입되는 객체의 유형을 확인할 수 없기 때문에 오류를 발생시킨다. 이러한 유형의 오류가 발생하면 일반적으로 컴파일러는 변수에 잘못된 타입의 값을 할당하고 있다고 믿는다는 것을 의미한다. 이렇게 자바 컴파일타임에 타입안전을 추가하기위해 제네릭이 추가된 것이다.
이러한 경우 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
로 변환
<?>
, <T>
)은 Object
로 변환type erasure가 적용되면서 특정 타입으로 제한되지 않은 <T>
는 다음과 같이 Object
로 대체된다.
제네릭 메서드에서도 동일하다.
T
는 비한정적 타입이므로, 컴파일러가 Object
로 변환한다.
bound type(<E extends T>
)의 경우는 Object가 아닌 T
로 변환
<E extends T>
)의 경우는 Object가 아닌 T
로 변환한정된 타입(bound type)에서는 컴파일 시점에 제한된 타입으로 변환된다. Comparable
로 변환된 것을 확인할 수 있다.
여기서는 Shape
로 변환된 것을 확인할 수 있다.
확장된 제네릭 타입에서 다형성을 보존하기위해 bridge method 생성
컴파일러가 컴파일 시점에 제네릭 타입 안정성을 위해 bridge method를 생성할 수 있다. 다음 예제를 살펴보자.
위 두개의 클래스가 있다. 이때 다음과 코드를 실행해야한다고 예를 들어보자.
타입이 소거된 후에는 다음과 같이 적용되며,런타임시 ClassCastException
를 발생시키게 된다.
타입 소거 후에 Node
와 MyNode
는 다음과 같이 변환되는 것을 볼 수 있으며, 소거 후에는 Node
시그니처 메서드가 setData(T data)
에서 setData(Object data)
로 바꾸기 때문에 MyNode
의 setData(Integer data)
를 overriding 할 수 없게 된다.
런타임 시에는 다음과 같이 타입이 소거된 상태로 변할 것이다. (Object
로 변환) 그렇게 되면, Object
로 변하게 되는 경우에 대한 불일치를 없애기 위해 컴파일러는 런타임에 해당 제네릭 타입의 타임 소거를 위한 bridge method를 생성해준다.
그렇기때문에 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) 매개변수를 포함한 제네릭 메서드는 힙 오염을 유발할 수 있다.
컴파일시에 addToList()
메서드 정의 부분에 다음과 같은 경고 문구가 뜬다.
컴파일이 가변인수(varargs) 메서드를 만나게되면 가변인수(varargs) 파라미터를 배열로 변환시킨다. 하지만, 자바는 파라미터화된 타입의 배열의 생성을 허용하지 않는다. 위 예제에서 ArrayBuilder.addToList
가변인수 매개변수 T...
를 T[]
배열로 변환하는데, 타입 소거에 의해서 컴파일러는 가변인수 파라미터를 Object[]
로 변환시키게되고 결과적으로 힙오염의 가능성이 생기는 것이다.
구문은 힙오염을 잠재적으로 내재하고 있는데, 컴파일러는 경고문구를 생성하지 않고 있다. 왜냐하면, 컴파일는 List<String>... l
을 형식 파라미터 List[]
로 변환할 때 이미 경고를 만들었기 때문이다. 즉, 변수 l
은 Object[]
의 하위 타입인 List[]
타입을 갖게되고 이 구문은 유효하게 되는 것이다.
여기서 objectArray
의 첫번째 원소에 Integer
타입의 객체 리스트를 할당하고,
ArrayBuilder.faultyMethod
를 호출한다면, 런타임시에 ClassCastException
오류를 발생시키는 것이다.
Prevent Warnings from Varargs Methods with Non-Reifiable Formal Parameters
파라미터화 타입의 가변인수 메서드가 ClassCastException
을 발생시키지 않고, 비슷한 오류가 발생하지 않는 다는 것이 보장되면(타입 안전성이 보장) static
과 생성자가 아닌 메서드 선언부에@SafeVarargs
어노테이션을 추가해 경고 문구를 지울 수 있다. 이 어노테이션은 메서드 구현에서 varargs 형식 파라미터에 대해 부적절한 처리를하지 않았음을 나타낸다. 좋은 방법은 아니지만 메서드 선언에 다음과 같은 방법으로도 경고 문구를 억제할 수 있다.
단, 이경우에는 메서드 호출 부분에서 생성되는 경고를 막아주지는 않는다.
Generic Type의 상속과 구현
제네릭 타입도 부모 클래스가 될 수 있다.
자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있다.
제네릭 인터페이스를 구현한 클래스도 제네릭 타입이된다.
참고
Last updated