제네릭(Generic)이란?
- 데이터의 타입을 일반화한다는 것을 의미 (사전적 의미는 일반적인)
- 제네릭을 활용하면 타입 변환 및 타입 검사에 들어가는 코드 생략이 가능하다.
제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 지정하는 방법을 말한다.
컴파일 시에 미리 타입 검사를 시행하게 되면 클라스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있으며, (잘못 된 타입인 경우 컴파일 에러를 발생시킴)
반환 값에 대한 타입 변환 및 타입 검사에 들어가는 코드 생략이 가능해진다.
(instanceof 비교 및 다운캐스팅 작성 불필요)
JDK 1.5 버전부터 추가된 문법이다.
제네릭 작성 방법
- 제네릭 설정은 클래스 선언부 마지막 부분에 다이아몬드 연산자를 이용하여 작성하게 된다.
- 다이아몬드 연산자 내부에 작성하는 영문자는 관례상 대문자로 작성한다.
- 다이아몬드 연산자 내부에 작성한 T는 타입 변수라고 부른다.
- 타입변수를 자료형 대신 사용하는데, 가상으로 존재하는 타입이며 T가 아닌 영문자를 사용해도 무방하다.
- 또한 여러 개의 타입 변수를 작성할 때는 , 를 사용하여 여러 개 기술할 수도 있다.
- 사용하는 쪽에서는 작성한 제네릭 클래스를 이용할 시 실제 사용할 타입을 타입 변수 자리에 맞춰서 넣어주게 되면 컴파일 시점에서 타입이 결정되게 된다.
제네릭 용어
Class Box<T> { }
Box<T> - 제네릭 클래스. T의 Box 또는 T Box 라고 읽는다.
T - 타입 변수 또는 타입 매개변수. (T는 타입 문자)
Box - 원시 타입(taw type)
Box<String> b = new Box<String>();
String - 대입된 타입(매개변수화된 타입)
=>컴파일 후에는 Box<String>은 '원시타입'인 Box로 바뀐다. 즉 제네릭 타입이 제거됨.
현재 해당 필드는 타입이 결정되지 않은 상태이다.
private T value;
setter/getter 작성 시에도 구체적인 타입 대신 T를 이용할 수 있다.
public void setValue(T value){
this.value = value;
}
public T getValue(){
return this.value;
}
타입을 Integer로 인스턴스를 생성하는 경우
GenericTest<Integer> gt1 = new GenericTest<Integer>();
메소드의 인자 및 반환 값 모두 Integer 타입인 것을 알 수 있다.
gt1.setValue(10);
System.out.println(gt1.getValue()); //10
System.out.println(gt1.getValue() instanceof Integer); //true
타입을 String으로 인스턴스를 생성하는 경우
GenericTest<String> gt2 = new GenericTest<String>();
gt2.setValue("홍길동");
System.out.println(gt2.getValue()); //홍길동
System.out.println(gt2.getValue() instanceof String); //true
JDK 7부터 타입 선언시 타입변수가 작성되면 타입 추론이 가능하기 때문에 생성자 쪽의 타입을 생략하고 사용할 수 있게 한다.
단, 타입이 명시되지 않은 빈 다이아몬드 연산자는 사용해야 한다.
GenericTest<Double> gt3 = new GenericTest<>();
gt3.setValue(0.5);
System.out.println(gt3.getValue()); //0.5
System.out.println9(gt3.getValue() instanceof Double); //true
- Animal (Interface)
- Animal interface를 상속 받는 Mammal, Reptile 클래스
- Mammal을 상속 받는 Rabbit (cry메소드 있음)
- Reptile을 상속 받는 Snake
- Rabbit을 상속 받는 Bunny
- Bunny를 상속 받는 DrunkenBunny => 총 7개의 클래스를 한 코드에 작성해놓음.
public interface Animal{} //해당 인터페이스를 상속 받는 Mammal(포유류) 클래스를 만들자
public class Mammal implements Animal{} //파충류 클래스도 만든다(Reptile)
public class Reptile implements Animal{}//Mammal클래스를 상속 받는 Rabbit 클래스를 만들자
public class Rabbit extends Mammal{
public void cry(){
System.out.println("토끼가 울음소리를 냅니다. 끾끾!");
} //Reptile클래스를 상속받는 Snake 클래스를 만들자
public class Snake extends Reptile{} //Rabbit을 상속받는 Bunny 클래스를 만들자
public class Bunny extends Rabbit{
@Override //cry()메소드 오버라이딩
public void cry(){
System.out.println("바니바니 당근당근");
} //Bunny를 상속 받는 DrunkenBunny를 만들자
public class DrunkenBunny extends Bunny{
@Override
public void cry(){
System.out.println("보ㅑ니바니 당귿당근@#!");
} //이제 제네릭을 이용한 클래스를 만들자. RabbitFarm(토끼농장)
제네릭을 이용한 RabbitFarm 클래스
- 제네릭 표현 시 인터페이스도 extends로 표현한다. (implecents를 사용하지않음)
public class RabbitFarm <T extends Rabbit> {
/* 타입 변수는 아직 어떤 토끼가 될지 모른다. 단, 토끼이거나 토끼의 후손만 가능하다. */
private T animal;
public RabbitFarm() {}
public RabbitFarm(T animal) {
this.animal = animal;
}
public void setAnimal(T animal) {
this.animal = animal;
}
public T getAnimal() {
return this.animal;
}
}
실행클래스 Application1
- 제네릭 클래스 작성 시 extends 키워드를 이용하면 특정 타입만 사용하도록 제한을 걸 수 있다.
=> 즉, 토끼의 후손이거나 토끼인 경우에만 타입으로 사용이 가능하다. 그 외의 타입으로 타입 지정 시 컴파일 단계에서 에러를 발생시킨다.
/* Animal 타입으로는 제네릭 클래스 인스턴스 생성이 불가능하다. */
//RabbitFarm<Animal> farm1 = new RabbitFarm<>();
/* Mammal 타입으로는 제네릭 클래스 인스턴스 생성이 불가능하다. */
//RabbitFarm<Mammal> farm1 = new RabbitFarm<>();
/* 전혀 다른 타입인 Snake 타입으로는 제네릭 클래스 인스턴스 생성이 불가능하다. */
//RabbitFarm<Snake> farm1 = new RabbitFarm<>();
/* Rabbit 타입이나 Rabbit의 후손으로는 인스턴스 생성이 가능하다. */
RabbitFarm<Rabbit> farm1 = new RabbitFarm<>();
RabbitFarm<Bunny> farm2 = new RabbitFarm<>();
RabbitFarm<DrunkenBunny> farm3 = new RabbitFarm<>();
/* 제네릭을 사용해서 올바른 타입을 타입 변수로 지정하는 경우에는 인스턴스 내부에 있는 타입 자체가
* Rabbit 타입을 가지고 있는 것이 보장되어 있기 때문에 형번환 생략 가능하다.
* */
farm1.setAnimal(new Rabbit());
((Rabbit)farm1.getAnimal()).cry();//토끼가 울음소리를 냅니다. 끾끾!
farm1.getAnimal().cry(); //토끼가 울음소리를 냅니다. 끾끾!
farm2.setAnimal(new Bunny());
farm2.getAnimal().cry(); //바니바니 당근당근
farm3.setAnimal(new DrunkenBunny());
farm3.getAnimal().cry(); //보ㅑ니바니 당귿당근@#
WildCard(와일드카드)
- 제네릭 클래스 타입의 객체를 메소드의 매개변수로 받을 때, 그 객체의 타입 변수를 제한할 수 있다.
<?> : 제한 없음
<? extends Type> : 와일드 카드의 상한 제한 (Type과 Type의 후손을 이용해 생성한 인스턴스만 인자로 사용 가능)
<? super Type> : 와일드 카드의 하한 제한 (Type과 Type의 부모를 이용해 생성한 인스턴스만 인자로 사용 가능)
WildCardFarm 클래스
- 매개변수로 전달 받는 토끼 농장을 구현할 때 사용한 타입 변수에 대해 제한할 수 있다.
/* 토끼 농장에 있는 토끼가 어떤 토끼던 상관 없다. */
public void anyType(RabbitFarm<?> farm) {
farm.getAnimal().cry();
}
/* 토끼 농장의 토끼는 Bunny이거나 그 후손 타입으로 만들어진 토끼농장만 매개변수로 사용 가능 */
public void extendsType(RabbitFarm<? extends Bunny> farm) {
farm.getAnimal().cry();
}
/* 토끼 농장의 토끼는 Bunny이거나 그 부모 타입으로 만들어진 토끼농장만 매개변수로 사용 가능 */
public void superType(RabbitFarm<? super Bunny> farm) {
farm.getAnimal().cry();
}
와일드카드 실행 클래스 Application2
WildCardFarm wildCardFarm = new WildCardFarm();
/* 농장 생성 자체가 불가능한 것은 매개변수로 사용할 수 없다. */
//wildCardFarm.anyType(new RabbitFarm<Mammal>(new Mammal()));
/* 매개변수의 타입 제한이 없는 경우 */
wildCardFarm.anyType(new RabbitFarm<Rabbit>(new Rabbit()));
wildCardFarm.anyType(new RabbitFarm<Bunny>(new Bunny()));
wildCardFarm.anyType(new RabbitFarm<DrunkenBunny>(new DrunkenBunny()));
/* Bunny이거나 Bunny의 후손 토끼 농장만 매개변수로 사용이 가능한 경우 */
//wildCardFarm.extendsType(new RabbitFarm<Rabbit>(new Rabbit()));
wildCardFarm.extendsType(new RabbitFarm<Bunny>(new Bunny()));
wildCardFarm.extendsType(new RabbitFarm<DrunkenBunny>(new DrunkenBunny()));
/* Bunny이거나 Bunny의 상위 타입 토끼 농장만 매개변수로 사용이 가능한 경우 */
wildCardFarm.superType(new RabbitFarm<Rabbit>(new Rabbit()));
wildCardFarm.superType(new RabbitFarm<Bunny>(new Bunny()));
//wildCardFarm.superType(new RabbitFarm<DrunkenBunny>(new DrunkenBunny()));