본문 바로가기
Language/Java

[Java] 제네릭(Generic)

by 계범 2022. 3. 11.

제네릭이란

다양한 타입의 객체들을 다루기위해 클래스,인터페이스,메서드에 쓰이는 타입 매개 변수이다.

 

장점
1. 타입 안정성을 제공
2. 타입체크와 형변환을 생략가능

 

제네릭 클래스 선언

제네릭 타입은 클래스와 메서드에 선언 가능하다.

 

class Box{
	Object 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;
    }
    
    T getItem(){
    	return item;
    }
}

클래스 옆에 '<T>'를 붙여서 선언.

Obejct를 모두 'T'로 변경

 

여기서 T는 '타입 변수(type variable)'이라고 하며, 'Type'의 첫 글자를 따온 것이다.

타입변수는 T만이 아닌 다른 것을 사용해도 된다.

(E: Element, K: Key, V: Value 등등)

기호와 종류만 다를 뿐 '임의의 참조형 타입'을 의미

 

제네릭 도입 이전의 코드와 호환을 위해, 제네릭 클래스여도 예전의 방식으로 객체를 생성하는 것을 허용한다. 하지만 경고가 발생한다.

 

Box b = new Box(); // T는 Object로 간주.
b.setItem("ABC"); // 경고. unchecked or unsafe operation
b.setItem(new Object()); // 경고.

Box<Object> b = new Box(); //  T에 타입 지정해두면 경고 안뜸.

 

제네릭의 용어

class Box<T> {}
Box<T> : 제네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽음.
T          : 타입 변수 또는 타입 매개변수(T는 타입 문자)
Box       : 원시 타입(raw type)
Box<String> b = new Box<String>();

타입 매개변수에 타입을 지정하는 것을 '제네릭 타입 호출'이라고 하고,

지정된 타입 'String'을 '매개변수화된 타입' -> '대입된 타입'이라고 부른다.

 

제네릭의 제한

모든 객체에 대해 동일하게 동작해야하는 static멤버에 타입 변수 T를 사용할 수 없다.

T는 인스턴스변수로 간주되기 때문이다.

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

배열에도 사용 불가.

: new 연산자 때문. new 연산자는 컴파일 시점에 타입 T가 뭔지 알아야함. 하지만 클래스 컴파일 시점에서 T는 어떤 타입이 될지 전혀 알 수 없다. instanceof연산자도 마찬가지로 사용 불가.

 

꼭 제네릭 배열이 필요할땐, new연산자대신 'Reflection API'의 newInstance() 또는, Object 배열 생성하여 복사 한 다음 T[]로 형변환해서 씀.

 

  1. 스태틱 타입에 사용 불가
  2. 배열에 사용불가

 

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

import java.util.ArrayList;

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 static 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();}
}
결과
[Fruit, Apple]
[Apple, Apple]
[Toy]

 

제한된 제네릭 클래스

제네릭 타입엔 모든 타입의 종류를 지정할 수 있다.

제한된 타입만 넣을 수 있게 하고 싶다면, extends를 사용하면 된다.

 

class FruitBox<T extends Fruit>

Fruit 타입과 자손 타입만 담을 수 있게 제한된다.

인터페이스도 여기선 implements가 아닌 extends로 쓴다.

클래스 Fruit의 자손이면서 Eatable 인터페이스도 구현해야한다면 '&'기호로 연결

class FruitBox<T extends Fruit & Eatable>

 

와일드 카드

'?' 기호로 표현하며, 어떠한 타입도 될 수 있다는 의미이다.

 

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

 

제네릭 메서드

메서드의 선언부에 제네릭 타입이 선언된 메서드.

Collections.sort()가 제네릭 메서드이며, 제네릭 타입의 선언 위치는 반환타입 바로 앞이다.

 

제네릭 클래스에 정의된 타입변수와 메서드에 정의된 타입변수는 별개의 것이다.

 

static Juice makeJuice(FruitBox<? extends Fruit> box){
	String temp = "";
    for(Fruit f : box.getList()){
    	temp += f + " ";
    }
    return new Juice(temp);
}
// 메서드 타입으로 변환
static <T extends Fruit> Juice makeJuice(FruitBox<T> box){
	String temp = "";
    for(Fruit f : box.getList()){
    	temp += f + " ";
    }
    return new Juice(temp);
}

 

호출할땐 원칙 상으론 타입 변수에 타입을 대입해야하나, 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략 가능하다.

 

//원칙
Juicer.<Fruit>makeJuice(fruitBox));

//생략가능
Juicer.makeJuice(fruitBox));

 

제네릭 메서드를 통한 코드 줄이기

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


//제네릭 메서드 사용
public satatic <T extends Product> void printAll(ArrayList<T> list,
                                    	ArrayList<T> list2){
 			// ...                           
}

 

컴파일러와 제네릭 타입

컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다.

그리고나서 제네릭 타입을 제거한다. 즉, 컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없다.

이렇게 하는 이유는 제네릭타입이 없던 이전 버전과의 호환성을 유지하기 위해서이다.

 

1. 제네릭 타입의 경계(bound)를 제거.

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);
}

 

참조

'Java의 정석' 책

댓글