본문 바로가기
Study/JAVA

[Java] 12-1. 지네릭스(Generics)

by jeongwle 2022. 10. 5.
728x90
반응형

 

지네릭스(Generics)

JDK1.5에 처음 도입되었고 당시에는 선택적으로 사용되었다. 하지만 이제는 지네릭스를 모르고는 Java API문서조차 제대로 보기 어려울 만큼 중요한 위치를 차지하고 있다. 기본적인 개념을 알아보도록 하자.

 

1. 지네릭스란?

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

지네릭스의 장점
  1. 타입 안정성을 제공
  2. 타입체크와 형변환을 생략할 수 있어 코드가 간결해짐​

 

2. 지네릭 클래스의 선언

클래스에 지네릭 타입을 선언하는 경우를 먼저 살펴보자.

// 변경 전

class Box {
  Obejct item;
  
  void setItem(Object item) {
    this.item = item;
  }
  
  Object getItem(){
    return item;
  }
}

// 지네릭 타입 적용 후

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


클래스 옆에 <T>를 붙여 지네릭 클래스로 변경할 수 있다. 여기서 T를 타입 변수(type variable)이라 하고 Type의 첫 글자에서 따온 것이다. 타입 변수는 T가 아닌 다른 것을 사용해도 된다. 타입 변수가 여러개인 경우 Map<K, V>와 같이 콤마를 구분자로 나열하면 된다. 기존에는 다양한 종류의 타입을 다루는 메서드의 매개변수나 리턴타입으로 Object타입의 참조변수를 많이 사용하여 형변환이 불가피했다. 하지만 이제 원하는 타입을 지정하기만 하면 된다.

지네릭스 용어

class Box<T>

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

타입 문자 T는 지네릭 클래스 Box<T>의 타입 변수 또는 타입 매개변수라 한다. 타입 매개변수에 타입을 지정하는 것을 지네릭 타입 호출이라 하고 지정된 타입을 매개변수화된 타입(parameterized type)이라 한다.

지네릭스의 제한
모든 객체에 대해 동일하게 동작해야하는 static멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스변수로 간주된다. static멤버는 타입 변수에 지정된 타입의 종류와 관계없이 동일한 것이어야 한다. 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, new T[10]과 같이 배열을 생성하는 것은 안 된다. new 연산자는 heap영역에서 메모리를 확보하기 위해 타입을 알아야 하지만 컴파일 시점에서는 어떤 타입인지 알 수 없기 때문이다. 지네릭 배열을 생성해야할 필요가 있을 경우엔 Reflection API의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나 Object배열을 생성하여 복사한 후 T[]로 형변환하는 방법을 사용해보자.

 

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

Box<T>의 객체를 생성할 때는 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야한다.

Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<Grape>(); // X​


Apple이 Fruit의 하위클래스라 가정하여도 결과는 똑같다.

Box<Fruit> appleBox = new Box<Apple>(); // X​


하지만 두 지네릭 클래스의 타입이 상속관계이고 대입된 타입이 같다면 괜찮다. FruitBox가 Box의 하위클래스라 가정해보자.

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


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

Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<>(); // 위와 같다.​


생성된 Box<T>의 객체에 객체를 추가할 때 매개변수화된 타입과 다른 타입의 객체는 추가할 수 없다. 다만 상속관계에 있을 경우 가능하다. Apple이 Fruit의 하위클래스라 가정하자.

Box<Fruit> fruitBox = new Box<Fruit>();

fruitBox.add(new Fruit()); // OK
fruitBox.add(new Apple()); // OK​

 

4. 제한된 지네릭 클래스

타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있다. 하지만 여전히 모든 종류의 타입을 지정할 수 있다. 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법을 살펴보자.

지네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
class FruitBox<T extends Fruit> {
  ...
}​


여전히 한 종류의 타입만 담을 수 있지만 Fruit클래스의 하위들만 담을 수 있다는 제한이 더 추가되었다. 만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면 extends를 사용하여 제한을 할 수 있다. implements가 아니라는 점을 주의하자

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

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


위의 코드처럼 T에 들어올 종류를 제한할 수 있다.

 

5. 와일드 카드

매개변수에 과일박스를 대입하면 주스를 만들어서 반환하는 Juicer라는 클래스가 있고 이 클래스에 과일을 주스로 만들어 반환하는 makeJuice()라는 static메서드가 있다고 가정하자

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

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

Juicer.makeJuice(fruitBox) // OK
Juicer.makeJuice(appleBox) // X​


Juicer클래스가 지네릭 클래스도 아니고 지네릭 클래스라해도 static메서드는 타입 매개변수 T를 사용할 수 없으므로 위와 같이 특정타입을 지정해주어야 한다. 하지만 이럴 경우 FruitBox<Apple>타입의 객체는 makeJuice()의 매개변수가 될 수 없다. 그렇다고 오버로딩도 할 수 없다. 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다. 지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다. 그래서 오버로딩이 아닌 메서드 중복 정의가 되어버린다. 이럴 때 사용하기 위해 만든 것이 와일드 카드이다. 와일드 카드는 기호 ?로 표현하고 어떠한 타입도 될 수 있다.

