제네릭

목표


  • 자바의 제네릭에 대해 학습하세요.

학습할 것


  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • 제네릭 메소드 만들기
  • Erasure

제네릭 사용법


제네릭이란

JDK1.5에서 처음 도입된 제네릭은 간단히 이야기해서 컴파일 시의 타입체크를 해주는 기능입니다. 특히 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에서 유용하게 쓰입니다.

제네릭의 장점

  1. 타입 안정성을 제공한다.
  2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.
  • 타입 안정성을 높힌다는 것은?
    • 의도하지 않은 타입의 객체가 저장되는 것을 막는다.
    • 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다.
제네릭 클래스 선언

제네릭 타입은 클래스와 메서드에 선언할 수 있는데, 먼저 클래스에 선언하는 타입을 보겠습니다.

class Box {
  Object item;

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

위처럼 클래스 Box가 정의되어 있을 때, Box 클래스를 제네릭으로 바꾸면 아래와 같습니다. (클래스 옆에 ‘'를 붙이고, 'Object'를 모두 'T'로 바꿉니다.)

class Box<T> {  // 제네릭 타입 T를 선언
  T item;

  void setItem(T item) { this.item = item; }
  T getItem() { return item;}
}
  • Box에서 **T**를 '타입 변수(type variable)'이라고 하며, 'Type'의 첫 글자에서 따온 것입니다.
  • 타입 변수는 T가 아닌 다른 것을 사용해도 됩니다.
    • ArrayList 의 경우 'Element(요소)'의 첫 글자를 따와 사용했습니다.
    • Map<K, V> 처럼 타입 변수가 여러 개인 경우에는 콤마’,’를 구분자로 나열했습니다. (K=Key, V=Vale)

타입 변수는 기호의 종류만 다를 뿐 ‘임의의 참조형 타입’을 의미한다는 것은 모두 같습니다.

제네릭 클래스의 객체를 생성할 때는 참조변수와 생성자에 타입 T 대신에 사용될 실제 타입을 지정해주어야 합니다.

Box<String> b = new Box<String>();  // 타입 T 대신, 실제 타입을 지정
b.setItem(new Object());  // 에러 - String 이외의 타임은 지정불가
b.setItem("ABC");         // OK

String item = (String) b.getItem(); // 형변환 필요없음
String item = b.getItem();  // OK

위의 코드에서 타입 T대신에 String 타입을 지정해 주었으므로, 제네릭 클래스 Box는 다음과 같이 정의된 것과 같습니다.

class Box {
  String item;

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

제네릭이 도입되기 이전의 코드와 호환을 위해, 제네릭 클래스인데도 예전의 방식으로 객체를 생성하는 것이 허용됩니다. 다만, 제네릭 타입을 지정하지 않아서 안전하지 않다는 경고가 발생합니다.

Box b = new Box();  // OK, T는 Object로 간주됩니다.
b.setItem("ABC");   // 경고, unchecked or unsafe operation
b.setItem(new Object()); // 경고, unchecked or unsafe operation

타입 변수 T에 Object를 지정해주면 경고는 발생하지 않습니다.

Box<Object> b = new Box<Object>();
b.setItem("ABC");
b.setItem(new Object());
제네릭 용어
class Box<T> {}
  • **Box** : 제네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽습니다.
  • T : 타입 변수 또는 타입 매개변수 (T는 타입 문자)
  • Box : 원시 타입(raw type)

타입 매개변수라는 것은 **Box**과 **Box**는 제네릭 클래스 **Box**에 서로 다른 타입을 대입하여 호출한 것일 뿐, 이 둘이 별개의 클래스를 의미하는 것은 아닙니다.

제네릭 제한사항

제네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절합니다.

Box<Apple> applieBox = new Box<Apple>();
Box<Grape> applieBox = new Box<Grape>();

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

static 맴버는 인스턴스 변수를 참조할 수 없습니다.

class Box<T> {
  static T item;  // error
  static int compare(T t1, T t2) {} // error
}

제네릭 주요 개념 (바운디드 타입, 와일드 카드)


바운디드 타입 (제한된 제네릭 클래스)

타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 여전히 모든 종류의 타입을 지정할 수 있습니다.

FruitBox<Toy> fruitBox = new FruitBox<Toy>();
fruitBox.add(new Toy());  //fruitBox에 toy를 넣을 수 있다.

타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법으로 extends를 사용할 수 있습니다. 제네릭 타입에 ‘extend’를 넣어, 특정 타입의 자손들만 대입할 수 있게 됩니다.

class FruitBox<T extends Fruit> { // Fruit의 자손타입만 지정가능
  ArrayList<T> list = new ArrayList<T>();
}

Fruit의 자손 타입을 지정가능하므로 여러 과일을 담을 수 있는 상자가 가능하게 됩니다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple());  // Fruit의 자손 Apple
fruitBox.add(new Grape());  // Fruit의 자손 Grape

다형성에서 조상 타입의 참조변수에서 자손 타입의 객체를 가리킬 수 있는 것처럼, 매개변수화된 타입의 자손 타입도 가능합니다. 타입 매개변수 T에 Object를 대입하면, 모든 종류의 객체를 저장할 수 있게 됩니다.

만약 클래스가 아닌 인터페이스 제약이 필요하다면 이때도 extends를 사용합니다.

interface Eatable {}
class FruitBox<T extends Eatable> { ... }

클래스 Fruit의 자손이면서 Eatable의 인터페이스도 구현해야 한다면 & 기호로 연결합니다.

class FruitBox<T extends Fruit & Eatable> { ... }
와일드 카드

와일드 카드는 기호 ’?’로 표현되는 것으로 어떤 타입이 와도 상관없이 대응하기 위해 고안되었습니다. 그러나 ’?’만으로는 Object타입과 다를게 없으므로, 다음과 같이 extendssuper로 상한((upper boud)와 하한(lower bound)을 제한할 수 있습니다.

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

참고 : 제네릭 클래스와 달리 와일드 카드에는 ‘&’을 사용할 수 없습니다. <? extends T & E> 와 같이 할 수 없습니다.

와일드 카드를 사용하지 않은 static 메서드

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

와일드 카드를 사용한 메서드

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

이렇게 함으르써, 이 메서드의 매개변수로 **FruitBox**뿐만 아니라, **FruitBox**와 **FruitBox**도 가능하게 됩니다.

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

Juicer.makeJuice(fruitBox);
Juicer.makeFjuice(appleBox);

제네릭 메소드 만들기

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

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

제네릭 클래스에 정의된 타입 매개변수제네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이므로, 같은 타입 문자 T를 사용해도 같은 것이 아니라는 것을 주의하자.

class FruitBox<T> {
  ...
  static <T> void sort(List<T> list, comparator<? super T> c)
  ...
}

여기서 한가지 주목해야할 것은 sort()가 static 메서드라는 점입니다.

  • static 맴버에는 타입 매개변수를 사용할 수 없지만,
  • 메서드에 제네릭 타입을 선언하고 사용하는 것은 가능합니다.

Erasure


컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어줍니다. 그러고 나서 제네릭 타입을 제거합니다.

  • 즉 컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없습니다.
제네릭 타입의 제거과정
  1. 제네릭 타입의 경계(bound)를 제거합니다. 제네릭 타입이 <T extends Fruit>라면 T는 Fruit로 치환됩니다. <T>인 경우는 T는 Object로 치환됩니다. 그리고 클래스 옆의 선언은 제거됩니다.
  • As-Is
    class Box<T extends Fruit> {
    void add(T t) { ... }
    }
    
  • To-Be
    class Box {
    void add(Fruit t) { ... }
    }
    
  1. 제네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가합니다.
    • As-Is
      T get(int i) {
      return list.get(i);
      }
      
    • To-Be
      Fruit get(int i) {
      return (Fruit)list.get(i);
      }
      

와일드 카드가 포함되어 있는 경우에는 다음과 같이 적절한 타입으로 형변환이 추가됩니다.

  • As-Is
    static Juice makeJuice(FruitBox<? extends Fruit> box) {
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
    }
    
  • To-Be
    static Juice makeJuice(FruitBox box) {
    String tmp = "";
    Iterator it = box.getList().iterator();
    while(it.hasNext()) {
      tmp += (Fruit)it.next() + " ";
    }
    return new Juice(tmp);
    }
    

출처


자바의 정석