다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(complie-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
타입 안정성을 높인다는 것은 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.
지네릭스 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.
class Box<T> { // 지네릭 타입 T를 선언
T item;
void setItem(T item) { this.item = item; }
T getItem() { return item; }
}
Box<'T'>에서 T를 '타입 변수(type variable)'라고 하며 'Type'의 첫 글자에서 따온 것이다. 타입 변수는 T가 아닌 다른 것을 사용해도 된다. ArrayList<'E'>의 경우 'Element(요소)'의 첫 글자를 따서 사용했다. 타입 변수가 여러 개인 경우에는 Map<K, V> 처럼 콤마를 사용하면 된다. 무조건 T를 사용하기보다 상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋다.
Box<T> : 지네릭 클래스 'T의 Box' 또는 'T Box'라고 읽는다.
T : 타입 변수 또는 타입 매개변수.<T는 타입 문자)
Box : 원사 타입(ray type)
지네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다. 지네릭스는 이처럼 인스턴스별로 다르게 동작하도록 하려고 만든 기능이다.
Box<Apple> appleBox = new Box<Apple>(); // OK. Apple객체만 저장가능
Box<Grape> grapeBox = new Box<Grape>(); // OK. Grape객체만 저장가능
그러나 모든 객체에 대해 동일하게 동작해야하는 static멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스변수로 간주되기 때문이다.
class Box<T> {
static T item; // 에러
static int compare(T t1, T t2) { ... } // 에러
...
static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다. 즉 'Box<'Apple'>'.item과 Box<'Grape'>.item이 다른 것이어서는 안된다는 것이다. 그리고 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, '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를 피연산자로 사용할 수 없다.
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" ; }}
public 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<>();
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]
와일드카드는 지네릭스를 사용할 때 지네릭스로 지정한 타입이 달라도 지네릭스를 사용할 수 있는 방법이다.
와일드카드 기호는 '?'로 표현하는데 이 와일드카드로 어떠한 타입도 될 수 있다.
"?"가 Object타입과 다를 게 없기 때문에 다음과 같이 'extends'와 'supber'로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.
아래 예제를 통해 확인해보자
<12-3>
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 Juice {
String name;
Juice(String name) { this.name = name + "Juice"; }
public String toString() { return name; }
}
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box){
String tmp = "";
for(Object f : box.getList())
tmp += f + " ";
return new Juice(tmp);
}
}
public class FruitBoxEx3 {
public static void main(String[] args) {
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
fruitBox.add(new Apple());
fruitBox.add(new Grape());
appleBox.add((new Apple()));
appleBox.add((new Apple()));
System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));
} // main
}
class FruitBox<T extends Fruit> extends Box<T> {}
class Box<T> {
ArrayList<T> list = new ArrayList<>();
void add(T item) { list.add(item); }
T get(int i) { return list.get(i); }
ArrayList<?> getList() { return list; }
int size() { return list.size(); }
public String toString() { return list.toString(); }
}
결과
Apple Grape Juice
Apple Apple Juice
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box){
String tmp = "";
for(Object f : box.getList())
tmp += f + " ";
return new Juice(tmp);
}
}
이 부분에서 "FruitBox<? extends Fruit> box" 부분을 보면 Fruit로 부터 상속 받은 클래스 타입들에 대해서만 상관 없이 매개변수로 받겠다는 의미이다.
이렇게 되면 Fruit로 상속받은 Apple타입 클래스와 Grape클래스 어느 클래스 타입이 들어와도 해당 메서드를 실행시킬 수 있다.
[12-4]
import java.util.*;
class Fruit{
String name;
int weight;
Fruit(String name, int weight){
this.name = name;
this.weight = weight;
}
public String toString() { return name + " ("+weight+") "; }
}
class Apple extends Fruit{
Apple(String name, int weight){
super(name, weight);
}
}
class Grape extends Fruit{
Grape(String name, int weight){
super(name, weight);
}
}
class AppleComp implements Comparator<Apple>{
public int compare(Apple t1, Apple t2){
return t2.weight - t1.weight;
}
}
class GrapeComp implements Comparator<Grape>{
public int compare(Grape t1, Grape t2){
return t2.weight - t1.weight;
}
}
class FruitComp implements Comparator<Fruit>{
public int compare(Fruit t1, Fruit t2){
return t1.weight - t2.weight;
}
}
public class FruitBoxEx4 {
public static void main(String[] args) {
FruitBox<Apple> appleBox = new FruitBox<Apple>();
FruitBox<Grape> grapeBox = new FruitBox<Grape>();
appleBox.add(new Apple("GreenApple", 300));
appleBox.add(new Apple("GreenApple", 100));
appleBox.add(new Apple("GreenApple", 200));
grapeBox.add(new Grape("GreenGrape", 400));
grapeBox.add(new Grape("GreenGrape", 300));
grapeBox.add(new Grape("GreenGrape", 200));
Collections.sort(appleBox.getList(), new AppleComp());
Collections.sort(grapeBox.getList(), new GrapeComp());
System.out.println(appleBox);
System.out.println(grapeBox);
System.out.println();
Collections.sort(appleBox.getList(), new FruitComp());
Collections.sort(grapeBox.getList(), new FruitComp());
System.out.println(appleBox);
System.out.println(grapeBox);
} // main
}
class FruitBox<T extends Fruit> 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);
}
ArrayList<T> getList() { return list; }
int size() {
return list.size();
}
public String toString(){
return list.toString();
}
}
결과
[GreenApple (300) , GreenApple (200) , GreenApple (100) ]
[GreenGrape (400) , GreenGrape (300) , GreenGrape (200) ]
[GreenApple (100) , GreenApple (200) , GreenApple (300) ]
[GreenGrape (200) , GreenGrape (300) , GreenGrape (400) ]
예제에서 보면 Collections.sort() 메서드를 확인해보면 다음과 같다.
static <T> void sort(List<T> list, Comparator<? super T> c)
sort메서드가 위와 같이 정의 되어있는데 와일드카드를 사용하지 않고 사용했다고 가정해보고 타입 매개변수 T에 Apple이 대입되면, 아래와 같이 대입 될 것이다.
static void sort(List<Apple> list, Comparator<Apple> c)
현재까지는 문제가 없어보이지만 Apple 대신 Grape가 대입된다고 하면 List를 정렬하기 위해 Comparator가 필요하다. <>안에 타입이 맞지 않기 때문에 Comparator로는 List를 정렬할 수 없다.
그러면 Comparator클래스를 implements하여 구현한 구현체들을 타입에 맞게 생성을 해서 타입에 맞게 대입을 해야한다.
그러면 코드의 중복 문제도 있지만 새로운 타입이 생길 떄 마다 같은 코드르 반복해서 만들어야 한다는 것이 더 문제가 된다.
앞서 Collections.sort 메서드를 살펴본 것 처럼 원래 정의되어 있는 것을 보면
static void sort(List list, Comparator<? super T> c)
위 코드에서 T에 Apple이 대입되면 아래와 같이 된다.
static <T> void sort(List<Apple> list, Comparator<? super Apple> c)
매개변수의 타입이 Comparator<? super Apple>이라는 의미는 Comparator의 타입 매개변수로 Apple과 그 조상이 가능하다는 뜻이다. 즉, Comparator, Comparator, Comparator 중의 하나가 두 번째 매개변수로 올 수 있다는 뜻이다.
Comparator<? super Apple>
Comparator<Apple>
Comparator<Fruit>
Comparator<Object>
Comparator<? super Grape>
Comparator<Grape>
Comparator<Fruit>
Comparator<Object>
그래서 위 예제 <12-4>에서 만든 구현체인 FruitComp를 만들어 사용하면 List과 List를 모두 정렬할 수 있다.
Apple과 Grape는 Fruit의 조상으로 정의되어 있기 때문에 가능한 것이다.
※ 참고 문헌
남궁성, 『Java의 정석 3nd Edition』, 도우출판(2016) 책으로 공부하고 정리한 내용 입니다.