다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.
타입 파라미터라고 이해하면 쉽다.
우리에게 익숙한 ArrayList 도 지네릭스를 사용한다.
List<String> strList = new ArrayList<String>();
strList.add("string");
strList.add(1); // 컴파일러가 에러를 띄운다. <> 에 선언된 타입과 다르기 때문이다.
간단히 직접 지네릭 클래스를 만들어보자.
class MyClass<T> {
T element;
void setElement(T element){
this.element = element;
}
T getElement(){
return element;
}
}
T 를 타입 파라미터라고 하며 T가 아닌 다른것을 사용해도 된다.
예로 ArrayList는 E 를 사용한다.
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
문자만 다를 뿐 의미는 같다.
MyClass의 객체를 생성할 때는 다음과 같이 타입을 지정해주어야 한다.
MyClass<String> cls = new MyClass<String>();
cls.setElement("element");
cls.setElement(123); // 에러. String 이외의 타입은 지정 불가.
String element = cls.getElement();
MyClass의 T 는 타입변수, MyClass는 원시타입이라고 부른다.
static 멤버에 사용 불가
static 멤버에 타입 변수 T를 사용할 수 없다.
T는 인스턴스 변수로 간주되기 때문이다.
class MyClass<T> {
static T element; // 에러
}
생각해보면 당연하다.
static 멤버는 객체를 생성하지 않고도 사용 가능해야 한다.
그러나 지네릭 클래스의 객체를 생성하기 전에는 T의 타입이 뭔지 알 수가 없다.
지네릭 배열 생성 불가
지네릭 배열 타입의 참조 변수를 선언하는 것은 가능하지만
new T[10] 과 같이 배열을 생성하는 것은 안된다.
class Limit<T> {
T[] elements;
T[] toArray(){
T[] tmpArr = new T[elements.length]; // 에러
...
return tmpArr;
}
...
}
이것은 new 연산자 때문인데, new 연산자는 컴파일 시에 타입 T 가 뭔지 정확히 알아야 한다.
같은 이유로 instanceof 연산자도 new 연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.
지네릭 클래스의 객체를 생성할때,
변수와 객체의 타입변수가 정확히 일치해야 한다.
ArrayList<Parent> list = new ArrayList<Child>(); // 에러
ArrayList<Parent> list = new ArrayList<Child>();
두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다.
Parent<String> a = new Child<String>();
대입된 타입과 다른 타입의 객체는 추가할 수 없다.
List<Car> list = new ArrayList<Car>();
list.add(new Car());
list.add(new Bike()); // 에러
만약 대입된 타입과 추가되는 타입이 상속관계에 있으면 상관 없다.
List<Vehicle> list = new ArrayList<Vehicle>();
list.add(new Car()); // 가능. Car는 Vehicle 의 자손
list.add(new Bike()); // 가능. Bike도 Vehicle의 자손
extends 를 사용하면 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
class MyClass<T extends Vehicle> {
T vehicle;
void setVehicle(T vehicle){
this.vehicle = vehicle;
}
}
만일 MyClass의 타입변수로 Vehicle 의 자손이 아닌 클래스를 지정한다면 에러가 발생한다.
MyClass<Car> cls1 = new MyClass<Car>(); // 가능. Car는 Vehicle의 자손
MyClass<String> cls2 = new MyClass<String>();
// 에러. String 은 Vehicle의 자손이 아니다.
Vehicle 의 자손이면서 Product라는 인터페이스를 구현하는 클래스들만 타입 변수로 허용하고 싶다면 아래와 같이 앰퍼샌드를 쓰면 된다.
class MyClass<T extends Vehicle & Product> {
//...
}
다음 코드를 보자.
class Rider {
String name;
public Rider(String name){ this.name = name; }
void run(Action<Vehicle> action){
System.out.println(name + " is ready to run");
action.run();
}
}
run 메서드에서 Action 객체를 받아 해당 탈것을 운전한다.
그런데 run 의 파라미터를 보면 Action의 타입변수가 Vehicle로 고정되어 버렸다.
위에서 말했듯이 타입 변수는 상속 관계와 상관없이 일치해야한다.
그래서 아래의 코드는 에러가 발생한다.
Rider rider = new Rider("Mike");
Action<Car> carAction = new Action<Car>(new Car("Lamborghini"));
rider.run(carAction);
타입 변수로 무조건 Vehicle만 받고, Car나 Bike와 같은 그 자손은 받을 수 없게 됏다.
Car와 Bike 까지 받아서, 라이더가 자유롭게 차량을 바꾸면서 운전하게 하고 싶으면 어떻게 해야할까?
아래처럼 와일드카드(?) 를 쓰면 해결된다.
class Rider {
String name;
public Rider(String name){ this.name = name; }
//와일드 카드 사용
void run(Action<? extends Vehicle> action){
System.out.println(name + " is ready to run");
action.run();
}
}
와일드 카드로 다음과 같이 상한 제한과 하한 제한을 걸 수 있다.
<? extends T> T와 그 자손들만 가능
<? super T> T와 그 조상들만 가능
<?> 모든 타입이 가능.
예를 들어, Rider 클래스의 run 메서드의 파라미터를 다음과 같이 바꾸면,
Car과 그 조상들 (Vehicle, Object) 만 타입 변수로 지정할 수 있게 된다.
void run(Action<? extends Vehicle> action){
System.out.println(name + " is ready to run");
action.run();
}
추가로, 지네릭 클래스와 달리 와일드 카드에는 &를 사용해 extends 뒤에 복수의 클래스를 지정할 수 없다.
메서드의 선언부에 지네릭 타입이 선언된 메서드다.
예를 들어 Collections.sort()가 있다.
// 지네릭 메서드의 지네릭 타입의 선언 위치는 반환타입 바로 앞이다.
static <T> void sort(List<T> list, Comparator<? super T> c)
위의 Rider 클래스의 run 메서드를 static 지네릭 메서드로 바꾸면 다음과 같다.
class Rider {
static <T extends Vehicle>void run(Action<T> product){
System.out.println("Rider is ready to run");
product.run();
}
}
지네릭 메서드는 스태틱이어도 상관 없다. 메서드 내에서만 지역적으로 사용할 것이기 때문이다.
위의 run 메서드를 사용하려면 메서드를 호출할때 타입을 지정해줘야 한다.
그러나 대부분의 경우 컴파일러가 대입된 타입을 추정할 수 있기 때문에 생략해도 된다.
Action<Car> carAction = new Action<>(new Car("Lamborghini"));
Rider.<Car>run(carAction);
컴파일러는 지네릭 타입을 이용해서 소스파일을 체크한다.
그리고 필요한 곳에 형변환을 넣어준다.
그 다음 지네릭 타입을 제거한다.
즉, 컴파일된 파일에는 지네릭 타입에 대한 정보가 없는 것이다.
이렇게 하는 주된 이유는 지네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해서다.
지네릭 타입의 경계를 제거한다.
T extends Vehicle 이라면 R는 Vehicle로 치환된다.
T 라면 Object로 치환된다.
지네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가한다.