: 컴파일 시 타입을 체크해주는 기능
ArrayList<Tv> tvList = new ArrayList<>();
tvList.add(new Tv());
tvList.add(new DVD()); // 컴파일 에러
<Tv>
: ArrayList에 Tv 객체 타입만 저장할 수 있게 설정해주는 제네릭스이다.객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여준다.
ArrayList list = new ArrayList();
list.add(10);
list.add("20"); // String 추가
Integer i = (Integer) list.get(1); // 컴파일 OK
(Integer) list.get(1)
은 컴파일 에러가 나지 않는다. get()
메서드의 반환 타입이 Object라서 그 안의 값이 String 타입인지 Integer 타입인지 컴파일러가 구분할 수 없기 때문이다.ClassCastException
예외가 발생하게 된다. 실행 시에 발생하는 예외를 제네릭스들 통해 컴파일에게 타입의 정보를 알려줌으로써 컴파일 때 예외를 체크할 수 있게 해준다. 이로써, 객체의 타입 안정성을 높일 수 있다.ArrayList<Tv> tvList = new ArrayList<>();
tvList.add(new Tv());
Tv t = tvList.get(0);
get()
메서드는 반환 타입이 Object라서 Tv t = (Tv) tvList.get(0);
이런 식으로 (Tv)를 붙여 형변환을 해줘야 한다.클래스를 작성할 때, Object 타입 대신 타입 변수<E>
를 선언해서 사용
객체 생성 시, 타입 변수 대신 실제 타입을 대입
public class ArrayList<E> ... {
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
...
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
}
ArrayList<E>
클래스 안의 add(E e)
메서드는 매개변수가 타입 변수 <E>
로 되어 있고 public E get(int index)
메서드는 반환 타입이 타입 변수 <E>
로 되어 있다. 참조 변수와 생성자의 대입된 타입은 일치해야 한다.
class Product {}
class Tv extends Produce {}
...
ArrayList<Tv> list = new ArrayList<Tv>();
ArrayList<Product> list = new ArrayList<Tv>(); // 에러
제네릭 클래스간의 다형성은 성립된다.
List<Product> list = new ArrayList<Product>();
List<Product> list = new LinkedList<Product>(); // OK
매개변수의 다형성도 성립된다.
ArrayList<Product> list = new ArrayList<Product<();
list.add(new Product());
list.add(new Tv()); // OK
list.add(new Tv());
: 매개변수 타입이 Product이기 때문에 다형성에 의해 그 자손들도 매개변수 타입으로 올 수 있다.반환타입의 다형성 성립(형변환 필요)
ArrayList<Product> list = new ArrayList<Product<();
list.add(new Product());
list.add(new Tv()); // OK
Product p = list.get(0);
Tv t = (Tv) list.get(1);
Tv t = (Tv) list.get(1);
: list.get(1)의 반환타입은 Product기 때문에 (Tv)로 형변환을 해줘야 한다.extends로 대입할 수 있는 타입을 제한
class FruitBox<T extends Fruit> {}
...
interface Eatable {}
class FruitBox<T extends Eatable> {}
<T extends Fruit>
: Fruit과 Fruit의 자손만 타입으로 지정할 수 있게 제한<T extends Eatable>
: 타입을 제한할 때 인터페이스도 implements
가 아닌 extends
를 쓴다.(주의)타입 변수에 대입은 인스턴스별로 다르게 가능하다.
class FruitBox<T extends Fruit> {}
class Fruit {}
class Apple extends Fruit {}
class Grape extends Fruit {}
...
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>(); // OK
FruitBox<Grape> grapeBox = new FruitBox<>(); // OK
static 멤버에는 타입 변수 사용 불가
class Box<T> {
static T item; // 에러
static void add(T item) {} // 에러
}
객체(배열) 생성할 때 타입 변수 사용 불가. 타입 변수로 배열 선언은 가능
T[] itemArr; // OK
T[] tmpArr = new T(itemArr.length); // 사용 불가
<?>
class Product {}
class Tv extends Product {}
...
ArrayList<? extends Product> list = new ArrayList<Tv>(); // OK
ArrayList<? super Tv> list = new ArrayList<Product>(); // OK
ArrayList<?> list = new ArrayList<Object>();
<? extends T>
: T와 그 자손들만 가능<? super T>
: T와 그 조상들만 가능<?>
: 제한 없음. <? extends Object>
와 동일메서드의 매개변수에 와일드 카드 사용
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box) {}
...
}
...
Juicer.makeJuice(new FruitBox<Fruit>()); // OK
Juicer.makeJuice(new FruitBox<Apple>()); // OK
: 제네릭 타입이 선언된 메서드(타입 변수는 메서드 내에서만 유효)
class FruitBox<T> {
static <T> void sort(List<T>) {}
...
}
FruitBox<T>
의 타입변수 <T>
와 메서드 static <T> void sort(List<T>)
의 타입변수 <T>
은 별개의 타입변수이다.메서드를 호출할 때마다 타입을 대입해야 한다.(대부분 생략 가능)
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {}
...
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));
makeJuice(FruitBox<T> box)
: makeJuice 메서드는 <T extends Fruit>
타입을 제한하고 있기 때문에 FruitBox<T>
에서 <T>
에는 Fruit과 그 자손만 올 수 있다.
Juicer.<Fruit>makeJuice(fruitBox)
여기서 <Fruit>
생략 가능하다.
참조변수 fruitBox 생성할 때 이미 <Fruit>
타입인지 알고있기 때문이다.
// 와일드카드가 사용된 메서드
static Juice makeJuice(FruitBox<? extends Fruit> box) {}
// 제네릭 메서드
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {}
와일드카드가 사용된 메서드는 하나의 참조변수로 서로 다른 타입이 대입된 여러 제네릭 객체를 다루기 위한 것
제네릭 메서드는 메서드를 호출할 때마다 다른 제네릭 타입을 대입할 수 있게 한 것
둘은 서로 다른 메서드지만 기능은 똑같다. 그래서 와일드카드를 쓸 수 없을 때 제네릭 메서드를 사용한다.
제네릭 타입과 원시 타입간의 형변환은 가능하지만 바람직하지 않다.(경고 발생)
Box b = null;
Box<String> bStr = null;
b = (Box) bStr; // 가능하지만 경고 발생
bStr = (Box<String>) b; // 가능하지만 경고 발생
제네릭 타입 간의 형변환 불가능. 와일드 카드가 사용된 제네릭 타입은 형변환 가능
Box<Object> bObj = new Box<String>(); // 에러
Box<? extends Object> bWild = new Box<String>(); // OK
Box<? extends Object> bWild = new Box<String>();
이 문장은 양변의 타입이 불일치하기 때문에 형변환을 해줘야한다.Box<? extends Object> bWild = (Box<? extends Object>) new Box<String>();
이 문장에서 형변환해주는 (Box<? extends Object>)
이게 생략되어있는 것이다.컴파일러는 제네릭 타입을 제거하고, 필요한 곳에 형변환을 넣는다.
class Box<T extends Fruit> {
void add(T t) {};
}
컴파일🔽
class Box {
void add(Fruit t) {};
}
기본적으로는 컴파일 하면 <T>
-> Object로 바뀌지만 제한된 제네릭 타입은 조상 타입으로 바뀐다.
자바는 하위호환성(안정성)을 중요하게 생각하기 때문에 제네릭스가 나오기 전에 Object를 쓴 것처럼 컴파일을 하면 컴파일러가 제네릭 타입을 제거하고 Object로 바꾼다.
T get(int i) {
return list.get(i);
}
컴파일🔽
Fruit get(int i) {
return (Fruit)list.get(i);
}