C++에서 배우는 자바-제너릭스

지네릭스(Generics)

java에서는 대부분의 자료구조가 타입이 캐스팅되지 않고 모든 형의 자료를 저장할 수 있었다. 템플릿을 사용하여 미리 자료구조에 형을 캐스팅하고 사용하는 C++과는 달랐는데, 이에 대한 문제점이 인식되었는지 JDK 1.5부터는 지네릭스 기능이 추가되어 자료구조의 형을 제한할 수 있게 되었다.
지네릭스는 한마디로 하면 템플릿이다. C++의 그 템플릿. 거기에 기능 몇 가지가 추가되었다고 보면 된다. 간단한 사용례를 살펴보자.

class Box {
    Object item;
    void setItem(Object item) {
        this.item=item;
    }
    Object getItem() {
        return item;
    }
}

간단한 클래스이다. 위의 클래스를 지네릭 클래스로 변환해보자.

class Box<T> {
    T item;
    void setItem(T item) {
        this.item=item;
    }
    T getItem() {
        return item;
    }
}

클래스 이름 옆에 타입변수 T를 선언하여 지네릭 클래스로 변환하였다. 이제 이 클래스는 실제 사용될 타입을 T 대신 받아 사용하게 된다. 지네릭 클래스의 사용법은 다음과 같다.

class Arr {
    public static void main(String args[]) {
        Box<string> b=new Box<string>();
        //b.setItem(new Object()); 에러, string이 아님
        b.setItem("abc");
        String item=b.getItem();
    }
}

위와 같이 사용한다면 우리는 가장 위의 클래스에서 Object 대신 String을 넣어 작성한 것과 동일한 효과를 얻을 수 있다.
static 멤버에 타입변수를 지정할 순 없음을 유의해야 한다. static은 클래스마다 하나뿐인 고유한 것이기에 타입변수에 따라 달라질 수 없기 때문이다.
배열에도 타입변수를 사용할 수 없다. new를 사용하면 컴파일 당시에 해당 객체의 타입을 알아야 하는데, 타입변수를 사용하면 알 수 없기 때문이다. newInstance()등을 사용하여 동적으로 객체를 생성하면 가능하다.

new는 동적 할당에 사용되는 것으로 아는데 왜 이것이 불가능한가? 에 대하여 생각해보았는데 동적 할당과 동적으로 객체를 생성하는 것은 다른 문제인 것 같다.
분명 new는 동적 할당으로 런타임에 메모리를 할당해주긴 하지만, 컴파일 할때에 이것이 무슨 타입이고 런타임 때 얼마나 할당해주어야 할지를 미리 결정해야 하는 것이다. newInstance() 등을 사용하면 이러한 계산도 런타임 때 진행하도록 하는 것 같다.

지네릭 클래스의 사용과 제한

지네릭 클래스를 생성하면 기본적으로 캐스팅한 타입과 생성자를 통해, 매개변수를 통해 들어가는 타입이 똑같아야 한다. 다만 상속이 일상적인 객체지향적 언어인 만큼 몇 가지 예외가 있는데,

Box<apple> appleBox=new fruitBox<apple>();

fruitBox는 Box의 자손이라 했을 때, 위와 같은 사용은 허용된다.

Box<Fruit> FruitBox=new Box<Fruit>();
FruitBox.add(new Fruit());
FruitBox.add(new Apple());

또한 Fruit의 자손이 Apple이라고 했을 때, Fruit형을 캐스팅한 Box에 Apple도 매개변수로서 들어갈 수 있다.

지네릭 클래스로 클래스를 선언하면 어떠한 타입이든 다 들어갈 수 있다. 그렇다면 특정 타입만 들어갈 수 있도록 제한할수도 있을까? 다음과 같이 하면 가능하다.

class Box<T extends Fruit> {
    T item;
    void setItem(T item) {
        this.item=item;
    }
    T getItem() {
        return item;
    }
}

위와 같이 지네릭 타입변수에 extends를 붙여 사용하면 특정 클래스와 그 자손들만 대입할 수 있도록 구현할 수 있다. 인터페이스를 구현해야 할 때에도 implements 대신 extends를 사용하는 것에 주의하자.

와일드 카드

만약 지네릭 클래스를 적용한 인스턴스를 static 메서드에서 매개변수로 사용한다고 가정해보자.

class Juicer {
    static Juice makeJuice(FruitBox<Fruit> box) {
        ...
    }
}

