개발/Java

[Java] 지네릭스(generics)

훈배 2024. 5. 25. 22:19

지네릭스란?

지네릭스란 jdk1.5부터 도입된 개념으로 여러 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 단계에서 타입체크(compile-time type check)를 해주는 기능입니다.
이에 따라 객체의 타입 안정성이 높아지고 형변환의 번거로움이 줄어들게 되었습니다.

지네릭 클래스 선언과 사용

지네릭 타입은 클래스와 메서드에 선언할 수 있습니다. 먼저 클래스에는 아래와 같이 선언된 클래스 Box를 지네릭 클래스로 선언하고 싶으면 클래스 옆에 '<T>'를 붙이고 object를 'T' 로 바꿔주면 선언이 됩니다.

class Box{
    object item;
    void setItem(object item){this.item = item;}
    object getItem(){return item;}
}
class Box<T>{
    T item;
    void setItem(T item){this.item = item;}
    T getItem(){return item;}
}

이 때 Box<T>에서 T를 타입 변수(type variable) 이라고 합니다. 여기서 T를 다른 문자로 사용해도 상관이 없다. 이제 지네릭 클래스가 된 Box 클래스의 객체를 생성할 때는 다음과 같이 참조변수와 생성자에 타입 T 대신에 사용할 실제 타입을 넣어주면 됩니다.

Box<String> b = new Box<String>;   //타입 T 대신, 실제 타입을 지정
b.setItem(new Object());           //경고, 이전 버전의 코드 호환을 위해 허용하지만 타입 불안정 경고(T를 Object로 간주)
b.setItem("ABC")                   //OK, String 타입이므로 가능
String item = (String) b.getItem();//이 코드처럼 형변환이 필요 없음

코드 호환성을 위해 이전 방식으로도 객체 생성이 가능하지만 unchecked or unsafe operation 경고가 발생합니다. 다만 new Box<Object>(); 처럼 생성하면 경고는 발생하지 않습니다.

 

또한 static 멤버에 타입 변수 T를 사용할 수 없습니다. 모든 객체에 대해 동일하게 동작해야 하는 static 멤버에 인스턴스변수로 간주되는 T를 참조할 수 없기 때문입니다.

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

 

그리고 지네릭 타입의 배열을 생성하는 것도 허용되지 않습니다.

class Box<T>{
    T[] itemArr;
    T[] toArray(){
        T[] tmpArr = new T[itemArr.length]; //에러
        return  tmpArr;
    }
}

왜냐하면 new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야하기 때문입니다. 이와 같은 이유로 instanceof 연산자도 사용할 수 없습니다. 그래서 지네릭 배열을 생성해야 할 때는 newInstance()와 같이 동적으로 객체를 생성하는 메서드를 사용하거나 tArr = (E[ ])new Object [ ]와 같이 'T[ ]'로 형변환을 해야 합니다.

 

Box<T> 객체를 생성할 때는 참조변수와 생성자에 대입된 타입이 같아야 합니다. 또한 상속관계에 있는 지네릭 클래스간의 다형성이 성립하며, 타입 추정이 가능한 경우 타입 생략이 가능합니다. 

Box<Apple> appleBox = new Box<Apple>(); //OK
Box<Apple> appleBox = new Box<Grape>(); //에러
Box<Apple> appleBox = new FruitBox<Apple>(); //OK 다형성 성립
Box<Apple> appleBox = new Box<>;  //OK 타입 추정이 가능한 경우 생략 가능

 

제한된 지네릭 클래스

만약 타입의 종류를 제한하려고 한다면 타입 매개변수에 다음과 같이 'extends' 키워드를 사용하면 됩니다. 

class FruitBox<T extends Fruit>{
	ArrayList<T> list = new ArrayList<T>;
}

또한 클래스가 아닌 인터페이스를 구현하는 제약도 'implements' 가 아닌 'extends'를 사용하며 '&' 기호로 여러 클래스와 인터페이스를 연결하여 다중상속 조건을 걸 수 있습니다. 

 

와일드 카드

어떤 클래스에 static 메서드가 있다고 하면, 타입 매개변수는 인스턴스 변수로 받아들여지기 때문에 지네릭스를 사용할 수 없거나 아래처럼 특정 타입을 지정해 주어야 합니다.

class Juicer {
	static Juice makeJuice(FruitBox<Fruit> box) {
    	...
    }
}

이처럼 타입을 지정해 놓으면 해당 타입 이외의 타입은 메서드의 arguments로 올 수 없다는 문제가 생깁니다. 이럴 때 사용하는 것이 바로 와일드 카드입니다.

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

아래처럼 위의 메서드를 와일드 카드를 사용하여 여러 타입을 사용 할 수 있습니다.

static Juice makeJuice(FtruitBox<? extends Fruit> box) {
	...
}

 

지네릭 메서드

메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 합니다. 

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

 

위와 같이 지네릭 메서드로 선언하면  static 메서드도  타입 매개변수를 사용할 수 있게 됩니다. 여기서 FruitBox와 sort에 선언된 T는 각각 다른 T 입니다.

 

앞서 나왔던 makeJuice 메서드를 지네릭 메서드로 바꾸면 다음과 같습니다. 

static <T extends Fruit> Juice makeJuice(FruitBox<T> box){
	...
}

 지네릭 메서드를 사용하면 매개변수 타입이 복잡할 때 유용합니다.

 

틀린 부분이 있으면 지적해주시면 감사하겠습니다.