[ TIL ] Generics

지네릭스란?

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.
타입 파라미터라고 이해하면 쉽다.

우리에게 익숙한 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);

지네릭 타입의 제거

컴파일러는 지네릭 타입을 이용해서 소스파일을 체크한다.
그리고 필요한 곳에 형변환을 넣어준다.
그 다음 지네릭 타입을 제거한다.
즉, 컴파일된 파일에는 지네릭 타입에 대한 정보가 없는 것이다.
이렇게 하는 주된 이유는 지네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해서다.

기본적인 제거 과정

  1. 지네릭 타입의 경계를 제거한다.
    T extends Vehicle 이라면 R는 Vehicle로 치환된다.
    T 라면 Object로 치환된다.

  2. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가한다.

좋은 웹페이지 즐겨찾기