컴파일시 타입을 체크해주는 기능으로 객체의 타입 안정성을 높이고, 형변환의 번거로움을 줄여준다.
//Tv 객체만 저장할 수 있는 ArrayList 생성
ArrayList<Tv> tvList = new ArrayList<Tv>();
tvList.add(new Tv());
tvList.add(new Audio()); //컴파일 에러
위의 예제와 같이 지정된 객체 외 다른 객체가 들어오면 컴파일러가 막아준다. 런타임에러(실행 시 발생하는 에러)를 컴파일 에러로 변경해 사전에 예방할 수 있다.
ArrayList tvList = new ArrayList();
tvList.add(new Tv());
Tv t = (Tv)tvList.get(0); //get()의 반환 타입이 Object이기 때문에 형변환 필요
ArrayList<Tv> tvList = new ArrayList<Tv>();
tvList.add(new Tv());
Tv t = tvList.get(0); //형변환 불필요
지네릭스를 사용하지 않은 경우 get()의 반환 타입은 Object이기 t의 타입은 Tv이기 때문에 형변환이 빌표한 반면, 지네릭스를 사용하는 경우는 반환타입도 Tv이기 때문에 형변환이 필요없다.
지네릭스의 장점
- 타입의 안정성을 제공한다.
- 타입체크의 형변환을 생략할 수 있으므로 코드가 간결해 진다.
Box<T> : 지네릭 클래스, 'T의 Box' 또는 'T Box'라고 읽는다.
T : 타입 변수 또는 타입 매개변수(T는 타입문자)
Box : 원시 타입
class Box<T> { }
Generics 클래스를 작성할 때, Object 타입 대신 타입 변수(E)를 선언해서 사용한다.
public class ArrayList<E> extends AbstractList<E> {
private transient E[] elementData;
public boolean add(E o) { ... }
public E get(int index) { ... }
...
}
객체를 생성할 때, 타입 변수(E) 대신 실제 타입을 지정(대입)하면 된다.
ArrayList<Tv> tvList = new ArrayList<Tv>();
참조 변수와 생성자의 대입된 타입은 일치해야 한다.
class Product { }
class Tv extends Product { }
ArrayList<Product> list = new ArrayList<Tv>(); //에러
위와 같이 Product 클래스가 Tv 클래스의 조상 클래스여도 에러가 발생한다. 참조 변수와 생성자의 대입된 타입은 일치해야한다.
단, 지네릭 클래스간의 다형성은 성립한다.
List<Tv> list = new ArrayList<Tv>();
매개변수의 다형성도 성립한다.
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv()); //Ok
위와 같이 매개변수의 경우 Product의 자손들이 와도 성립한다.
클래스를 작성할 때, Object 타입 대신 T와 같은 타입 변수 사용
//지네릭스 사용 전
public interface Iterator {
boolean hasNext();
Object next();
void remove();
}
//지네릭스 사용 후
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
//지네릭스 사용 전
Iterator it = list.iterator();
while(it.hasNext()) {
Student s = (Student)it.next();
...
}
//지네릭스 사용 후
Iterator<Student> it = list.iterator();
while(it.hasNext()) {
Student s = it.next();
...
}
Oject 타입이 E로 바뀌었기 때문에 형변환을 해주지 않아도 된다.
여러 개의 타입 변수가 필요한 경우, 콤마(,)로 구분자 선언
public class HashMap<K, V> extends AbstractMap<K, V> {
...
public V get(Object key) { ... }
public V put(K key, V value) { ... }
public V remove(Object key) { ... }
...
}
HashMap<String, Student> map = new HashMap<String, Student>(); //생성
map.put("자바", new Student("자바", 1, 1, 100, 100, 100)); //데이터 저장
public class HashMap extends AbstractMap {
...
public Student get(Object key) { ... }
public Student put(String key, Student value) { ... }
public Student remove(Object key) { ... }
...
}
extends로 대입할 수 있는 타입을 제한한다(인터페이스인 경우에도 extends를 사용한다)
class FruitBox<T extends Fruit> {
ArrayList<T> list = new ArrayList<T>();
}
위의 예제는 Fruit의 자손만 T의 타입으로 지정가능하게 제한 것과 같다.
class Box<T> {
static T item; //error
static int compare(T t1, T t2) { ... } //error
}
class Box<T> {
T[] itemArr; // 선언할 때 ok
T[] temArr = new T[10]; //생성할 때 error
}
하나의 참조 변수로 대입된 타입이 다른 객체를 참조 가능하다.
ArrayList<? extends Product> list = new ArrayList<Tv>();
ARrayList<? extends Product> list = new ArrayList<Audio>();
< ? extends T > : 와일드 카드의 상한 제한. T와 그 자손들만 가능
< ? super T > : 와일드 카드의 하한 제한. T와 그 조상들만 가능
< ? > : 제한 없음. 모든 타입이 가능 < ? extends Object >와 동일
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
System.out.println(Juicer.makeJuice(new FruitBox<Apple>()));
지네릭 타입이 선언된 메서드(타입 변수는 메서드 내에서만 유효)이다. 이때 클래스 타입 매개변수 <T>와 메서드의 타입 매개변수 <T>는 별개이다. 지네릭 메서드 내에서는 지네릭 메서드의 타입 변수가 우선이 된다.
static <T> void sort(List<T> list, Comparator<? super T> c)
메서드를 호출하라 때마다 타입을 대입해야하지만 대부분 생략 가능하다. 메서드를 호출할 때 타입을 생략하지 않을 때는 클래스 이름 생략이 불가능하다.
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
...
}
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));
System.out.println(<Fruit>makeJuice(fruitBox)); //error