자바의 정석을 참고했습니다.
컬렉션 클래스에 컴파일 시 타입 체크를 해주는 기능.
장점
class Box<T> {
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item
}
}
Box<T>에서 T를 타입 변수라고 한다. 타입변수는 꼭 T가 아니라도 상황에 맞는 다른 알파벳을 지정할 수 있다.
위에서 정의한 제네릭 클래스 Box의 객체를 생성한다고 했을 때, <T>가 무슨 타입인지 지정해야 한다.
Box<String> box = new Box<String>();
new Box<String>(); 이 부분은 당연히 String이 될 것이므로 new Box<>()로 적어도 된다.
class Box<T> { }
지정한 타입을 매개변수화된 타입parameterized type이라고 한다.
static T item; // 에러
타입변수는 인스턴스 멤버로 간주하기 때문이다.
static변수는 매개변수화된 타입에 상관없이 동일한 것이어야 한다.
T[] itemArr; //T타입 배열을 위한 참조변수
...
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; //Error!
return tmpArr
}
선언T[] itemArr할 수 있지만 생성new할 수는 없다.
바로 'new'연산자 때문이다. new는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 한다. 그런데 지네릭 클래스는 컴파일 시점에 T가 어떤 타입이 될 것인지 알 수 없다.
지네릭 클래스 Box<T>를 사용하는 예제 코드다.
자세한 것은 주석에.
package com.javaex.generics;
import java.util.ArrayList;
import java.util.List;
public class FruitsBoxEx1 {
public static void main(String[] args) {
//매개변수화된 타입으로, 어떤 타입이든 올 수 있다.
Box<Fruit> fruitBox = new Box<>(); // == new Box<Fruit>();
Box<Apple> appleBox = new Box<>();
Box<Toy> toyBox = new Box<>();
/*
*Box<Fruit> box = new Box<Apple>(); 은 매개변수화된 타입이 다르므로 에러가 발생한다.
*반드시 참조 변수와 생성자의 매개변수화된 타입이 일치해야만 한다.
*다형성은
* Box<Apple> = new FruitBox<Apple>();
* 이렇게 사용할 수 있다.
* */
//void T add(T item) -> void add(Fruit item)
fruitBox.add(new Fruit());
fruitBox.add(new Grape());
fruitBox.add(new Apple());
appleBox.add(new Apple());
// appleBox.add(new Graple()); //사과 박스엔 사과만 담을 수 있다.
toyBox.add(new Toy());
System.out.println("fruitBox : " + fruitBox);
System.out.println("appleBox : " + appleBox);
System.out.println("toyBox : " + toyBox);
}
}
class Box<T> {
List<T> list = new ArrayList<>();
void add(T item) {
list.add(item);
}
T getItem(int i) {
return list.get(i);
}
int size() {
return list.size();
}
public String toString(){
return list.toString();
}
}
class Fruit {
@Override
public String toString(){
return "Fruit";
}
}
class Apple extends Fruit {
@Override
public String toString(){
return "Apple";
}
}
class Grape extends Fruit {
@Override
public String toString(){
return "Grape";
}
}
class Toy {
@Override
public String toString(){
return "Toy";
}
}
지네릭은 타입 안정성을 보장한다고 했다. <T>타입으로 무슨 타입이든 받는 것은 과연 안전할까? 좀더 안전하게 <T>에 지정할 수 있는 타입을 제한할 수 있는 방법이 있다.
class Box<T extends Fruit>
간단하다. 지네릭 타입에 'extends'를 사용하면 된다. 구태여 설명할 것도 없겠지만, 매개변수화된 타입으로 Fruuit을 상속하는 클래스만 받겠다는 뜻이다. 참고로 인터페이스를 구현한 클래스를 매개변수화된 타입으로 받겠다고 선언할 때도 'extends'를 사용한다.
package com.javaex.generics;
import java.util.ArrayList;
import java.util.List;
public class FruitsBoxEx2 {
public static void main(String[] args) {
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
FruitBox<Grape> grapeBox = new FruitBox<>();
ToyBox<Toy> toyBox = new ToyBox<>();
fruitBox.add(new Fruit());
fruitBox.add(new Apple());
fruitBox.add(new Grape());
appleBox.add(new Apple());
grapeBox.add(new Grape());
System.out.println("fruitBox - " + fruitBox);
System.out.println("appleBox - " + appleBox);
System.out.println("grapeBox - " + grapeBox);
}
}
//Eatable을 구현하면서, Fruit를 상속하는 타입만 <T>로 받는다.
class FruitBox<T extends Fruit & Eatable> extends Box<T> {}
//Funable을 구현한 타입만 <T>로 받는다.
class ToyBox<T extends Funable> {}
class Box<T> {
List<T> list = new ArrayList<>();
void add(T item) {
list.add(item);
}
T getItem(int i) {
return list.get(i);
}
int size() {
return list.size();
}
public String toString(){
return list.toString();
}
}
interface Eatable {}
interface Funable {}
class Fruit implements Eatable {
@Override
public String toString(){
return "Fruit";
}
}
class Apple extends Fruit {
@Override
public String toString(){
return "Apple";
}
}
class Grape extends Fruit {
@Override
public String toString(){
return "Grape";
}
}
class Toy implements Funable{
@Override
public String toString(){
return "Toy";
}
}
간단하게 조금 설명하자면.
class Box<T>
이 클래스는 세상 모든 박스의 원형이다. 말하자면 나로써는 감히 상상만 해볼 수밖에 없는 Box의 이데아! 우리는 이 박스를 이용해서 세상에 존재하는 모든 물건을 담을 수도 있을 것이다. 그러면 아마 잡동사니 박스가 되겠지. 그것을 도저히 견딜 수 없는 인간. 그러니까 너무나 게을러서 이 박스에 뭐가 들었는지 하나하나 찾아보기가 너무나 귀찮은 인간은 분류라는 것을 하기로 했다.
박스를 여러 개로 나눈 다음, 1번 박스에는 먹을 수 있는 과일만 담기로 하고, 2번 박스에는 장난감만 담기로 한다.
코드로 이렇겠지.
class FruitBox<T extends Fruit & Eatable> extends Box<T> {}
class ToyBox<T extends Funable> {}
이렇게 하면 언제든지 1번 박스에는 과일만, 2번 박스에는 장난감만 있을 것이다. 어쩌다 실수로 과일박스에 장난감을 넣을 수도, 장난감박스에 과일을 넣을 수도 없다.
그러니까 제한된 제네릭 클래스 얘기다.