Generics

rivermt·2023년 4월 10일
0

JAVA

목록 보기
6/9

지네릭스?

지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크 (compile-time type check)를 해주는 기능이다.

컴파일 시에 객체의 타입을 체크한다면 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄일 수 있게된다.

타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 워낼의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

ArrayList와 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있긴하지만 보통의 경우 한 종류의 객체를 담는다. 그런데도 꺼낼때마다 타입체크를 하고 형변환을 하는 것은 아무래도 불편하다. 게다가 원하지 않는 종류의 객체가 포함되는 것을 막을 수도 없다. 이러한 문제들을 지네릭스를 통해 해결할 수 있다.

장점
1. 타입 안정성 제공
2. 타입체크와 형변환을 생략가능 -> 코드 간결

지네릭 클래스 선언

지네릭 타입은 클래스와 메서드에 선언할 수 있다. 먼저 클래스에 선언하는 지네릭 타입을 살펴보자.
클래스 Box가 다음과 같이 정의되어있다고 가정하자.

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 variables)이라고 한다. 이는 임의의 참조형 타입을 뜻한다. 지네릭 클래스가 된 Box 클래스의 객체를 생성시에는 참조변수와 생성자에 타입 T대신에 실제 타입을 지정한다.

Box<String> b = new Box<String>(); // 실제 타입 지정
b.setItem("ABC");
String item = b.getItem(); // 형변환 필요없음

지네릭스 용어

class Box<T> {}

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

컴파일 후에 Box<String>, Box<Integer> 는 이들의 원시타입Box로 바꾸니다. 즉 지네릭 타입이 제거가 된다.

지네릭스의 제한

모든 객체에 대해 동이랗게 동작해야하는 static 멤버에 타입 변수 T를 사용할 수는 없다. T는 인스턴스 변수로 간주되기 때문이다. static멤버는 인스턴스 변수를 참조할 수가 없다.
static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 된다.

지네릭 타입의 배열은 직접적으로 생성할 수 없다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만 new T[10]과 같이 배열을 생성하는 것은 안된다.
그 이유는 new 연산자 때문이다. 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야한다.
꼭 지네릭 배열을 생성해야한다면 new 대신 Reflection APInewInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object배열을 생성해서 복사한 다음 T[]로 형변환하는 방법 등을 사용해야 한다.

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

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

제한된 지네릭 클래스

지네릭 타입에 extends 를 사용하면 특정 타입의 자손들만 대입할 수 있도록 제한가능하다.

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

여전히 한 종류의 타입만 담을 수 있으며, Fruit 클래스의 자손들만 담을 수 있다는 뜻이다.
add()의 매개변수의 타입 T도 Fruit와 그 자손 타입이 될 수 있으므로, 여러 과일을 담을 수 있게 된다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>(); 
fruitBox.add(new Apple()); // ok
fruitBox.add(new Grape()); // ok

만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면 이때도 extends 를 사용한다. (implements 아님)

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

Fruit의 자손이면서 Eatable도 구현해야한다면 & 연산자를 사용한다.

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

와일드 카드

지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다.
지네릭 타입은 컴파일러가 컴파일할 때만 사용하거 제거해버리기 때문이다.
이럴 때 사용하도록 만들어진게 와일드 카드 ? 이다.

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

다음 예제를 살펴보자

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

이 메서드는 매개변수로 FruitBox<Fruit> 뿐 아니라 FruitBox<Apple>, FruitBox<Grape>도 가능하다.

참고자료

자바의 정석 - 남궁성

profile
화이팅!!

0개의 댓글