[Java] Generics

Java

Language :

제네릭스 Generics

컴파일시 타입 체크(Compile-Time Type Check)해주는 기능.

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스는 

어떤 자료를 담을 지 알 수 없기 때문에 최상위 객체인 Object 타입으로 저장, 관리한다. 

이 경우 의도치 않은 자료형이 담겨 실행 시에 오류가 발생할 수 있다.

해당 오류는 컴파일 시에 알 수 없는 것으로, 제네릭 타입을 지정하면 컴파일 시 오류를 확인할 수 있게 된다.

인스턴스 별로 다르게 동작할 수 있도록 만들어졌다.

다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다.

제네릭스를 모르면 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

제네릭 타입의 형변환

객체 안에 담을 수 있는 자료형을 지정하기 때문에 해당 자료형에 대한 형변환이 필요 없다.

ArrayList<String> list = new ArrayList<>();
list.add("dico");

String str = list.get(0);

ArrayList<String> : ArrayList에 담을 수 있는 자료형은 String 뿐이다.

  • 제네릭 타입과 넌제네릭(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

    Example

    package blog;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    
    public class Main {
        public static void main(String[] args) {
            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));
    
            System.out.println(Juicer.makeJuice(fruitBox)); // fruit 200 apple 150 grape 300 Juice
            System.out.println(Juicer.makeJuice(appleBox)); // apple 100 apple 170 apple 200 Juice
            System.out.println(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
    
            System.out.println(fruitBox); // [apple 150, fruit 200, grape 300]
            System.out.println(appleBox); // [apple 100, apple 170, apple 200]
            System.out.println(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;
        }
    }
    

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

    민갤

    Back-End Developer

    백엔드 개발자입니다.