자바의정석 - 지네릭스

지네릭스

지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크(compile-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다. 또한 원하지 않는 종류의 객체가 포함되는 것을 막아주는 역할도 한다.

[지네릭스의 장점]

타입 안정성을 제공한다.
타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

지네릭 클래스의 선언

GenericsBox 에서 T를 '타입 변수'라고 하며, Type의 첫 글자에서 따온 것이다. 여기서 'T'는 편의상 참조형 타입을 의미한 것일 뿐이다. 다른 기호로 넣어도 상관이 없다. 아래의 예제소스를 보면 쉽게 알 수 있다. 참고로 아래의 Generics 클랙스는 GenericsModel을 원시타입이며 Generisc을 지네릭스 클래스라고 부르면 된다.

package com.java.mystudy.generis;

// 일반적인 class
public class NomalModel {

    String item;

    public String getItem() {
        return item;
    }

    public void setItem(String item) {
        this.item = item;
    }
}

package com.java.mystudy.generis;

// 지네릭스를 활용한 class
public class GenericsModel<T> {

    T gItem;

    public T getgItem() {
        return gItem;
    }

    public void setgItem(T gItem) {
        this.gItem = gItem;
    }
}

public String main() {

    // Only String
    NomalModel nm = new NomalModel();

    // String type
    GenericsModel<String> gm1 = new GenericsModel<>();

    // Integer type
    GenericsModel<Integer> gm2 = new GenericsModel<>();

    nm.setItem("Nomal Model");
    gm1.setgItem("Generis(String) Model");

    // Generis(Integer) Model
    gm2.setgItem(2);

    StringBuilder sb = new StringBuilder();

    sb.append("Nomal Model: ").append(nm.getItem())
            .append("   Generis(String) Model: ").append(gm1.getgItem())
            .append("   Generis(Integer) Model: ").append(gm2.getgItem());

    return sb.toString();
}

[출력결과]
compare

지네릭스의 제한

위의 예제소스의 GenericsModel을 통해 객체별로 다른 타입을 지정하는것은 적절하며 그러한 용도로 사용하기 위해 지네릭스를 사용한 것이다. 하지만 모든 객체에 대해 동일하게 동작해야하는 static맴버에 타입 변수 T를 사용할 수 없다. static멤버는 인스턴스변수를 참조할 수 없기 떄문이다.

// 지네릭스를 활용한 class
public class GenericsModel<T> {

    static T gItem; // error
}

추가로 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조 변수를 선언하는 것은 가능하지만 new T[10]과 같이 배열을 생성하는 것은 안된다. 이유는 new연산자는 컴파일 시점에 타입 T가 뭔지정확히 알아야 한다. 그런데 new T[10]과 같이 정의된 코드를 컴파일하는 시점에서 T가 어떤 타입이 될지 알 수 없다. (instanceof 연사자도 같음)

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

class Box<T> {
    ArrayList<T> list = new ArrayList<T>();

    void add(T item) {
        list.add(item);
    }
    T get(int i) {
        return list.get(i);
    }
    ArrayList<T> getList() {
        return list;
    }
    int size() {
        return list.size();
    }
    public String toString() {
        return list.toString();
    }
}

public String generics() {

    GenericsParent<String> pList = new GenericsParent<>();
    pList.add("Mother");
    pList.add("Father");

    return "Parent Size: " + pList.size() + " | Parent List: " + pList.get(0) + " " + pList.get(1);
}

위와 같은 지네릭 클래스가 있다고 하자.

Box<Apple> appleBox = new Box<Apple>(); // OK
Box<Apple> appleBox = new Box<Grape>(); // error
Box<Grape> appleBox = new Box<Grape>(); // OK

Box의 객체를 생성할 때는 참조변수와 생성자에 대입된 타입이 일치해야 한다. 그렇지 않으면 에러가 발생한다.

Box<Fruit> appleBox = new Box<Apple>(); // error

두 타입이 상속관계에 있어도 마찬가지 타입이 다르기에 에러가 발생한다.

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

단, 두 지네릭 클래스의 타입이 상속관계에 있고 대입된 타입이 같은 것은 error가 발생하지 않는다.

아래는 지끔까지의 지네릭스 사용법을 통한 간단한 예제이다.

class Box<T> {
    ArrayList<T> list = new ArrayList<T>();

    void add(T item) {
        list.add(item);
    }
    T get(int i) {
        return list.get(i);
    }
    ArrayList<T> getList() {
        return list;
    }
    int size() {
        return list.size();
    }
    public String toString() {
        return list.toString();
    }
}

public String generics() {

    GenericsParent<String> pList = new GenericsParent<>();
    pList.add("Mother");
    pList.add("Father");

    return "Parent Size: " + pList.size() + " | Parent List: " + pList.get(0) + " " + pList.get(1);
}

출력결과
compare

제한된 지네릭 클래스

여태까지 지네릭의 사용법은 마치 Ojbect 타입으로 선언하여 사용하는 방식이였다. 이제 정리할 내용은 모든 타입이 아닌 특정한 타입의 종류만으로 제한할 수 있는 방법이 있다. 그 방법은 extends를 활용한 방법이다.

class Family<T extends Parent> { // Parent의 자손만 타입으로 지정가능
    List<T> list = new ArrayList<>();
}

// 사용시
Family<Father> f = new Family<Father>(); // OK
Family<Mother> m = new Family<Mother>(); // OK
Fmaily<Parent> p = new Family<Parent>(); // OK
p.add(new Father()); // OK
p.add(new Mother()); // OK

다형성에서 조상타입의 참조변수로 자손타입의 객체를 가리킬 수 있는 것처럼, 매개변수화된 타입의 자손 타입도 가능한 것이다. 만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하더라도 extends를 사용한다 implements를 사용하면 안되는 것을 명심해야한다. 만일 위의 상황에서 인터페이스를 구현해야 한다면 class Family<T extends Parent & interfaceName> 으로 & 기호를 활용하여 정의하면 된다.

와일드 카드

와일드 카드는 기호 ‘?’로 푠현하는데, 와일드 카드는 어떠한 타입도 될 수 있다. ‘?’만으로는 Object타입과 다른게 없으므로, 다음과 같이 extendssuper로 상한과 하한을 제한할 수 있디.

<? extends T> 와일드 카드의 상한 제한. T와  자손들만 가능.   
<? super T>   와일드 카드의 하한 제한. T와  조상들만 가능.   
<?>           제한 없음. 모든 타입이 가능. <? extends Object> 동일

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

지네릭 메서드

메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 한다. Collections.sort()가 지네릭 메서드이며, 지네릭 타입의 선언 위치는 반환 타입의 바로 앞이다.

class Collections<T> {
    static <T> void sort(List<T> list, Comparator<? super T> c)
}

지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다. 같은 타입 문자를 사용한다 해도 같은 것이 아니라는 것에 주의를 해야한다. 예를 들어 위의 Collections<T>static<T> 에서의 T는 서로가 아무런 상관이 없는 타입이다.

위의 모든 정리내용은 자바의 정석을 공부하며 복습차 정리한 내용입니다.

Leave a comment