제네릭이란?
- 컴파일시 타입 체크를 강화 해주는 기능 ->실행 시 에러를 어떻게 컴파일러로 끌어올까? 에 대한 결과 중 하나가 제네릭
- 객체 타입 안정성을 높이고 형변환의 번거로움을 줄여준다.
// Tv객체만 저장할 수 있는 ArrayList를 생성 (ArrayList는 Object 타입)
ArrayList<Tv> = new ArrayList<Tv>();
tvList,add(new tv()); //OK
tvList.add(new Audio()); // 컴파일 에러(형변환 에러). tv 외의 다른 타입은 저장 불가
Tv t = tvList.get(0); //제네릭 사용 시 형변환 불필요
//제네릭을 사용하지 않을 시
Tv t = (Tv)tvList.get(0); //Object 타입 요소 꺼낼 시 형변환 필요
제네릭의 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 체크할 수 있으므로 코드가 간결해진다.
타입 변수
- Object를 포함한 클래스를 작성할 때, Object 타입 대신 타입 변수(E)를 선언해서 자료형으로 사용.
- 타입 변수로는 아무 문자나 가능, 대문자 한 글자를 쓰는 게 보통 (T 또는 E)
- 객체 생성 시 타입 변수 대신 실제 타입을 지정(대입)
//JDK 1.5 이후부터 Object 타입을 타입 변수로 작성한다
public class ArrayList<e> extends AbstractList<E> { // 일부 생략
private transient E[] elementData;
public boolean add(E o) { 생략 }
public E get(int index) { 생략 }
...
}
//객체 생성 시
public class ArrayList extends AbstractList<E> { // 일부 생략
private transient Tv[] elementData;
public boolean add(Tv o) { 생략 }
public Tv get(int index) { 생략 } //Object가 아닌 Tv를 반환
...
}
제네릭 용어
Box<T> | 제네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다. |
T | 타입 변수 또는 타입 매개변수. (T는 타입문자) |
Box | 원시 타입(raw type) |
제네렉 타입과 다형성
class Product {}
class tv extends Product {}
class Audio extends Product {}
- 참조 변수와 생성자의 대입된 타입은 일치해야 한다
ArrayList<Tv> list = new ArrayList<Tv>(); //OK. 일치
ArrayList<Product> list = new ArrayList<Tv>(); //에러. 불일치
- 제네릭 클래스간의 다형성은 성립.(여전히 대입된 타입은 일치히야함)
List<Tv> list = new ArrayList<Tv>(); //OK. ArrayList가 List를 구현
List<Tv> list = new LinkedList<Tv>(); //Ok. LinkedList가 List를 구현
- 매개변수의 다형성도 성립.
ArrayList<Product> list = new ArrayList<Tv>(Product);
list.add(new Product());
list.add(new Tv()); // Product의 자손 Ok
list.add(new Audio());
- JDK 1.7부터 타입 선언 시 타입 변수가 작성 되면 타입 추론이 가능하기 때문에 생성자 쪽의 타입을 생략하고 작성할 수 있다. 단, 빈 다이아몬드 연산자는 사용해야 한다.
ArrayList<Product> list = new ArrayList<>();
제한된 제네릭 클래스
- extends로 대입할 수 있는 타입을 제한. 인터페이스인 경우에도 extends를 사용한다.
class FruitBox<T extends Fruit> { // Fruit의 자손만 타입으로 지정가능
ArrayList<T> list = new ArrayList<T>();
...
}
FruitBox<Apple> applebox = new FruitBox<Apple>();
FruitBox<Toy> toybox = new FruitBox<Toy>(); //Toy는 Fruit의 자손이 아니므로 에러
와일드카드 <?>
<? extends T> 와일드카드의 상한 제한. T와 그 자손들만 가능 -> 가장 많이 쓰임
<? super T> 와일드카드의 하한 제한. T와 그 조상들만 가능
<?> 제한 없음. 모든 타입이 가능. <? extends Object>와 동일
- 하나의 참조 변수로 대입된 타입이 다른 객체를 참조 가능
ArrayList<? extends Product> list = new ArrayList<Tv>();
ArrayList<? extends Product> list = new ArrayList<Audio>();
ArrayList<Product> list = new ArrayList<Tv>(); // 에러. 대입된 타입 불일치
- 메서드의 매개변수에 와일드 카드를 사용
static Juice makeJuice(FruitBox<? extends Fruit> box) {
...
apple, grape 등등 모두 사용 가능
제네릭의 제약
- 타입 변수 대입은 인스턴스 별로 다르게 가능
Box<Apple> applebox = new Box<Apple>();
Box<Grape> grapebox = new Box<Grape>();
- static멤버에 타입 변수 사용 불가
->static 은 모든 인스턴스의 공통
- 배열 생성할 때 타입변수 사용 불가. 타입 변수로 배열 선언은 가능
-> new 연산자 다음에 사용할 수 없다. new는 타입이 확정된 다음에 사용 가능함.
class Box<T> {
T[] itemArr; // OK, T타입의 배열을 위한 참조변수
...
T[] toArray() {
T[] tmpArr = new T [itemarry length]; //에러, 제네릭 배열 생성 불가
...
제네릭 메소드
- 제네릭 타입이 선언된 메소드(타입 변수는 메소드 내에서만 유효)
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> applebox = new FruitBox<Fruit>();
...
Juicer.<Fruit>makeJuice<Fruit>();
Juicer.makeJuice<Fruit>(); // 생략 가능
제네릭 타입의 형변환
- 지네릭 타입과 원시 타입 간의 형변환은 바람직 하지 않다. (경고 발생)
Box<Object> objBox = null;
Box<String> strBox = null;
Box box = (Box)objBox; //Ok, 제네릭 타입 -> 원시타입 경고 발생
objBox = (Box<Object>)box //Ok, 원시 타입 -> 제네릭 타입 경고 발생
objBox = (Box<Object>)strBox; //에러 Box<String> -> Box<Object>
strBox = (Box<Object>)objBox; //에러 Box<Object> -> Box<String>
- 와일드 카드가 사용된 제네릭 타입으로는 형변환 가능
Box<Object> objBox = (Box<Object>)new Box<String>(); // 에러. 형변환 불가능
Box<? extends Object> wBox = (Box<? extends Object>)new Box<String>(); //OK
Box<? extends Object> wBox = new Box<String>(); //위 문장과 동일. (Box<? extends Object>) 생략 가능
// 매개변수로 FruitBox<Fruit>, FruitBox<Apple>, FruitBox<Grape> 등이 가능
static Juice makeJuice(FruitBox<? extends Fruit> box) { ...}
FruitBox<? extends Fruit> box = new FruitBox<Fruit>();
FruitBox<? extends Fruit> box = new FruitBox<Apple>();
제네릭 타입의 제거
- 컴파일러는 제네릭 타입을 제거하고, 필요한 곳에 형변환을 넣는다.
-> 하위호환성을 위해(제네릭 타입 도입 이전 버전과의)
-> 우리에게 불필요해서 안 보일뿐이지 형변환은 필요하다.
1. 제네릭 타입의 경계(bound)를 제거.
-> 컴파일하면 <T>가 다시 Object로 변환, 제한된 경우엔 Objectrk Fruit로 변환
class Box<T extends Fruit> {
void add(T t) {
...
}
}
class Box {
void add(Fruit t) {
...
}
}
2. 제네릭 타입 제거 후에 타입이 불일치하면, 형변환을 추가
T get(int i) {
return list.get(i)
}
Fruit get(int i) {
return (Fruit)list.get(i);
}
3. 와일드 카드가 포함된 경우, 적절한 타입으로 형변환 추가
남궁성의 자바의 정석 강의 정리