[자바] 제네릭

June·2021년 1월 4일
0

자바

목록 보기
28/36

지네릭스란

지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다. 타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준 다는 뜻이다.

class Box<T> { //지네릭 타입 T를 선언
    T item;
    
    void setItem(T item) {
    	this.item = item;
    }
    T getItem() {
    	return item;
    }
}

기존에는 다양한 종류의 타입을 다루는 메서드의 매개변수나 리턴타입으로 Object 타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만, 이젠 Object 타입 대신 원하는 타입을 지정하기만 하면 된다.

class Box<T> { }
 Box<T> : 지네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다
 T	: **타입 변수** 또는 타입 매개변수. (T는 타입 문자)
 Box    : 원시 타입(raw type)

타입 매개변수에 타입을 지정하는 것을 '지네릭 타입 호출'이라고 하고, 지정된 타입 'String'을 '매개변수화된 타입(parameterized type)'이라고 한다.

지네릭스의 제한

모든 객체에 대해 동일하게 동작해야하는 static 멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스변수로 간주되기 때문이다. 이미 알고 있는 것처럼 static멤버는 인스턴스변수를 참조할 수 없다.

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

static 멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.

그리고 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 'new T[10]'과 같이 배열을 생성하는 것은 안 된다는 뜻이다.

class Box<T> {
  T[] itemArr;	//OK. T타입의 배열을 위한 참조변수
  	...
  T[] toArray() {
  	T[] tmpArr = new T[itemArr.length];	//에러. 지네릭 배열 생성불가
  		...
 	return tmpArr;
  }
  ...
}

지네릭 배열을 생성할 수 없는 것은 new 연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 그런데 위의 코드에 정의된 Box<T>클래스를 컴파일 하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없다. instanceof 연산자도 new 연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.

그렇다면 해결책은?

  T[] tArr;
  tArr = (T[])new Object[itemArr.length];

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

Box<T>객체를 생성할 때는 참조변수와 생성자에 대입된 타입이 일치해야한다. 일치하지 않으면 에러가 발생한다.

Box<Apple> appleBox = new Box<Apple>();	//OK
Box<Apple> appleBox = new Box<Grape>();	//에러

두 타입이 상속관계에 있어도 마찬가지이다. Apple이 Fruit의 자손이라고 가정하자.

Box<Fruit> appleBox = new Box<Apple>();	//에러. 대입된 타입이 다르다.

단, 두 지네릭 클래스이 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다. FruitBox는 Box의 자손이라고 가정하자.

Box<Apple> appleBox = new FruitBox<Apple>();

JDK1.7부터는 추정이 가능한 경우 타입을 생략할 수 있게 되었다. 참조변수의 타입으로부터 Box가 Apple 타입의 객체만 저장한다는 것을 알 수 있기 때문에, 생성자에 반복해서 타입을 지정해주지 않아도 되는 것이다.

Box<Apple> appleBox = new Box<Appple>();
Box<Apple> appleBox = new Box<>();

생성된 Box<T> 객체에 void add(T item)으로 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다.

Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple()); // OK
appleBox.add(new Grape()); // 에러. Apple 객체만 추가 가능

그러나 타입 T가 'Fruit'인 경우, void add(Fruid item)가 되므로 Fruit의 자손들은 이 메서드의 매개변수가 될 수 있다.

Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit()); // OK
fruitBox.add(new Apple()); // OK

제한된 지네릭 클래스

타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 그래도 여전히 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없다.

FruitBox<Toy> fruitBox = new FruitBox<Toy>();
furitBox.add(new Toy()); // 과일 상자에 장난감을 담고 있다. 

타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법은 없을까? 다음과 같이 지네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

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

여전히 한 종류의 타입만 담을 수 있지만, Fruit 클래스의 자손들만 담을 수 있다는 제한이 추가된 것이다.

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

만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 'extends'를 사용한다. 'implements'를 사용하지 않는 다는 점에 주의하자.

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

클래스 Fruit의 자손이면서 Eatble 인터페이스도 구현해야한다면 아래와 같이 &기호로 연결한다.

class FruitBox<T extends Fruit & Eatable> {...}

이제 FruitBox에는 Fruit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수 T에 대입될 수 있다.

와일드 카드

와일드 카드

지네릭 메서드

메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 한다. 앞서 살펴본 것처럼, 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) {
        ...
    }
}

