Generics

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(Compile-Time Type Check)를 해주는 기능.
인스턴스 별로 다르게 동작할 수 있도록 만들어졌다.
다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다.
지네릭스를 모르면 Java API 문서를 제대로 볼 수 없을 만큼 중요하다.

•  장점
    타입 안전성 제공
    타입 체크와 형변환을 생략할 수 있어 코드가 간결해 진다.




선언

지네릭 타입은 클래스와 메서드에 선언할 수 있다.

•  일반 클래스
public class Test {
    Object item;

    void setItem(Object  item) {
        this.item = item;
    }

    Object  getItem() {
        return item;
    }
}

•  지네릭 클래스
public class Test<T> {
    T item;

    void setItem(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }
}

Test<T>의 T를 타입 변수라고 한다.

•  용어
    class Test<T>
    class 지네릭 클래스
    class 원시 타입<T>
    class Test<타입 변수>
Test<T> 지네릭 클래스. ‘T의 Test’ 또는 ‘T Test’라고 읽는다.
T Test<T>의 타입 변수 또는 타입 매개변수. (T는 타입 문자)
Test 원시 타입 (raw type)




타입 변수 Type Variable

상황에 맞게 의미 있는 문자를 선택해서 사용한다.
기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같다.
여러 개인 경우 콤마(,)를 구분자로 나열한다.

•  자주 쓰이는 타입 문자
    T :  Object
    E :  Element
    K :  Key
    V :  Valeu




제한

•  static 멤버
    지네릭스는 객체 별로 다르게 동작하기 위해 만들어졌다.
    때문에 모든 객체에 대해 동일하게 동작해야 하는 static 멤버에 타입 변수를 사용할 수 없다.
    타입 변수는 인스턴스 변수로 간주 된다.

•  지네릭 타입의 배열
    지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 배열을 생성하는 것은 안 된다.
    new 연산자는 컴파일 시점에 타입 변수가 뭔지 정확히 알아야 하는데
    지네릭 클래스는 컴파일하는 시점에 타입 변수가 어떤 타입이 될 지 전혀 알 수 없기 때문이다.
public class Test<T> {
    T[] arr;
    T[] arr1 = new T[10];    // error

    instanceof 연산자도 같은 이유로 타입 변수를 피연산자로 사용할 수 없다.

•  지네릭 배열을 생성해야 할 경우
    'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나,
    Object 배열을 생성해서 복사한 후 'T[]'로 형변환하는 방법 등을 사용한다.




와일드 카드 Wildcards

기호 ?
타입 변수는 보통 단 하나의 타입만 지정하지만 와일드 카드를 이용하면 하나 이상의 타입을 지정할 수 있다. (타입 변수의 다형성)
어떠한 타입도 될 수 있다.
<? extends T> 와일드 카드의 상한 제한(Upper Bound). T와 그 자손들만 가능
<? super T> 와일드 카드의 하한 제한(Lower Bound). T와 그 조상들만 가능
<?> 제한 없음. 모든 타입 가능. <? extends Object>와 동일

•  고안된 이유
    static 메서드에 지네릭스를 적용할 경우, 타입 매개변수는 사용하지 못하므로 특정 타입을 지정해야 한다.
    그렇게 되면 해당 메서드는 특정 타입의 객체만을 사용할 수 있게 되어
    다른 타입의 객체를 매개변수로 오게 하려면 타입 변수만 다른 똑같은 메서드를 만들어야 한다.
    이 때 지네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않기 때문에 메서드 중복 정의가 된다.
    와일드 카드는 이런 상황에 사용하기 위해 고안되었다.
static void method(Box<TypeA> b){}    // Compile error
static void method(Box<TypeB> b){}    // Compile error
static void method(Box<TypeC> b){}    // Compile error

•  Example
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        FruitBox<Fruit> fruitBox = new FruitBox<>();
        FruitBox<Apple> appleBox = new FruitBox<>();
        FruitBox<Grape> grapeBox = new FruitBox<>();

        fruitBox.add(new Fruit("fruit", 200));                       // Upper Bounded Wildcards
        fruitBox.add(new Apple("apple", 150));                       // Upper Bounded Wildcards
        fruitBox.add(new Grape("grape", 300));                       // Upper Bounded Wildcards

        appleBox.add(new Apple("apple", 100));
        appleBox.add(new Apple("apple", 170));
        appleBox.add(new Apple("apple", 200));

        grapeBox.add(new Grape("grape", 300));
        grapeBox.add(new Grape("grape", 400));
        grapeBox.add(new Grape("grape", 200));