static 메서드, 변수에서는 위에서 설명했듯이 타입변수를 사용할 수 없으므로 Fruit 등으로 타입을 확실히 정해서 캐스팅해야 한다.
하지만 이런 식으로 작성하면 FruitBox 형식의 매개변수는 받을 수 없게 된다. 이를 해결하기 위해 FruitBox을 매개변수로 받는 같은 이름의 함수를 작성하여 오버로딩을 시도해도 불가능하다. 왜냐하면 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다. 이를 해결하기 위한 방법이 바로 와일드 카드이다. 와일드 카드는 ?로 표시하고 extends를 붙여 그와 그 자손으로 제한, super를 붙여 그와 그 조상으로 제한하는 식으로 활용한다. 위의 코드를 와일드 카드를 사용하여 고쳐보면

class Juicer {
    static Juice makeJuice(FruitBox<? extends Fruit> box) {
        ...
    }
}

위와 같이 수정하면 우리가 원하던대로 FruitBox, FruitBox 등도 받을 수 있게 된다. 와일드 카드는 타입변수와도 같이 사용이 가능하다. 보통 후에 배울 지네릭 메서드에서 타입변수와 같이 사용하는데 Comparator<? super T>와 같은 형식으로 사용하여 하나의 Comparator를 여러 타입을 대상으로 사용할 수 있게 한다.

지네릭 메서드

클래스가 아닌 메서드에도 타입변수를 적용하여 지네릭 메서드를 사용할 수 있다. 사용법은 다음과 같다.

static <T> void sort(List<T> list, Comparator<? super T> c)

이렇게 반환 타입 앞에 타입변수를 붙여 사용한다.
static 메서드는 원래 타입변수 사용이 불가능하지만 위와 같이 지네릭 메서드로 선언하면 사용이 가능하다. 메서드에 선언된 타입변수는 지역변수와 같이 해당 메서드 내부에서만 사용되므로 메서드가 static이든 아니던 상관이 없는 것이다.
위의 와일드 카드를 설명하는데 사용했던 makeJuice 메서드를 와일드카드 대신 지네릭 메서드를 사용하는 방식으로 수정할 수도 있다.

class Juicer {
    static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
        ...
    }
    public static void main(String args[]) {
        Juice j=Juicer.<Apple>makeJuice(new FruitBox<Apple>());
    }
}

위와 같이 수정하여 사용할 수 있다. 단 이렇게 타입을 대입할 때에는 클래스 이름이나 this를 생략할 수 없는 규칙이 있음을 유의해야 한다.

지네릭 타입의 형변환

지네릭 타입과 넌-지네릭 타입의 형변환은 경고를 출력할 뿐 언제나 가능하다.

Box box=null;
Box<Object> obj=null;
box=(Box) obj;
obj=(Box<Object>) box;

하지만 대입된 타입이 다른 지네릭 타입간의 형변환은 불가능하다.

Box<String> box=null;
Box<Object> obj=null;
box=(Box<String>) obj; //에러
obj=(Box<Object>) box; //에러

와일드카드를 사용한 경우 와일드카드 범위 내에 있는 객체를 와일드카드로 형변환하는 것은 가능하다.

Box<? extends Object> box=new Box<String>();

하지만 반대의 경우는 와일드 카드는 어떠한 타입도 될 수 있지만 String 타입이 아니면 형 변환이 불가능하므로 확인되지 않은 형변환이라는 경고가 출력된다.
위에서 확인했듯이 더 범위가 큰 Object 타입이더라도 서로 다른 타입은 형변환이 불가능한데, ? extends Object는 String일수도 있지만 아닐 수도 있기 때문이다.

지네릭의 제거

컴파일러는 코드를 읽어들인 뒤 필요한 부분에 형변환을 시켜준 뒤 지네릭 타입을 제거한다. 즉 .class 파일에는 지네릭 타입에 대한 정보가 들어가지 않는다.
이는 지네릭스가 JDK 1.5에 추가된 기능인만큼 이전 버전의 코드들과의 호환을 위한 것이다.
지네릭 타입을 제거할 때에는 먼저 타입변수 T를 extends가 붙어있다면 해당 타입으로, 없다면 Object로 치환한다. 타입 변수를 제거한 후 타입이 일치하지 않는 부분이 있다면 적절히 형변환을 시켜준다. 와일드카드가 포함되어 있는 경우에는 좀 더 복잡한 형변환이 이루어진다.

지금까지 java의 지네릭스에 대해 알아봤다. C++의 템플릿과 기본적으로 비슷하면서도 와일드 카드, 지네릭 메서드와 상속 등 여러 기능과 생각해봐야 할 점이 많이 추가되어 훨씬 복잡한 기능같다.
다만 제대로 사용한다면 코드의 중복 없이 여러 타입을 받을 수 있는 강력한 기능이니 꼭 알아둬야 할 것 같다.