제네릭(Generics)
제네릭은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 이를 통해 객체별 다른 타입 데이터를 저장할 수 있게된다.
배열 선언시 [](배열 자료형) 앞에 저장할 자료형의 타입을 저장하는 것과 같이 순서만 다를 뿐 자료형과 <>(꺾쇠 괄호) 안에 타입을 기재하여 사용한다.
제네릭 예시 : ArrayList<Integer> list = new ArrayList<Integer
자료형<타입>
배열 예시 : Integer[] array = new Integer[10];
타입[](배열자료형)
제네릭을 왜 사용할까?
- 컴파일시 타입을 체크해 주는 기능 (compile-time type check)을 한다.
ArrayList list = new ArrayList();
list.add(10);
list.add(20);
list.add("30");
// get은 Object 객체를 반환하기 때문에 형변환이 필요
Integer i = (Integer)list.get(2);
// ClassCastException 발생, 형변환 에러
get의 반환값은 Object 타입으로 무슨 값을 가지는지 컴파일러는 알 수 없기 때문에 컴파일러의 한계로 컴파일시 에는 문제가 없음지만 실행 시 에러가 발생한다. 실행시 발생하는 에러는 프로그램을 종료시키기 때문에 컴파일시 오류 발생하는 것이 비교적 경미한 오류라고 볼수 있다. 따라서 실행시 발생하는 에러를 컴파일러에서 잡아내기 위해 제네릭 사용한다.
객체의 타입정보를 제공, 컴파일러에 더 많은 정보를 제공하기 위해 사용된다. 지정한 객체만 저장이 가능해진다.
- 객체의 타입 안전성을 높이고 형변환의 번거로움을 줄여줌, 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(10);
list.add(20);
// list.add("30"); // 컴파일 에러, Tv 외에 다른 타입은 저장 불가
list.add(30); // 타입 체크가 강화됨. (제네릭에 의해)
Integer i = list.get(2); // 형변환 생략이 가능
제네릭 타입 변수
클래스를 작성할 때, Object타입 대신 타입 변수(E)를 선언해서 사용한다. 꺾쇠 괄호 안에 식별자 기호를 지정함으로써 파라미터화 할 수 있다. 메소드가 매개변수를 받아 사용하는 것과 비슷하여 제네릭의 타입 변수 또는 타입 매개변수라고 부른다.
타입 변수의 이름은 무엇을 사용하든 상관없지만 보통 Type의 T나 Element의 E를 사용한다.
class Box{
Object item;
void setItem (Object item) { this.item = item;}
Object getItem() { return item;}
}
class Box<T>{
T item;
void setItem (T item) { this.item = item;}
T getItem() { return item;}
}
- 객체 생성시, 타입 변수 대신 실제 타입을 지정해야 한다. 참조변수와 생성자에 대입된 타입은 일치해야 한다.
// 매개변수화된 타입
Box<String> b = new Box<String>(); // 타입 T 대신, 실제 타입을 지정
b.setItem(new Object()); // 에러. String이외의 타입은 지정 불가
b.setItem("ABC"); // String 타입으로 가능
String item = (String) b.getItem(); // 형변환이 필요 없음
실제 타입이 지정되면 반환타입이 변경되기 때문에 형변환의 생략이 가능해진다.
타입 변수의 이름
예시들에서 <T>를 사용해 왔지만 타입 변수의 이름은 무엇을 사용하든 상관없지만 보통 Type의 T나 Element의 E를 사용 하듯 암묵적 규칙이 존재한다.
타입 | 설명 |
<T> | 타입(Type) |
<E> | 요소(Element) |
<K> | 키(Key) |
<V> | 리턴 값 또는 매핑된 값(Variable) |
<N> | 숫자(Number) |
<S, U, V> | 2, 3, 4 번쨰에 선언된 타입(Type) |
제네릭 용어
Box<T> : 제네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다.
T : 타입 변수 또는 타입 매개변수. (T는 타입 문자)
Box : 원시 타입 (raw type)
제네릭 클래스의 타입의 할당 가능 타입
타입 변수에 대입 가능한 타입은 Reference 타입만 가능하기 때문에 int형과 같은 자바 기본 타입을 제네릭 변수로 대입할 수 없다 . 따라서 래퍼(Wrapper) 클래스를 사용하여 대입해야 한다.
List<int> list = new ArrayList<int>();
List<Integer> list = new ArrayList<Integer>();
타입 변수에 대입 가능한 타입은 Reference 타입만 가능하다는 것은 클래스를 대입시 클래스 끼리 상속을 통해 관계를 맺는 다형성이이 적용된다는 것을 의미한다.
제네릭과 다형성
- 참조변수와 생성자의 대입된 타입은 일치해야 한다.(부모 자식 관계시에도)
Box<String> b = new Box<String>(); // 일치
Box<Object> b = new Box<String>(); // 에러. 불일치
- 제네릭 클래스간의 다형성은 성립한다.( 대입된 타입은 일치해야 한다.)
List<String> list = new ArrayList<String>(); // ArrayList가 List를 구현함 Ok
List<String> list = new LinkedList<String>(); // LinkedList가 List를 구현함 Ok
- 매개변수의 다형성도 성립한다.
class Product{};
class Box<T> extends Product{...)
List<Product> list = new ArrayList<Product>();
// boolean add(E e) { .. } => boolean add(Product e) { .. }
list.add(new Product());
list.add(new Box()); // Product의 자손이기 때문에 가능하다
복수 타입 변수
타입 지정은 한개만 사용할 수 있는것이 아니다. 만약 여러개의 타입 지정이 필요할 경우 얼마든지 생성이 가능하다.
타입 변수를 콤마(,)를 구분자로 하여 구분하여 복수의 타입 변수를 선언할 수 있다. 복수의 타입 변수를 가진다면 초기화시 복수의 타입을 대입해야 한다.
예시로 HahsMap<K, V>가 있다.
HashMap<String, Product> map = new HashMap<String, Product>();
map.put("자바", new Product());
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {
public V put(K key, V value) { /* 내용 생략*/}
public V get(Object key) {/* 내용 생략*/ }
public V remove(Object key) {/* 내용 생략*/}
}
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {
public Product put(String key, Product value) { /* 내용 생략*/}
public Product get(Object key) {/* 내용 생략*/ }
public Product remove(Object key) {/* 내용 생략*/}
}
제네릭의 선언 및 생성
제네릭 클래스
클래스명 옆에 제네릭 타입 변수가 있다면 제네릭 클래스이다.
제네릭 클래스 선언
class GenericClass <T> {
T element;
GenericClass(T element) {this.element = element;}
T getElement() {return element;}
}
제네릭 클래스 사용
JDK 1.7 버전부터 생성자 부분의 제네릭 타입을 생략할 수 있게 되어 생략이 가능하다.
// 제네릭 클래스 사용시에는 타입변수 자리에 사용할 실제 타입을 명시해야 한다.(생략 가능)
// 제네릭 타입 변수에 문자열 타입 대입
GenericClass<String> gc = new GenericClass<String>("제네릭 클래스"); // new GenericClass("제네릭 클래스")
System.out.println(gc.getElement());
// 제네릭 타입 변수에 정수형 타입 대입
GenericClass<Integer> gc1 = new GenericClass<Integer>(0);
System.out.println(gc1.getElement());
제네릭 인터페이스
클래스뿐만 아니라 인터페이스에도 제네릭을 사용할 수 있다. 제네릭 인터페이스를 구현한 클래스는 오버라이딩한 메서드의 제네릭 타입를 인터페이스의 제네릭 타입에 맞춰야 한다.
제네릭 인터페이스 구현
interface GenericInterface<T>{
public void setElement(T element);
public T getElement();
}
class GenericClass<T> implements GenericInterface<T> {
T element;
@Override
public T getElement() {return element;}
@Override
public void setElement(T element) { this.element = element;}
// public void setElement(int element) { .........}, 다른 메서드이다.
}
제네릭 인터페이스 사용
// 제네릭 타입 변수에 문자열 타입 대입
GenericClass<String> gc = new GenericClass<String>();
gc.setElement("1");
System.out.println(gc.getElement());
// 제네릭 타입 변수에 정수형 타입 대입
GenericClass<Integer> gc1 = new GenericClass<Integer>();
gc1.setElement(2);
System.out.println(gc1.getElement());
다시 정리 필요
람다식 함수형 인터페이스에 많이 사용되나 아직 배우지 않았다.
제네릭 메서드
아래와 같이 제네릭 클래스의 제네렉 타입 변수를 사용하는 메서드는 제네릭 메서드가 아니다. 단순히 제네릭 클래스의 타입 변수를 받아와 반환 타입으로 사용할 뿐인 메서드이다.
class GenericClass<T>{
T element;
...
public T getElement() {return element;} // 제네릭 메서드가 아니다.
...
}
제네릭 메서드 선언
제네릭 클래스와 같이 메선드 선언부에 제네릭 타입변수가 선언된 메서드를 제네릭 메서드라 한다. 제네릭 클래스의 타입변수와 독립적으로 타입을 받아와 사용할수 있는 메서드를 말한다.
제네릭 클래스의 타입 매개변 수<T>와 제네릭 메서드의 타입 매개 변수<T>는 별개로 제네릭 메서드의 타입 매개 변수는 메서드 내에서만 유효하다.
class GenericClass<T>{
T element;
// 제네릭 클래스의 일반 메서드,
// 제네릭 클래스의 타입 매개 변수를 가져와 사용
public T setElement(T element) {
}
// 제네릭 메서드
// 독립적인 메서드의 타입 매개 변수
public static <T> T ReturnClass(T x){
}
}
제네릭 메서드의 사용
제네릭 메서드의 호출시 메서드 왼쪽에 제네릭 타입을 대입해야 하며 메서드를 호출할 때마다 타입을 대입해야 한다.
GenericClass.<String>ReturnClass("10");
하지만 제네릭 메서드의 타입 변수에 대입될 타입을 컴파일러가 메서드의 매개변수를 통해 추정할 수 있어 대부분이 생략 가능하다.
GenericClass.ReturnClass("10");
GenericClass.ReturnClass(10);
메서드를 호출할 때 타입을 생략하지 않을 때는 클래스 이름의 생략이 불가능하다.
<String>ReturnClass("10"); // 에러, 클래스 이름 생략 불가
GenericClass.<String>ReturnClass("10");
메서드 호출시 클래스의 제네릭 매개 변수와 메서드의 매개 변수의 이름이 같을 때는 제네릭 클래스의 객체 생성시 대입한 타입에 따라 메서드 타입이 정해지게 된다. 만약 메서드 호출시 따로 타입을 대입준다면 클래스와 다른 타입을 가진 제네릭 메서드가 된다.
public class Main {
public static void main(String[] args) {
GenericClass<String> gc = new GenericClass<String>();
gc.ReturnClass("10"); // 제네릭 클래스의 인스턴스에 따라 타입이 정해진다.
gc.<Integer>ReturnClass(10); // 다른 타입을 대입하게 된다면 클래스와 다른 타입을 가진 제네릭 메서드가 된다.
}
}
class GenericClass<T>{ // 제네릭 클래스, 클래스의 타입 매개변수
T element;
// 메서드
public T setElement(T element) {.....}
// 제네릭 메서드
public <T> T ReturnClass(T x){
System.out.println(x.getClass().getSimpleName());
return x;
}
}
FruitBox <Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox< Apple> appleBox = new FruitBox<Apple>();
// 객체 생성시 명시해 두었기 때문에 타입을 생략이 가능하다.
// System.out.println(Juicer.<Fruit>makejuice(fruitBox));
// System.out.println(Juicer.<Apple>makejuice(appleBox));
System.out.println(Juicer.makejuice(fruitBox));
System.out.println(Juicer.makejuice(appleBox));
class Juicer {
// 지네릭 메서드
static <T extends Fruit> Juice makejuice(FruitBox<T> box) {
String tmp = "";
for(Fruit f : box.getList())
tmp += f+ " ";
return new Juice(tmp);
}
}
// 같은 동작을 하나 와일드 카드를 사용한 것으로
// 하나의 참조변수로 서로 다른 타입이 대입된 여러 지네릭 객체를 다루기 위한 것이다.
// 지네릭 메서드호출할 때마다 다른 지네릭 타입을 대입할 수 있게 한 것이다.
static Juice makejuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList())
tmp += f+ " ";
return new Juice(tmp);
}
제네릭 클래스의 타입 제한
제네릭 클래스의 제한이 없다면 컴파일 시 오류를 발견할 수 있지만 목적과는 다른 타입도 대입될 수 있다는 문제가 있다. 이를 해결하기 위해 사용하는 것이 extends키워드로 대입할 수 있는 타입을 제한할 수 있다.
타입 변수(<T>) extends 키워드와 [제한타입]을 추가하여 타입 대입시 제한타입과 그 하위 타입들만 대입할 수 있도록 범위를 제한할 수 있다.
<T extends [제한타입]>
- Product 클래스와 그 하위 타입들만 대입할 수 있도록 타입을 제한한다.
class Box<T extends Product>{
T item;
void setItem (T item) { this.item = item;}
T getItem() { return item;}
}
class Product{};
class Tv extends Product{};
class Studnet{};
Box<Tv> b = new Box();
Box<Studnet> s = new Box(); // 에러, Studnet는 Product의 자손이 아님
- 인터페이스를 구현한 클래스들만 대입할 수 있도록 타입을 제한하려 할때도 마찬가지로 extends 키워드를 사용한다.
interface Product{};
class Tv implements Product{};
class Box<T extends Product>{ ... }
- 만약 2개 이상의 타입을 동시에 상속, 구현 한 타입으로 타입을 제한하고 싶다면 & 연산자를 사용한다.
다중 상속이 불가능 하기 때문에 클래스의 다중 extends 키워드 사용은 불가능 하고 인터페이스 다중 사용이 가능하다.
interface Product{};
interface Appliances{};
class Home{};
class HomeAppliances extends Home implements Appliances, Product{};
class Box<T extends Home & Appliances & Product>{
T item;
void setItem (T item) { this.item = item;}
T getItem() { return item;}
}
다시 정리 필요
재귀적 타입 한정
자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 자신의 타입을 포함하는 상위 모듈에 허용 범위를 한정시키는 것을 말한다. 주로 Comparable 인터페이스와 함께 사용한다.
<E extends Comparable<E>>
E를 중첩 시켜 제네릭<E> 의 타입을 Comparable<E>를 구현한 타입으로 한정시키는 것과 같이 사용하는데 자기 자신을 타입변수로 Comparable을 구현한 구현체로 한정한다는 뜻으로 대입된 타입<E>는 Comparable<E>를 구현한 객체 E여야 한다는 것으로 자기 자신만을 타입으로 받는 다는 표현식이다.
Comparable 인터페이스는 자신과 파라미터로 받은 객체를 비교하는데 사용되는 메서드인 compareTo() 추상 메서드를 가지고 있다. Comparable 인터페이스를 구현한 구현체는 자신과 파라미터로 받은 객체의 대소관계를 비교할 수 있음을 의미한다. 그러나 자신과 다른 전혀 다른 타입과의 대소관계의 비교는 어렵기 때문에 Comparable 인터페이스를 구현하는 클래스는 대부분은 자신의 타입과 동일한 타입의 객체와만 비교 할 수 있도록 구현한다.
제네릭의 제약
- static멤버에 타입 변수 사용 불가능하다.
타입 변수에 대입은 인스턴스 별로 다르게 가능하다. 인스턴스는 각 객체가 사용하지만 static 멤버는 모든 인스턴스에서 공통으로 사용하기 때문이다.
class Box<T extends Home & Appliances & Product>{
static T No; // 에러
T item;
void setItem (T item) { this.item = item;}
T getItem() { return item;}
}
- 객체나 배열을 생성할 때 타입 변수(T) 사용불가. 타입 변수로 배열 선언은 가능하다.
new 연산자 다음 제네릭 타입 변수의 사용이 불가능하다.
class Box<T extends Home & Appliances & Product>{
T[] itemArr; // T타입의 배열을 위한 참조변수
T t = new T(); // 에러, T 타입을 지정하여 객체를 생성하는 것은 불가능
T[] tmpArr = new T[itemArr.length]; // 에러, 제네릭 배열 생성 불가
}
- 제네릭 배열을 생성할 수 없다.
런타입에 실체화 되는 배열의 타입을 제네릭으로 하기 때문에 타입 안전성을 보장할 수 없게 된다.
배열은 공변, 제네릭은 불공변
- 클래스 Sub이 Super 클래스의 하위 타입이라 했을 때
배열 : Sub[]은 배열 Super[]의 하위 타입으로 공변이다.
제네릭 : ArrayList<Sub>는 ArrayList<Super>의 하위 타입이 아니며 불공변이다.
Object[] objects = new String[1]; // String[]은 Object[]의 하위이므로 공변, 컴파일 가능
ArrayList<Object> objectList = new ArrayList<String>(); // 제네릭 타입은 불공변, 컴파일 불가
- 배열은 런타임에 실체화, 제네릭 타입은 런타임에 소거
배열
Object[] objects = new String[1];
배열은 런타임에 실체화 되기 때문에 Objects는 런타임에 String[]이 된다.
제네릭
// 컴파일
ArrayList<String> sList = new ArrayList<String>();
ArrayList<Integer> iList = new ArrayList<Integer>();
// 런타임
ArrayList sList = new ArrayList();
ArrayList iList = new ArrayList();
제네릭 타입은 런타임에 소거되어 런타임에는 타입이 소거되어 구분이 불가능해진다.
제네릭 배열의 생성이 가능하다면 아래와 같은 상황이 발생할 수 있어 런타인 예외가 발생한다.
// 런타임에 제네릭이 소거되므로 ArrayList[]가 된다.
ArrayList<String>[] stringLists = new ArrayList<String>[1];
// 런타임에 제네릭이 소거되어 ArrayList가 된다.
ArrayList<Integer> intList = new ArrayList();
intList.add(1);
// 배열은 공변이기 때문에 Object[]는 ArrayList[]가 될 수 있다.
Object[] objects = stringLists;
// intList또한 ArrayList이므로 배열의 요소가 될 수 있다.
objects[0] = intList;
// String 타입을 가져야 하지만 Integer이므로 오류가 발생한다.
String s = stringLists[0].get(0);
제네릭 클래스 배열의 생성은 불가능 하지만 제네릭 클래스 타입의 참조변수는 허용한다.
ArrayList<String>[] Lists = new ArrayList<String>[1]; // 제네릭 클래스 배열 생성 불가
ArrayList<String>[] stringLists = new ArrayList[1]; // 제네릭 클래스 타입의 참조변수는 허용
제네릭 형변환
배열은 공변이라 했듯이 배열과 같은 일반적인 변수 타입끼리의 형변환은 가능하나 제네릭은 불공변으로 제네릭 서브 타입 간에는 형변환이 불가능하다.
// 가능, 일반적인 배열 Object로 형변환 가능
Object [] arrBox = new Box[10];
// 불가능, 대입된 타입이 다른 제네릭 객체
Box<Object> objBox = new Box<String>();
- 제네릭 서브 타입과 제네릭 원시 타입 간의 형변환은 가능하나 바람직하지 않다.
Box<Object> objBox = null;
// 경고 발생
Box box = (Box)objBox; // 가능, 지네릭 타입 -> 원시타입
objBox = (Box<Object>)box; // 가능, 원시 타입 -> 제네릭 타입
- 다른 제네릭 서브 타입간의 형변환은 불가능하다.
Box<String> strBox = null;
Box<Object> objBox = null;
objBox = (Box<Object>)strBox; // 에러, Box<String> -> Box<Object>
strBox = (Box<String>)objBox; // 에러, Box<Object> -> Box<String>
배열과 같은 일반적인 타입을 사용하였다면 업캐스팅/다운캐스팅이 가능하겠지만 제네릭 타입을 사용한다면 제네릭 서브 타입이 똑같을 경우에만 파라미터로 받을 수 있게된다. 이런 제네릭의 형변환 문제를 해결하기 위해서는 와일드 카드를 사용해야 한다.
- 와일드 카드가 사용된 제네릭 타입으로는 형변환 가능하다.
// Box<String> -> Box<? extends Object>
Box<? extends Object> wBox = (Box<? extends Object>)new Box<String>();
Box<? extends Object> wBox = new Box<String>(); // 형변환 생략가능
// Box<? extends Object> -> Box<String>
Box<String> sBox = (Box<String>)wBox; // 가능, 경고 발생
Box<String> sBox = wBox; // 에러, 형변환 생략 불가능
와일드 카드
와일드 카드는 하나의 참조 변수로 대입된 타입이 다른 객체를 참조 가능하게 해준다.
<? extends T> : 와일드 카드의 상위 클래스 제한. T와 그 자손들만 가능
<? super T> : 와일드 카드의 하위 클래스 제한, T와 그 조상들만 가능
<?> : 제한 없음, 모든 클래스나 인터페이스가능. <? extends Object>와 동일
class Fruit{}
class Apple extends Fruit{}
class Banana extends Fruit{}
class FruitBox <T extends Fruit> extends Box<T>{}
class Box<T> { ...... }
- 일반적으로 지네릭 클래스는 참조변수와 생성자에 대입된 타입이 일치해야 한다.
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>(); 참조변수와 생성자에 대입된 타입이 일치한다.
FruitBox<Fruit> appleBox = new FruitBox<Apple>(); 참조변수와 생성자에 대입된 타입이 일치하지 않는다.
- 와일드 카드를 사용한다면 참조변수와 생성자에 대입된 타입이 일치하지 않아도 된다.
FruitBox<? extends Fruit> appleBox = new FruitBox<Apple>();
// Fruit와 그 자손들을 의미 하며 Fruit의 자손들이라면 참조가 가능해진다.
- 메서드의 매개변수에도 와일드 카드를 사용이 가능하다.
class Juicer {
static Juice makejuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList())
tmp += f+ " ";
return new Juice(tmp);
}
}
제네릭 메서드
제네릭 타입이 선언된 메서드 ( 타입 변수는 메서드 내에서만 유효)
static <T> void sort(List<T> list, Comparator<? super T>) c){};
클래스의 타입 매개변수<T>와 메서드의 타입 매개변수수 <T>는 별개
class FruitBox <T>{ // 제네릭 클래스, 클래스의 타입 매개변수
static <T> void sort(List<T> list, Comparator<? super T>) c){
// 메서드, 메서드의, 타입 매개변수
........
};
}
지네릭 메서드는 메서드를 호출할 때마다 타입을 대입해야 한다.(대부분 생략 가능)
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox< Apple> appleBox = new FruitBox<Apple>();
// 객체 생성시 명시해 두었기 때문에 타입을 생략이 가능하다.
// System.out.println(Juicer.<Fruit>makejuice(fruitBox));
// System.out.println(Juicer.<Apple>makejuice(appleBox));
System.out.println(Juicer.makejuice(fruitBox));
System.out.println(Juicer.makejuice(appleBox));
class Juicer {
// 지네릭 메서드
static <T extends Fruit> Juice makejuice(FruitBox<T> box) {
String tmp = "";
for(Fruit f : box.getList())
tmp += f+ " ";
return new Juice(tmp);
}
}
// 같은 동작을 하나 와일드 카드를 사용한 것으로
// 하나의 참조변수로 서로 다른 타입이 대입된 여러 지네릭 객체를 다루기 위한 것이다.
// 지네릭 메서드호출할 때마다 다른 지네릭 타입을 대입할 수 있게 한 것이다.
static Juice makejuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList())
tmp += f+ " ";
return new Juice(tmp);
}
메서드를 호출할 때 타입을 생략하지 않을 때는 클래스 이름을 생략이 불가능하다.
System.out.println(<Fruit>makejuice(fruitBox)); // 에러, 클래스 이름 생략 불가
System.out.println(this.<Fruit>makejuice(fruitBox));
System.out.println(Juicer.<Apple>makejuice(appleBox));
제네릭 타입의 형변환
제네릭 타입과 원시 타입 간의 형변환은 가능하나 바람직하지 않다.
Box<Object> objBox = null;
// 경고 발생
Box box = (Box)objBox; // 가능, 지네릭 타입 -> 원시타입
objBox = (Box<Object>)box; // 가능, 원시 타입 -> 제네릭 타입
다른 제네릭 타입간의 형변환은 불가능하다.
Box<String> strBox = null;
objBox = (Box<Object>)strBox; // 에러, Box<String> -> Box<Object>
strBox = (Box<String>)objBox; // 에러, Box<Object> -> Box<String>
와일드 카드가 사용된 제네릭 타입으로는 형변환 가능하다.
// Box<String> -> Box<? extends Object>
Box<? extends Object> wBox = (Box<? extends Object>)new Box<String>();
Box<? extends Object> wBox = new Box<String>(); // 형변환 생략가능
// Box<? extends Object> -> Box<String>
Box<String> sBox = (Box<String>)wBox; // 가능, 경고 발생
Box<String> sBox = wBox; // 에러, 형변환 생략 불가능
제네릭 타입의 제거
제네릭스는 JDK 부터 도입되었기 때문에 이전 버전 자바에서는 제네릭 타입 파라미터를 사용하지 않았다. 따라서 이전 버전 자바와의 하위 호환성을 위해 컴파일 시 컴파일러는 지네릭 타입을 제거하고, 필요한 곳에 형변환을 넣는다.
(1) 지네릭 타입의 경계(bound)를 제거
지네릭 타입<T>은 기본적으로 Object로 변환화나 제한을 두었기 때문에 Fruit로 변환
(2) 지네릭 타입 제거 후에 타입이 불일치하면, 형변환을 추가
get의 반환타입이 Object기 때문에 형변환 필요
(3) 와일드 카드가 포함된 경우, 적절한 타입으로 형변환 추가
'Java' 카테고리의 다른 글
[JAVA] 애너테이션(Annotation) (0) | 2024.01.10 |
---|---|
[JAVA] 열거형(enum) (0) | 2024.01.05 |
[JAVA] Collections 클래스 (0) | 2023.11.24 |
[JAVA] Map 인터페이스 (0) | 2023.11.11 |
[JAVA] Set 인터페이스 (0) | 2023.11.10 |