JDK1.5에서 처음 도입되었으며, 이제는 지네릭스를 모르고는 Java API 문서조차 제대로 보기 어려울 만큼 중요한 위치를 차지하고 있다.
💡 제네릭스
- 정의 : 다양한 타입의 객체들을 다루는 메서드나 컬렉션에 컴파일 시의 타입 체크 (compile-time type check)를 해주는 기능이다.
- 장점 : 객체의 타입을 컴파일 시에 체크하기 때문에 1) 객체의 타입 안정성을 높이고 2) 형변환의 번거로움이 줄어든다.
1) 타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.
class Box {
private Object obj;
public Object getObj() { return obj; }
public void setObj(Object obj) { this.obj = obj; }
}
class Box <T> {
private T obj;
public T getObj() { return obj; }
public void setObj(T obj) { this.obj = obj; }
}
Box<Apple> aBox = new Box<Apple>();
인스턴스 생성 시 결정이 되는 자료형의 정보를 타입 매개변수 (T)로 대체한다.
class Box <T> {
private T obj;
public T getObj() { return obj; }
public void setObj(T obj) { this.obj = obj; }
}
타입 변수가 여러 개일 때는 콤마를 구분자로 나열하면된다.
Map<K, V> //K는 Key, V는 Value를 의미한다.
static
멤버에 타입 변수 T를 사용할 수 없다.배열
: 제네릭 타입의 배열
을 생성할 수 없다.T[] itemArr
은 가능하지만, new T[10]
과 같이 배열을 생성하는 것은 안 된다.new
연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. instanceof
연산자도 new 연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.Box<Apple> aBox = new Box<Apple>(); //OK
Box<Fruit> fBox = new Box<Apple>(); //error
두 타입이 상속 관계에 있어도 마찬가지이다.
Box<Apple> aBox = new Box<>(); //OK
Box<Fruit> fBox = new Box<Fruit>();
fBox.add (new Fruit());
fBox.add (new Apple());
타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있다. 그러나 여전히 모든 종류의 타입을 지정할 수 있다.
그래서 제네릭 타입에 extends
를 사용하여 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한한다. 아래와 같이 제네릭 타입에 extends
를 사용하면, 인스턴스 생성 시 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
class FruitBox<T extends Fruit> {
ArrayList<T> list = new ArrayList<T>
}
또한 타입이 제한되어 있지 않으면, 메서드 사용이 제한된다.
class Box <T> {
public int toInt() {
return obj.intValue(); //컴파일 에러
}
class Box <T extends Number> {
public int toInt() {
return obj.intValue(); //OK
}
메서드 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라 한다. 제네릭 타입의 선언 위치는 반환 타입 바로 앞이다.
제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다. 같은 타입 문자 T를 사용해도 같은 것이 아니다. 당연히 제네릭 메서드는 제네릭 클래스가 아닌 클래스에도 정의할 수 있다.
또한 제네릭 클래스의 타입 매개변수는 static 멤버에서 사용할 수 없다. 하지만 static 메서드를 제네릭 메서드로 선언하는 것은 가능하다.
Class BoxFactory<T>{
public static <T> Box<T> makeBox(T o){ //class에 정의된 T와 전혀 상관없다.
Box<T> box = new Box<T>();
box.set(o);
return box;
}
}
제네릭 메소드를 호출할 때는 타입 변수에 타입을 대입해야 한다. 하지만 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략해도 된다.
Box<String> sBox = BoxFactory.<String>makeBox("Sweet");
Box<Double> dBox = BoxFactory.<Double>makeBox(7.59);
//타입 인자 생략 가능
Box<String> sBox = BoxFactory.makeBox("Sweet");
Box<Double> dBox = BoxFactory.makeBox(7.59);
한가지 주의할 점은 제네릭 메서드를 호출할 때, 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없다는 것이다. 같은 클래스 내에 있는 멤버들끼리는 참조변수나 클래스 이름을 생략하고 메서드 이름만으로 호출이 가능하지만, 대입된 타입이 있을 때는 반드시 써줘야 한다.
또한 제네릭 메서드는 매개변수의 타입이 복잡할 때도 유용하다.
//변경 전
public static void printAll (ArrayList<? extends Prodect> list, ArrayList<? extends Prodect> list2> {
...
}
//변경 후
public static <T extends Product>void printAll (ArrayList<T> list, ArrayList<T> list2> {
...
}