제네릭스란?
제네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(compile-time type check)를 해주는 기능이다. 타입 안정성을 높인다는 것은 의도하지 않은 타임의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입고 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.
class Box<T>{
T item;
void setITem(T item){ this.item = item; }
T getItem() { return item; }
}
제네릭스의 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.
제네릭스의 제한
- 모든 객체에 대해 동일하게 동작해야하는 static 멤버에 타입 변수 T를 사용할 수 없다. static 멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.
- 제네릭 타입의 배열을 생성하는 것은 허용되지 않는다. 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 'new T[10]'과 같이 배열을 생성하는 것은 안된다. new 연산자와 instanceof 연산자 뒤에 T를 사용할 수 없다. 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다.
제네릭 클래스의 객체 사용
다음의 코드를 보고 가능한지 맞춰보자!
class Box<T> {
ArrayList<T> list = new ArrayList<T>();
void add(T item) { list.add(item); }
T get(int i) { return list.get(i); }
ArrayList<T> getList() { return list; }
int size() { return list.size(); }
public String toSTring() {return list.toString(); }
}
// 1.
Box<Apple> appleBox = new Box<Apple>(); //Ok. 참조변수와 생성자에 대입된 타입(매개변수 타입)일치
// 2.
Box<Apple> appleBox = new Box<Grape>(); // 에러. 참조변수와 생성자 타입 불일치
// 3.
Box<Fruit> appleBox = new Box<Apple>(); // 에러. 대입된 타입 불일치
// 4.
Box<Apple> appleBox = new FruitBox<Apple>(); // Ok. 다형성
// 5.
Box<Apple> appleBox = new Box<>(); // Ok.
// 6.
Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple()); // Ok.
appleBox.add(new Grape()); // 에러. Box<Apple>에는 Apple 객체만 추가 가능
// 7.
Box<Fruit> fruitBox = new Box<Fruit>();
appleBox.add(new Fruit()); // Ok.
appleBox.add(new Apple()); // Ok. void add(Fruit item)
제한된 제네릭 클래스
타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있다. 아래와 같이 제네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
class FruitBox<T extends Fruit>{ // Fruit의 자손만 타입으로 지정 가능
ArrayList<T> list = new ArrayList<T>();
// ...
}
※ 만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 'extends' 를 사용한다. 'implements'를 사용하지 않는다는 점에 주의하자!! 또한, 클래스 Fruit의 자손이면서 Eatable 인터페이스도 구현해야한다면 아래와 같이 '&' 기호로 연결한다.
class FruitBox<T extends Fruit & Eatable> { ... }
와일드 카드
제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 앖는다. 제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다. 그렇기 때문에 제네릭 타입만 다르게 작성한 함수는 오버로딩이 아니라 메서드 중복 정의가 된다. 그래서 이때는 와일드 카드를 사용하면 된다. 와일드 카드는 기호 "?"로 표현하는데, 와일드 카드는 어떠한 타입도 될 수 있다.
정리하자면, 와일드 카드는 하나의 참조변수로 대입된 타입이 다른 다른 객체를 참조할 수 있다. "?"만으로는 Object타입과 다를게 없으므로, 아래와 같이 'extends'와 'super'로 상한과 하한을 제한할 수 있다.
< ? extends T > 와일드 카드의 상한 제한. T와 그 자손들만 가능
< ? super T > 와일드 카드의 하한 제한. T와 그 조상들만 가능
< ? > 제한 없음. 모든 타입이 가능. < ? extends Object >와 동일
제네릭 메서드 - Collections.sort()
sort()는 static 메서드이다. static멤버에는 타입 매개 변수를 사용할 수 없지만, 이처럼 메서드에 제네릭 타입을 선언하고 사용하는 것은 가능하다. 메서드에 선언된 제네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 이해하기 쉽다. 이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이건 아니건 상관 없다.
// ( 정렬 대상 , 정렬 기준 )
static <T> void sort ( List<T> list, Comparator<? super T> C )
제네릭 타입의 형변환
1) 제네릭 타입과 논제네릭(Non-Generics) 타입 간의 형변환은 항상 가능하다. 다만 경고만 발생할 뿐이다.
Box box = null;
Box<Object> objBox = null;
box = (Box)objBox; // 제네릭 타입->원시타입. OK(경고)
objBox = (Box<Object>)box; // 원시타입->제네릭 타입. OK(경고)
2) 하지만 대입된 타입이 다른 제네릭 타입 간에 형변환하는 것은 불가능하다. 대입된 타입이 Object일지라도 말이다.
Box<Object> objBox = new Box<String>(); // 에러. 형변환 불가능
3) 와일드 카드일 때는 형변환 가능하다.
// Box<? extends Object> wBox = (Box<? extends Object>)new Box<String>(); // 뒤에 타입은 생략
Box<? extends Object> wBox = new Box<String>();
=> 그래서 매개변수에 다형성이 적용될 수 있는 것이다. 반대로의 형변환도 성립하지만, 확인되지 않은 형변환이라는 경고가 발생한다. 주의해서 사용하자.
제네릭 타입의 제거
컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 제네릭 타입을 제거한다. 즉, 컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없는 것이다.
그 이유는,
제네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해서이다.
<제네릭 타입 제거 과정>
- 제네릭 타입의 경계(bound)를 제거한다.
- 제네릭 타입이 <T extends Fruit> 라면 T는 Fruit로 치환된다. <T>인 경우는 T는 Object로 치환된다. 그리고 클래스 옆의 선언은 제거된다.
- 제네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
T get(int i){
return list.get(i);
}
// 컴파일
Fruit get(int i){
return (Fruit)list.get(i); // 형변환 추가 됨.
}
[참고]
Java의 정석(남궁성 저)
'공부 > 자바' 카테고리의 다른 글
어노테이션(Annotation) (0) | 2024.07.03 |
---|---|
열거형(Enum) (0) | 2024.07.03 |
컬렉션 프레임워크(Collections Framework) (0) | 2024.06.10 |
예외 처리(Exception handling) (0) | 2024.06.04 |
내부 클래스(inner class) (0) | 2024.06.01 |