Chapter 12 지네릭스, 열거형, 애너테이션 / 1. 지네릭스(Generics)

1. 지네릭스(Generics)

1.1 지네릭스란?

  • 컴파일 시의 타입체크(compile-time type check)를 해주는 기능

지네릭스의 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

  • 의도하지 않은 타입의 객체가 저장되는 것을 막음
  • 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여줌

1.2 지네릭 클래스의 선언

  • 클래스에 선언하는 지네릭 타입
class Box{
     Object item;
     
     void setItem(Object item) {this.item = item;}
     Object getItem(){return item;}
}
  • 지네릭 클래스로 변경하면 클래스 옆에'< T >’를 붙이면 된다.
  • ‘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가 아닌 다른 것을 사용해도 된다.
    ex) ArrayList< E >, Map<K,V>
  • 무조건 ‘T’를 사용하기보다 가능하면, 이처럼 상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋다.
  • 기호의 종류만 다를 뿐’임의의 참조형 타입’을 의미한다는 것은 모두 같다.
  • 기존에는 다양한 종류의 타입을 다루는 메서드의 매개변수나 리턴타입으로 Object타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만, 이젠 Objcet타입 대신 원하는 타입을 지정하기만 하면 되는 것이다.
Box<String> b = new Box<String>(); //타입 T 대신, 실제 타입을 지정
b.setItem(new Object());           //에러, String이외의 타입은 지정불가
b.setItem("ABC");                  //Ok. String타입이므로 가능
String item = (String) b.getItem(); // 형변환이 필요없음
  • 지네릭이 도입되기 이전의 코드와 호환을 위해, 지네릭 클래스인데도 예전의 방식으로 객체를 생성하는 것이 허용
  • 지네릭 타입을 지정하지 않아서 안전하지 않다는 경고 발생
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()); // 경고발생 안함
  • 지네릭스가 도입되기 이전의 코드와 호환성을 유지하기 위해서 지네릭스를 사용하지 않은 코드를 허용하는 것일 뿐, 앞으로 지네릭 클래스를 사용할 때는 반드시 타입을 지정해서 지네릭스와 관련된 경고가 나오지 않도록 하자.

지네릭스의 용어

Box 지네릭 클래스. ‘T의 Box’ 또는 ‘T Box’라고 읽는다.
T 타입 변수 또는 타입 매개변수.(T는 타입 문자)
Box 원시 타입(raw type)

지네릭스의 제한

  • 모든 객체에 대해 동일하게 동작해아하는 static멤버에 타입 변수 T를 사용할 수 없다.
  • T는 인스턴스변수로 간주되기 때문이다.
  • static멤버는 인스턴스변수를 참조할 수 없다.
class Box<T>{
    static T item; // 에러
    static int compare(T t1, T t2){...} // 에러
    ...
}
  • 대입된 타입의 종류에 관계없이 동일한 것이어야 됨
  • 지네릭 타입의 배열을 생성하는 것도 허용 안됨
class Box<T>{
   T[] itemArr; // OK. T타입의 배열을 위한 참조 변수 
   ...
   T[] toArray(){
       T[] tmpArr = new T[itemArr.length]; // 에러. 지네릭 배열 생성불가
       ...
       return tmpArr;
}
...
}
  • 지네릭 배열을 생성할 수 없는 것은 new 연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다.
  • 꼭 지네릭 배열을 생성해야할 필요가 있을 때는, new연산자대신 ‘Reflection API’의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object배열을 생성해서 복사한 다음에’T[]’로 형변환하는 방법 등을 사용한다.

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

  • 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야한다.
Box<Apple> appleBox = new Box<Apple>(); // OK
Box<Apple> appleBox = new Box<Grape>(); // 에러
  • 두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다.
    FruitBox는 Box의 자손이라고 가정
Box<Apple> appleBox = new FruitBox<Apple>(); // OK. 다형성
  • JDK1.7부터는 추정이 가능한 경우 타입을 생략할 수 있다.
  • 참조변수의 타입으로 부터 Box가 Apple타입의 객체만 저장한다는 것을 알 수 있기 때문에, 생성자에 반복해서 타입을 지정해주지 않아도 되는 것이다.
Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<>(); // OK. JDK1.7부터 생략가능
  • 생성된 Box의 객체에 ‘void add(T item)’으로 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다.
Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple()); // Ok.
appleBox.add(new Grape()); // 에러. Box<Apple>에는 Apple객체만 추가가능 
  • 타입 T가’Fruit’인 경우, ‘void add(Fruit item)’가 되므로 Fruit의 자손들은 이 메서드의 매개변수가 될 수 있다.
Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit()); //OK
fruitBox.add(new Apple()); //Ok. void add(Fruit item)
import java.utilArrayList;

class Fruit { public String toString(){return "Fruit";}}
class Apple extends Fruit { public String toString() {return "Apple";}}
class Grape extends Fruit { public String toString() {return "Grape";}}
class Toy { public String toString() {return "Toy";}}

class FruitBoxEx1 {
   public sttic void main(String[] args) {
       Box<Fruit> fruitBox = new Box<Fruit>();
       Box<Apple> appleBox = new Box<Apple>();
       Box<Toy> toyBox = new Box<Toy>()
//     Box<Grape> grapeBox = new Box<Apple>(); // 에러. 타입 불일치

       fruitBox.add(new Fruit()); 
       fruitBox.add(new Apple()); // OK. void add(Fruit item)
   
       appleBox.add(new Apple());
       appleBox.add(new Apple());
//     appleBox.add(new Toy()); // 에러. Box<Apple>에는 Apple만 담을 수 있음 

       toyBox.add(new Toy());
       toyBox.add(new Apple()); // 에러. Box<Toy> 에는 Apple을 담을 수 없음

       System.out.println(fruitBox);
       System.out.println(appleBox);
       System.out.println(toyBox);
} // main의 끝
}

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

1.4 제한된 지네릭 클래스

  • 지네릭 타입에 ‘extends’를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
class FruitBox<T extends Fruit> { // Fruit의 자손만 타입을 지정가능
     ArrayList<T> list = new ArrayList<T>();
...
}
  • Fruit클래스의 자손들만 담을 수 있다는 제한 추가
FruitBox<Apple> appleBox = new FruitBox<Apple>(); // OK
FruitBox<Toy> toyBox = new FruitBox<Toy>(); // 에러. Toy는 Fruit의 자손이 아님 
  • 다형성에서 조상타입의 참조변수로 자손타입의 객체를 가리킬 수 있는 것처럼, 매개변수화된 타입의 자손 타입도 가능한 것이다.
  • 타입 매개변수 T에 Object를 대입하면, 모든 종류의 객체를 저장할 수 있게 된다.
  • 만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 ‘extends’를 사용한다.
  • ‘implements’를 사용하지 않는다.
interface Eatable{}
class FruitBox<T extends Eatable>{ ... }
  • 클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 아래와 같이 ‘&’기호로 연결한다.
    class FruitBox<T extends Fruit & Eatable>{...}

좋은 웹페이지 즐겨찾기