<? extends T> 와일드카드의 상한 제한. T와 그 하위클래스만 가능
<? super T>   와일드카드의 하한 제한. T와 그 상위클래스만 가능
<?>           제한 없음. 모든타입이 가능​


와일드 카드를 사용해 makeJuice()의 매개변수 타입을 FruitBox<? extends Fruit>으로 바꾸면 FruitBox<Fruit>과 FruitBox<Apple> 모두가 매개변수가 될 수 있다.

 

6. 지네릭 메서드

메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 한다. 지네릭 타입의 선언 위치는 반환 타입 바로 앞이다. 지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 별개이다. 같은 타입문자 T를 사용해도 같은 것이 아니라는 것에 주의하자.

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

지네릭 클래스 FruitBox에 선언된 타입 매개변수 T와 지네릭 메서드 sort()에 선언된 타입 매개변수 T는 타입 문자만 같을 뿐 서로 다른 것이다. static멤버에는 타입 매개변수를 사용할 수 없지만 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다. 메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다고 생각하며 이해가 쉽다. 이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이건 아니건 상관없다.

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

// 지네릭 메서드로 변환
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
  ...
}​

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

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

Juicer.<Fruit>makeJuice(fruitBox);
Juicer.<Apple>makeJuice(appleBox);

Juicer.makejuice(fruitBox); // 대입된 타입을 생략할 수 있다.​


컴파일러가 타입을 추정할 수 있기 때문에 대부분의 경우 대입된 타입을 생략할 수 있다. 하지만 대입된 타입을 생략할 수 없을 경우에는 참조변수나 클래스 이름을 생략할 수 없다.

<Fruit>makeJuice(fruitBox);		// error
this.<Fruit>makeJuice(fruitBox);	// OK
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) {
  ...
}​

 

7. 지네릭 타입의 형변환

지네릭 타입과 넌지네릭(non-generic) 타입간의 형변환은 항상 가능하다. 다만 경고가 발생한다. 하지만 대입된 타입이 다른 지네릭 타입간에는 형변환이 불가능하다.

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

box = (Box)objBox;		// OK
objBox = (Box<Object>)box;	// OK

Box<Object> objBox = (Box<Object>) new Box<String>(); // error​


Box<String>이 Box<? extends Object>로 형변환은 가능하다.

Box<? extends Object> wBox = new Box<String>();	// OK​

 

8. 지네릭 타입의 제거

컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고 필요한 곳에 형변환을 넣어준다. 그리고 지네릭 타입을 제거한다. 컴파일된 파일(.class)에는 지네릭 타입에 대한 정보가 없다. 이렇게 하는 주된 이유는 지네릭이 도입되기 이전의 소스코드와의 호환성을 유지하기 위해서이다. JDK1.5부터 지네릭스가 도입되었지만 아직도 원시 타입을 사용해서 코드를 작성하는 것이 가능하다. 하지만 가능하면 원시 타입을 사용하지 않도록 하자. 언젠가는 새로운 기능을 위해 하위 호환성을 포기하게 될 때가 올지도 모르기 때문이다.


지네릭 타입의 제거 과정
1. 지네릭 타입의 경계(bound)를 제거한다.
지네릭 타입이 <T extends Fruit>이면 T는 Fruit으로 치환된다. <T>인 경우에는 T가 Object로 치환된다. 그리고 클래 스 옆의 선언은 제거된다.

class Box<T extends Fruit> {
  void add(T t) {
    ...
  }
}

// ->

class Box {
  void add(Fruit t) {
    ...
  }
}​


2. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가한다.

T get(int i) {
  return list.get(i);
}

// ->

Fruit get(int i) {
  return (Fruit)list.get(i);
}​


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

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

// ->

static Juice makeJuice(FruitBox box) {
  String tmp = "";
  Iterator it = box.getList().iterator();
  while (it.hasNext()){
    tmp += (Fruit)it.next() + " ";
  }
  return new Juice(tmp);
}​

지네릭스를 완전히 이해하지는 못한 느낌이다. 계속 복습하고 찾아보아야 겠다.
728x90
반응형

'Study > JAVA' 카테고리의 다른 글

[Java] 11. 컬렉션 프레임웍(2)  (0) 2022.09.30
[Java] 11. 컬렉션 프레임웍(1)  (2) 2022.09.27
[Java] 10-3. java.time 패키지  (2) 2022.09.19
[Java] 10-1, 2. 날짜와 시간 & 형식화  (0) 2022.09.16
[Java] 9-2. 유용한 클래스  (0) 2022.09.14

댓글