[Java/자바] 제네릭스(Generics)란 ?

컬렉션 클래스들 타입 다음에 <String>, <Integer>, <Character> 등으로 붙여진 형식들을 많이 보셨을건데, 이게 바로 제네릭스입니다.

제네릭스란 클래스가 가질 타입을 미리 명시해줌으로써 제네릭스를 사용한 클래스의 객체가 형변환을 하지 않고 사용할 수 있도록 해줍니다.

하지만 만약 List<String> list = new ArrayList<>(); 로 제네릭스를 명시했는데 이 list 객체에 Integer 타입을 넣게 된다면 컴파일 에러가 발생하므로

제네릭스를 정하면 그 제네릭스에 맞는 타입을 사용해야 합니다.

 

제네릭스의 장점
1. 타입의 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

 

대표적인 제네릭스의 타입

<T> 타입 제네릭스는 Object로 모든 종류의 타입을 지정 가능 합니다.

타입 설명
<T> Object
<E> Element
<K> Key
<V> Value
<N> Number

 

위 표를 예시로 HashMap 컬렉션 클래스를 보면 다음과 같이 제네릭스가 정의되어 있습니다.

public class HashMap <K, V> { ... }

 


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

 

제네릭스 클래스 Box<K, V>가 다음과 같이 정의되어 있다고 가정해봅시다.

이 Box<K, V>의 객체에는 두 가지 종류 K, V타입의 객체만 저장할 수 있습니다.

아래 코드대로라면, K는 String 타입이 되고, V는 Interger 타입이 됩니다.

 

주의할점은, 타입 파라미터로 명시할 수 있는 것은 참조형타입만 올 수 있습니다.

즉, 기본형 타입인 int, char, double 등은 타입으로 올 수 없습니다.

class Box<K, V> {
	HashMap<String, Integer> hashmap = new HashMap<>();
}

 

예제
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 -> fruitBox로 다운 캐스팅했지만 제네릭스 <Fruit>을 지정해 형변환이 생략되었다.
		Box<Fruit> fruitBox = new Box<>(); 
		// Box -> appleBox로 다운 캐스팅했지만 제네릭스 <Apple>을 지정해 형변환이 생략되었다.
		Box<Apple> appleBox = new Box<>();
		// Box -> toyBox로 다운 캐스팅했지만 제네릭스 <Toy>을 지정해 형변환이 생략되었다.
		Box<Toy>   toyBox   = new Box<>();

		fruitBox.add(new Fruit());
		fruitBox.add(new Apple()); // OK. Box<Fruit> => Apple은 Fruit의 자손이다.

		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>에는 Toy만 담을 수 있다.

		System.out.println(fruitBox);
		System.out.println(appleBox);
		System.out.println(toyBox);
	}
}

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

 

위 예제를 보면 제네릭스를 지정함으로써 다운 캐스팅에서도 형변환이 생략된 것을 볼 수 있습니다.

 


제한된 제네릭스와 와일드 카드

<T> 타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한되어 있지만,

모든 종류의 타입을 지정할 수 있습니다.

 

하지만 제네릭스 타입에 "extends"를 사용한다면, 특정 타입의 자손들만 대입할 수 있게 제한 할 수 있습니다.

 

아래 코드 처럼 extends를 사용하면 Fruit와 그의 자손만 타입으로 지정가능하도록 제한을 걸 수가 있습니다.

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

 

와일드카드

다음과 같은 와일드 카드로 특정 범위 내로 좁혀 제한을 걸 수있습니다.

?는 와일드 카드로 "알 수 없는 타입"이라는 의미를 가지며, 어떠한 타입도 될 수 있습니다.

<? extends T> : T와 T의 자손 타입만 가능하다. (상한 제한)
<? super T> : T와 T의 부모(조상) 타입만 가능하다. (하한 제한)
<?> : 모든 타입 가능(제한 없음)  ,  <? extends Object>랑 동일하다.

 

예제
import java.util.ArrayList;

class Fruit implements Eatable {
	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"  ;}}

interface Eatable {}

class FruitBoxEx2 {
	public static void main(String[] args) {
		FruitBox<Fruit> fruitBox = new FruitBox<>();
		FruitBox<Apple> appleBox = new FruitBox<>();
		FruitBox<Grape> grapeBox = new FruitBox<>();
//		FruitBox<Toy>   toyBox    = new FruitBox<Toy>();   // 에러. (제한) Toy는 Fruit의 자손이 아니다.

		fruitBox.add(new Fruit());
		fruitBox.add(new Apple());
		fruitBox.add(new Grape());
		appleBox.add(new Apple());
//		appleBox.add(new Grape());  // 에러. Grape는 Apple의 자손이 아니다.
		grapeBox.add(new Grape());

		System.out.println("fruitBox-"+fruitBox);
		System.out.println("appleBox-"+appleBox);
		System.out.println("grapeBox-"+grapeBox);
	}  // main
}

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

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


참고자료 : 자바의 정석3