제네릭

Jaca·2021년 10월 10일
0

제네릭

제네릭이란 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.
객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

제네릭 클래스의 선언

제네릭 타입은 클래스와 메서드에 선언할 수 있는데, 먼저 클래스에 선언하는 제네릭 타입에 대해 알아보자.

예를 들어 Box 클래스가 다음과 같이 정의 되있다.

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

이 클래스를 제네릭 클래스로 변경하면 다음과 같이 클래스 옆에 <T> 를 붙이고, Object를 T로 바꾼다.

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

Box<T>에서 T를 타입 변수라고 한다.
타입 변수는 T가 아닌것도 사용가능하다.
ArrayList<E>의 E도 타입 변수이다.
타입 변수가 여러 개인 경우에는 Map<K, V>과 같이 콤마(,)로 구분하면 된다.
무조건 T를 사용하기 보다 K(key)와 V(value) 처럼 상황에 맞는 의미있는 글자를 선택해서 사용한다.

이들은 기호의 종류만 다를 뿐 모두 '임의의 참조형 타입'을 의미한다.

이제 제네릭 클래스가 된 Box 클래스의 객체를 생성할 때는 다으과 같이 참조변수와 생성자에 타입 T대신에 실제 타입을 지정해주어야 한다.

Box<String> b = new Box<String>(); // 타입 T 대신 실제 타입 지정
b.setItem(new Object()); // 에러, String 이외의 타입은 지정불가
b.setItem("ABC"); // 가능
String item = b.getItem(); // 형변환 필요 없음

제네릭의 용어

제네릭에 사용되는 용어는 헷갈리기 쉬워, 짚고 넢어가자

class Box<T> {}

Box<T> : 제네릭 클래스, 'T Box' 라고 읽는다.
T : 타입 변수
Box : 원시 타입

제네릭의 제한

제네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 바람직하다.
인스턴스별로 다르게 동작하려고 만든 것이기 때문.

그러나 모든 객체에 동일하게 동작해야하는 static 멤버에 타입 변수를 사용할 수는 없다.
T는 인스턴스 변수로 간주되기 때문.

static 멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.
Box<Apple>.itemBox<Grapge>.item 이 다른 것이어서는 안되기 때문이다.

제네릭 타입의 배열도 생성할 수 없다.
제네릭 배열 타입의 참조 변수를 선언하는 것은 가능하나, 'new T[10]' 과 같이 배열을 생성하는 것은 안된다.

class Box<T> {
   T[] itemArr; // T 타입의 배열을 위한 참조변수 가능.
   
   T[] toArray() {
       T[] tmpArr = new T[itemArr.length]; // 불가
       ...
   }
}

제네릭 배열을 생성할 수 없는 것은 new 연산자 때문인데,
new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다.
같은 이유로 instanceof 연산자도 타입 변수를 사용할 수 없다.

제네릭 클래스 객체의 생성과 사용

참조변수와 생성자에 대입된 타입이 일치해야 한다.
두 타입이 상속관계에 있어도 마찬가지이다. Apple이 Fruit의 자손이라고 가정하자

Box<Apple> appleBox = new Box<Apple>(); // 가능
Box<Fruit> appleBox = new Box<Apple>(); // 불가능

단, 두 제네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 가능하다. FriutBox가 Box의 자손이라고 하자.

Box<Apple> appleBox = new FruitBox<Apple>(); // 가능

제한된 제네릭 클래스

타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법은 없을까?

다음과 같이 제네릭 타입에 'extends' 를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit> {
    ArrayList<T> list = new ArrayList<T>();
}

여전히 한 종류의 타입만을 담을 수 있지만, Fruit 클래스의 자손들만 담을 수 있다는 제한이 더 추가된 것 이다.

와일드 카드

매개변수에 과일박스를 대입하면 주스를 만들어서 반환하는 Juicer 클래스가 있고, makeJuice() 라는 static 메서드가 정의되있다고 하자.

class Juicer {
    static Juice makeJuice(FruitBox<Fruit> box) {
        String tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }
}

Juicer 클래스는 제네릭 클래스가 아니고, static 메서드에는 타입 변수를 사용할 수 없으므로, 아예 제네릭을 적용하지 않거나, 특정 타입을 지정해줘야 한다.

그렇다면 FruitBox<Fruit>가 아닌 타입의 객체는 makeJuice() 의 매개변수가 될 수 없으므로, 각 타입의 매개변수를 갖는 makeJuice()를 만들어야 한다.

하지만 이러한 오버로딩은 컴파일 에러가 발생한다.
제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다.
이럴 때 사용하는 것이 와일드 카드 '?' 이다.

'?'만으로는 Object 타입과 다를 게 없으므로, 'extends'와 'super' 로 상한과 하한을 지정한다.

<? extends T> : 와일드 카드의 상한 제한, T와 그 자손들만 가능
<? super T> : 와일드 카드의 하한 제한, T와 그 조상들만 가능
<?> : 제한 없음, Object와 동일

제네릭 메서드

메서드 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 한다.
제네릭 메서드는 제네릭 클래스가 아니어도 정의할 수 있다.

제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다.
같은 T를 사용해도 다른 것이라는 것을 주의 해야한다.

profile
I am me

0개의 댓글