        Log.d("TAG_", Juicer.makeJuice(fruitBox) + "");              // fruit 200 apple 150 grape 300 Juice
        Log.d("TAG_", Juicer.makeJuice(appleBox) + "");              // apple 100 apple 170 apple 200 Juice
        Log.d("TAG_", Juicer.makeJuice(grapeBox) + "");              // grape 300 grape 400 grape 200 Juice

        Collections.sort(fruitBox.getList(), new FruitComp());       // Lower Bounded Wildcards
        Collections.sort(appleBox.getList(), new FruitComp());       // Lower Bounded Wildcards
        Collections.sort(grapeBox.getList(), new FruitComp());       // Lower Bounded Wildcards

        Log.d("TAG_", fruitBox + "");                                // [apple 150, fruit 200, grape 300]
        Log.d("TAG_", appleBox + "");                                // [apple 100, apple 170, apple 200]
        Log.d("TAG_", grapeBox + "");                                // [grape 200, grape 300, grape 400]
    }
}

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

    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(); }
}

class FruitBox<T extends Fruit> extends Box<T> { }

class Fruit {
    String name;
    int weight;

    Fruit(String name, int weight) {
        this.name = name;
        this.weight = weight;
    }

    public String toString() { return name + " " + weight; }
}

class Apple extends Fruit {
    Apple(String name, int weight) { super(name, weight); }
}

class Grape extends Fruit {
    Grape(String name, int weight) { super(name, weight); }
}

class Juice {
    String name;

    Juice(String name) { this.name = name + "Juice"; }
    public String toString() { return name; }
}

class Juicer {
    /* static Juice makeJuice(FruitBox<? extends Object> box)로 할 경우 box의 요소가 Fruit의 자손이라는 보장이 없다.
       하지만 여기서는 지네릭 클래스 FruitBox를 <T extends Fruit>으로 제한했기 때문에 문제 없다.*/
    static Juice makeJuice(FruitBox<? extends Fruit> box) { // FruitBox<Fruit/Apple/Grape>
        String tmp = "";
        for (Fruit f : box.getList()) { 
            tmp += f + " ";
        }
        return new Juice(tmp);
    }
}

/* public static <T> void sort(List<T> list, Comparator<? super T> c)
sort(List<Fruit>, list, Comparator<? super Fruit> c) : Comparator<Object/Fruit>
sort(List<Apple>, list, Comparator<? super Apple> c) : Comparator<Object/Fruit/Apple>
sort(List<Grape>, list, Comparator<? super Grape> c) : Comparator<Object/Fruit/Grape>
→ List를 정렬하기 위해 Comparator를 구현할 경우 동일한 조상(Fruit)으로 구현하여 Fruit의 자손이 생길 때마다 구현해야 하는 번거로움을 해소한다.*/
class FruitComp implements Comparator<Fruit> {

    @Override
    public int compare(Fruit o1, Fruit o2) {
        return o1.weight - o2.weight;
    }
}




지네릭 타입의 형변환

지네릭 타입과 넌지네릭(Non-Generic) 타입간의 형변환은 항상 가능하다. 단, 경고가 발생한다.
Test test = null;
Test<Object> objTest = null;

test = (Test)objTest;                  // Generic Type → Primitive Type. Warning
objTest = (Test<Object>)test;          // Primitive Type → Generic Type. Warning

대입된 타입이 다른 지네릭 타입 간에는 형변환이 불가능하다.
Test<String> strTest = null;
Test<Object> objTest = null;

strTest = (Test<String>) objTest;      // error. Test<Object> → Test<String>
objTest = (Test<Object>) strTest;      // error. Test<String> → Test<Object>




지네릭 타입의 제거

컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준 후 지네릭 타입을 제거한다.
∵   지네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해
∴   컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없다. (지네릭 타입은 대부분 Non-Reifiable Types)

•  과정 1.  지네릭 타입의 경계 제거
// before 
class Box<T extends Fruit> {
    void add(T t){}
}

// after
class Box { 
    void add(Fruit t){}
}

•  과정 2.  지네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가한다.
// before 
T get(int i){
    return list.get(i);
}

// after
Fruit get(int i){
    return (Fruit) list.get(i);
}

•  과정 3.  와일드 카드가 포함되어 있으면 적절한 타입으로 형변환한다.
// before 
static Juice makeJuice(FruitBox<? extends Fruit> box){
    String tmp = "";
    for(Fruit f : box.getList()) {
        tmp += f + " ";
    }
    return new Juice(tmp);
}

// after
static Juice makeJuice(FruitBox box){
    String tmp = "";
    Iterator it = box.getList().iterator();
    while(it.hasNext()) {
        tmp += (Fruit) it.next() + " ";
    }
    retrun new Juice(tmp);
}

•  컴파일 후에
    제거되지 않는 타입 == Reifiable Types
    제거되는 타입 == Non-Reifiable Types




•  참고 서적: 자바의 정석 3판 2