위의 코드에서 지네릭 클래스 FruitBox에 선언된 타입 매개변수 T와 지네릭 메서드 sort()에 선언된 타입 매개변수 T는 타입 문자만 같을 뿐 서로 다른 것이다. 그리고 sort()가 static 메서드라는 것에 주목하자. 앞서 설명한 것처럼, static 멤버에는 타입 매개변수를 사용할 수 없지만, 이렇게 지네릭 타입을 선언하고 사용하는 것은 가능하다.

메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 이해하기 쉬운데, 이 타입 매개변수는 메서드 내에서만 지엽적으로 사용될 것이므로 메서드가 static이건 아니건 상관이 없다.

makeJuice()를 지네릭 메서드로 바꿔보자.

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

위의 코드는 아래와 같다.

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

이제 이 메서드를 호출할 때는 아래와 같이 타입 변수에 타입을 대입해야 한다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
    ...
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(fruitBox));

그러나 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략해도 된다.

System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));

지네릭 메서드를 호출 할 때 만약 대입된 타입을 생략할 수 없으면 참조변수나 클래스 이름을 생략할 수 없다.

System.out.println(<Fruit>makeJuice(fruitBox)); // 에러. 클래스이름 생략 불가
System.out.println(this.<Fruit>makeJuice(fruitBox)); // OK
System.out.println(Juicer.<Fruit>makeJuice(fruitBox)); // OK

지네릭 메서드는 매개변수의 타입이 복잡할 때도 유용하다.

public static void printAll(ArrayList<? extends Product> list, ArrayList<? extends Product> list2) {
    ...
}

위의 코드는 아래로 줄일 수 있다.

public static <T extends Product> void printAll(ArrayList<T> list, ArrayList<T> list2) {
    ...
}

마지막으로 복잡한 지네릭 메서드를 살펴보자.

public static <T extends Comparable<? super T>> void sort(List<T> list)
  1. 타입 T를 요소로 하는 List를 매개변수로 허용한다.
  2. 'T'는 Comparable을 구현한 클래스여야 하며 (T extends Comparable). 'T' 또는 그 조상의 타입을 비교하는 Comparable 이어야하는 것(Comparable<? super T>)을 의미한다. 만약 T가 Student고 Person의 자손이라면, <? super T>는 Student, Person, Object 모두 가능.

지네릭 타입의 형변환

지네릭 타입과 원시 타입간의 형변환이 가능할까?

Box box = null;
Box<Object> objBox = null;

box = (Box)objBox;         // OK 지네릭 타입 -> 원시 타입. 경고 발생
objBox = (Box<Object>)box; // OK 원시 타입 -> 지네릭타입. 경고발생

위에서 알 수 있듯이, 지네릭 타입과 넌지네릭 타입간의 형변환은 항상 가능하다. 다만 경고가 발생한다.

그러면 대입된 타입이 다른 지네릭 타입간에는 형변환은?

Box<Object> objBox = null;
Box<String> strBox = null;

objBox = (Box<Object>)strBox; // 에러
strBox = (Box<String>)objBox; // 에러

그러면 Box<String>Box<? extends Object>로 형변환될까?
된다. 그래서 makeJuice에서 매개변수에 다형성이 적용될 수 있었다.

자바의 신

public void wildcardStringMethod(WildcardGeneric<?> c) {
    Object value = c.getWildcard();
    System.out.println(value);
}


이렇게 String 대신 ?를 적어주면 어떤 타입이 제네릭 타입이 되더라도 상관없다. 하지만 메소드 내부에서는 해당 타입을 정확히 모르기 때문에 앞서 사용한 것처럼 String 으로 값을 받을 수는 없고 Object로 처리해야만 한다.

와일드카드는 메소드의 매개변수로만 사용하는 것이 좋다. wildcardStringMethod()를 호출한 callWildcardMethod()에서 다음과 같이 사용한다면

public void callWildcardMethod() {
    WildcardGeneric<?> wildcard = new WildcardGeneric<String>();
    wildcard.setWildcard("A");
    wildcardStringMethod(wildcard);
}




오류가 난다. 즉, 알수 없는 타입에 String을 지정할 수 없다는 말이다. 어떤 객체를 wildcard로 선언하고 그 객체의 값을 가져올 수는 있지만, 와일드카드로 객체를 선언했을 때는 예제와 같이 특정 차입으로 값을 지정하는 것은 "불가능"하다.

나가기

0개의 댓글