💁♀️ 제네릭(Generic)이란,
데이터의 타입을 일반화 한다는 의미.
제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 지정하는 방법.
🙋 잠깐 ! 제네릭의 장점은 뭔가요?
- 컴파일 시에 미리 타입 검사를 시행하면 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있음 (잘못 된 타입인 경우 컴파일 에러를 발생시킴)
- 반환 값에 대한 타입 변환 및 타입 검사에 들어가는 코드 생략이 가능해짐
(instanceof 비교 및 다운 캐스팅 작성이 불필요)
📌 Ref.
* 제네릭 설정은 클래스 선언부 마지막 부분에 '다이아몬드 연산자를 이용'하여 작성
* 내부에 작성하는 영문자는 관례상 '대문자'로 작성
* 다이아몬드 연산자 내부에 작성한 T를 '타입 변수'라고 부르며 타입 변수를 자료형 대신 사용
* '가상으로 존재하는 타입'으로 T가 아닌 영문자를 사용해도 무방
* 사용하는 쪽에서 작성한 제네릭 클래스를 이용할 시 실제 사용할 타입을 타입 변수 자리에 맞추어 넣어주면
'컴파일 시점에 타입이 결정'
public class GenericTest<T> {
private T value; >>> 현재 해당 필드는 타입이 결정되지 않은 상태
>>> getter, setter 작성 시에도 구체적인 타입 대신 T를 이용
// getter, setter
public void setValue(T value) { >>> 결정되어있지않음
this.value = value;
}
public T getValue() {
return value;
}
}
GenericTest<Integer> gt1 = new GenericTest<Integer>();
>>> 따라서 GenericTest 클래스의 필드, 생성자 안에 있는 T는 Integer
gt1.setValue(10);
System.out.println(gt1.getValue()); // 10 출력
>>> 메소드의 인자 및 반환 값 모두 Integer 타입
GenericTest<String> gt2 = new GenericTest<String>();
gt2.setValue("치즈");
System.out.println(gt2.getValue()); // 치즈 출력
>>> 위에서 <T>는 Integer였지만 여기서는 String으로 사용 가능
GenericTest<Double> gt3 = new GenericTest<>();
📌 Ref.
* JDK 1.7부터 타입 선언 시 타입 변수가 작성되면 타입 추론이 가능하기 때문에 생성자 쪽의 타입을
생략하고 작성할 수 있음 (단, 빈 다이아몬드 연산자는 사용해야함)
💁 제네릭 클래스의 상속(extends)
: 제네릭 클래스 작성 시 extends 키워드를 이용하면 특정 타입만 사용하도록 제한을 걸 수 있음
- ex) 토끼의 후손이거나 토끼인 경우에만 타입으로 사용 가능하며 그 외의 타입 지정시 컴파일 단계에서 에러를 발생
💁 제네릭 클래스의 와일드 카드(wild card)
: 제네릭 클래스 타입의 객체를 메소드의 매개변수로 받을 때, 그 객체의 타입 변수를 제한할 수 있음
public interface Animal {
}
public class Mammal implements Animal {
}
public class Rabbit extends Mammal {
public void cry() {
System.out.println("토끼가 울음소리를 냅니다. 끾끼익!!");
}
}
public class Bunny extends Rabbit {
@Override
public void cry() {
System.out.println("바니바니 바니바니 당근! 당근! ㄴ(+_+)ㄱ");
}
}
public class DrunkenBunny extends Bunny {
@Override
public void cry() {
System.out.println("붸니붸니 붸니붸니 댕근.. 댕근.. ㄴ(@_@)ㄱ");
}
}
public class Reptile implements Animal {
}
public class Snake extends Reptile {
}
public class RabbitFarm<T extends Rabbit> {
>>> : Rabbit이거나 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 animal;
}
}
>>> Rabbit의 상위 클래스, 인터페이스인 Animal, Mammal 또는 관계가 없는 Snake 타입으로는 생성 불가능
// RabbitFarm<Animal> farm1 = new RabbitFarm<>(); // Bound mismatch 에러
// RabbitFarm<Mammal> farm1 = new RabbitFarm<>(); // Bound mismatch 에러
// RabbitFarm<Snake> farm1 = new RabbitFarm<>(); // Bound mismatch 에러
RabbitFarm<Rabbit> farm1 = new RabbitFarm<>();
RabbitFarm<Bunny> farm2 = new RabbitFarm<>();
RabbitFarm<DrunkenBunny> farm3 = new RabbitFarm<>();
>>> farm1은 Rabbit, farm2는 Bunny, farm3는 DrunkenBunny인 것 보장
farm1.setAnimal(new Rabbit());
farm1.getAnimal().cry();
// ((Rabbit)farm1.getAnimal()).cry();
>>> 이와 같은 형변환이 필요 없음(제네릭의 장점)
farm2.setAnimal(new Bunny());
farm2.getAnimal().cry();
farm3.setAnimal(new DrunkenBunny());
farm3.getAnimal().cry();
>>> farm3는 DrunkenBunny인 것이 보장이 되어있기 때문에 getAnimal의 cry()를 호출하면
DrunkenBunny의 cry가 나옴
📌 Ref.
* 제네릭을 사용해서 올바른 타입을 타입 변수로 지정하는 경우에는 인스턴스 내부에 있는 타입 자체가
Rabbit 타입을 가지고 있는 것이 보장되어 있기 때문에 형변환이 생략
public class WildCardFarm {
>>> 매개변수로 전달받는 토끼 농장을 구현할 때 사용한 타입 변수에 대해 제한할 수 있음
>>> 토끼 농장에 있는 토끼가 어떤 토끼이던 상관없다는 의미 (어떤 동물이든 상관없다는건 아님) >>> <T extends Rabbit> 범위여야하지만 어떤 토끼든 가능
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();
}
}
WildCardFarm wildCardFarm = new WildCardFarm();
>>> 토끼 농장 생성 자체가 불가능한 것은 매개변수로 사용할 수 없음
// wildCardFarm.anyType(new RabbitFarm<Mammal>());
/* 1. 매개변수의 타입 제한이 없는 경우 */
wildCardFarm.anyType(new RabbitFarm<Rabbit>(new Rabbit()));
wildCardFarm.anyType(new RabbitFarm<Bunny>(new Bunny()));
wildCardFarm.anyType(new RabbitFarm<DrunkenBunny>(new DrunkenBunny()));
/* 2. <? extends Bunny>인 경우 */
>>> Bunny이거나 Bunny를 상속받은 클래스만 가능
// wildCardFarm.extendsType(new RabbitFarm<Rabbit>(new Rabbit()));
wildCardFarm.extendsType(new RabbitFarm<Bunny>(new Bunny()));
wildCardFarm.extendsType(new RabbitFarm<DrunkenBunny>(new DrunkenBunny()));
/* 3. <? super Bunny>인 경우 */
>>> Bunny이거나 Bunny의 부모클래스만 가능
wildCardFarm.superType(new RabbitFarm<Rabbit>(new Rabbit()));
wildCardFarm.superType(new RabbitFarm<Bunny>(new Bunny()));
// wildCardFarm.superType(new RabbitFarm<DrunkenBunny>(new DrunkenBunny()));
>>>