제네릭(Generic) 타입 (+ 와일드카드, 이레이저)
※ 제네릭의 핵심 : 사용할 타입을 미리 결정하지 않는다. 클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 결정하는 것이 아니라 실제 사용하는 생성 시점에 타입을 결정한다.
1. 매서드와 제네릭 타입의 매개변수 개념
1) 메서드 :
- 메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루는 것
- 메서드는 매개변수에 인자를 전달해서 사용할 값을 결정한다.
2) 제네릭타입 :
- 제네릭 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것
- 제네릭 클래스는 타입 매개변수에 타입 인자를 전달해서 사용할 타입을 결정한다.
public static void main(String[] args) {
GenericBox<Integer> integerBox = new GenericBox<>(); // Integer : 타입 인자
public class GenericBox<T> {
private T value; // T : 타입 매개변수
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
** 제네릭 클래스에서 타입 매개변수 <> 선언 시 아래와 같은 관례 존재
2. 타입 매개변수 제한
1) 타입 매개변수를 Object 객체 타입으로 선언 하게 되면 아래와 같이 사용할 수 있는 메서드가 제한 된다.
public class AnimalHospitalV2<T> {
private T animal; // 매개변수를 object 타입으로 선언 시
public void set(T animal) {
this.animal = animal;
}
public void checkup() {
// T의 타입을 메서드를 정의하는 시점에는 알 수 없다. Object 기능만 사용한다. (toString(), hasCode() 등)
animal.equals(null);
animal.toString();
// System.out.println("동물 이름 : " + animal.getName()); // getName() 메서드를 사용 할 수 없다. object 타입이기 때문
// System.out.println("동물 크기 : " + animal.getSize()); // getSize() 메서드를 사용 할 수 없다. object 타입이기 때문
// animal.sound();
}
public class AnimalHospitalV3<T extends Animal> {
}
2) 타입 매개변수 제한 시
public class AnimalHospitalV3<T extends Animal> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public void checkup() {
// <T> 가 Animal 를 상속 받았기 때문에 Animal 과 Animal 의 서브 클래스 기능들을 사용 할 수 있다.
System.out.println("동물 이름 : " + animal.getName());
System.out.println("동물 크기 : " + animal.getSize());
animal.sound();
}
- 자바 컴파일러는 T 가 Animal 을 상속 받았기 때문에 T에 입력될 수 있는 값의 범위를 예측 할 수 있다.
- 타입 매개변수 T 에는 타입 인자로 Animal, Dog, Cat 만 들어올 수 있다. 따라서 이를 모두 수용할 수 있는 Animal 을 T의 타입으로 가정해도 문제가 없어진다.
- 따라서 getName() 과 같은 기능을 사용 가능해진다.
추가적으로 당연히 다른 타입이 들어 올 수 없다. (Animal 과 관련된 객체만 들어 올 수 있다.)
public static void main(String[] args) {
AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3<>();
AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3<>();
// AnimalHospitalV3<Integer> integer = new AnimalHospitalV3<Integer>(); 다른 타입이 들어 올 수 없다.
** 제네릭 메서드 & 메서드 타입 제한
1) 제네릭 메서드 선언
// 제네릭 타입 메서드 선언 <T>
public static <T> T genericMethod(T t) {
System.out.println("Generic print : " + t);
return t;
}
- 제네릭 메서드를 선언하면 해당 메서드 호출 시 타입을 결정 할 수 있다.
- 예를 들어, 메인 메서드에서 genericMethod() 호출을 할 때 아래 코드와 같이 타입을 Integer 또는 String 으로 결정이 가능 하다. (제약 조건 없음)
Integer result = GenericMethod.<Integer>genericMethod(i); // 메서드 호출 시점에, Integer 타입으로 변경해서 i 출력
Integer integerValue = GenericMethod.<Integer>numberMethod(10);
Double doubleValue = GenericMethod.<Double>numberMethod(20.0);
2) 메서드 타입 제한
- 제네릭 메서드를 생성시점에 타입을 제한하여 사용할 수 있다.
- 아래와 같이 제네릭 타입에 extends Number 를 하게 되면 외부에서 해당 메서드를 호출 할 때 Number 객체와 관련된 int, double 등밖에 사용 할 수 없다 (String 사용 불가)
// 제네릭 메서드 타입 제한 <T extend Number>
public static <T extends Number> T numberMethod(T t) {
System.out.println("Bound print : " + t);
return t;
}
※ 제네릭 메서드 활용
public static <T extends Animal> void checkup(T t) {
// <T> 가 Animal 를 상속 받았기 때문에 Animal 과 Animal 의 서브 클래스 기능들을 사용 할 수 있다.
System.out.println("동물 이름 : " + t.getName());
System.out.println("동물 크기 : " + t.getSize());
t.sound();
}
- 아래 코드에서 매개변수 타입을 T 를 Animal 로 정의한 클래스를 생성
- 별도의 pringAndReturn() 이라는 메서드를 Z 타입으로 생성하였고, animal.class 관련 기능과 z.class 관련 기능을
추가하였다.
import generic.animal.Animal;
public class ComplexBox <T extends Animal> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public <Z> Z printAndReturn(Z z) {
System.out.println("animal.className : " + animal.getClass().getName());
System.out.println("z.className : " + z.getClass().getName());
return z;
}
}
T 타입과 Z 타입을 각각 호출 하였을 때 어떻게 활용되는지 아래 코드를 보면 확인할 수 있다.
import generic.animal.Cat;
import generic.animal.Dog;
public class MethodMain3 {
public static void main(String[] args) {
Dog dog = new Dog("멍멍이", 100);
Cat cat = new Cat("야옹이", 50);
ComplexBox<Dog> hospital = new ComplexBox<>();
hospital.set(dog);
Cat returnCat = hospital.printAndReturn(cat);
System.out.println("returnCat = " + returnCat);
}
}
Step 1. 첫번째로 ComplexBox<> 의 타입을 Dog 제네릭 타입을 받는 hospital 객체를 생성 하였다. 그리고 hopital 객체의 메서드 set(dog) 를 호출 하였고, 현재 T(animal) 매개타입은 Dog 가 된다.
Step 2. 두번쨰로 해당 hospital 을 Cat 으로 재정의하고 printAndReturn 메서드를 호출 함과 동시에 returnCat 이라는 인스턴스로 재정의 하였다.
※ 출력 결과
- T 타입은 Step1 에서 Dog로 선언 하였기 때문에 클래스 이름이 Dog 으로 출력 되는 것을 확인 할 수 있다.
- Z 타입은 printAndReturn 메서드를 호출함과 동시에 매개타입은 Cat 으로 선언 하였기 때문에 Cat 으로 출력 된다.
그렇게 때문에 returnCat 이라는 인스턴스는 Cat 의 파라미터를 담을 수 밖에 없게 된다. (Dog 불가)
※ 정리 :
1) 정적 메서드는 메서드만 적용할 수 있지만, 인스턴스 메서드는 제네릭 타입도 제네릭 메서드도 둘 다 적용할 수 있다.
2) 제네릭 타입과 제네릭 메서드의 우선 순위를 살펴보면 T를 Dog 타입으로 정의하였지만 별도의 메서드를 Cat 타입으로 정의 하였을 때, 해당 메서드 인스턴스로 Dog 타입이 들어갈 수 없는 것을 알 수 있다. 결과 적으로 제네릭 메서드가 제네릭 타입보다 우선순위를 갖는 것을 확인 할 수 있다.
3) 제네릭 메소드와 타입 추론 : Java 7 이후부터는 제네릭 메소드를 사용할 때 타입 인수를 생략하고 컴파일러가 컨텍스트로부터 타입을 추론할 수 있다. 이는 코드를 간결하게 작성할 수 있게 해준다.
3. 와일드 카드 (제네릭 타입을 편리 하게 사용)
- 와일드 카드의 뜻은 컴퓨터 프로그래밍에서 *, ? 와 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다.
(여러 타입이 들어올 수 있다는 뜻)
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// 와일드카드 메서드 생성 <?> : 어떤 것도 다 들어올 수 있다는 뜻 (Bob<Dog>, Box<Cat>, Box<Object>)
static void printWildcardV1(Box<?> box) {
System.out.println("? = " + box.get());
}
아래 각각 제네릭타입과 와일드카드 메서드를 생성 하였다.
// 1. 제네릭타입 메서드 생성
static <T extends Animal> void printGenericV2(Box<T> box) {
T t = box.get();
System.out.println("이름 = " + t.getName());
}
// 2. 와일드카드 메서드 생성 <? extends Animal> 제한
static void printWildcardV2(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println("이름 = " + animal.getName());
}
- 와일드카드의 메서드를 생성하는 방법은 <?> 를 사용하여 생성하고, 호출 시 제네릭타입과 동일한 기능할 가지게 된다.
- 그러나, 와일드카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다. 와일드카드는 이미 만들어진 제네릭 타입을 활용할 때 사용한다.
- 위의 내용과 같이 제네릭 타입 메서드와 와일드카드 메서드는 서로 비슷한 부분이 많지만 제네릭 타입을 반드시 사용해야 하는 경우가 있다.
** 타입 매개변수가 꼭 필요한 경우
- 메서드 타입들을 특정 시점에 변경하려면 제네릭 타입 or 제네릭 메서드를 사용해야 한다.
- 와일드카드는 이미 만들어진 제네릭 타입을 전달 받아 활용할 때 사용한다.
※ 정리 : 제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이면 <T> 를 사용하고, 그렇지 않은 상황이면 와일드 카드 사용 권장.
추가 : 와일드카드 상한/하한 지정 가능 : <? super Animal>
public class WildcardMain2 {
public static void main(String[] args) {
Box<Object> objBox = new Box<Object>();
Box<Animal> animalBox = new Box<>();
Box<Dog> dogBox = new Box<Dog>();
Box<Cat> catBox = new Box<Cat>();
// Animal 포함 상위 타입 전달 가능
writeBox(objBox);
writeBox(animalBox);
writeBox(dogBox); // 컴파일 오류 Dog < Animal
writeBox(catBox); // 컴파일 오류 Cat < Animal
}
// <? 가 Animal 보다 같거나 위에 있어야 호출 가능>
static void writeBox(Box<? super Animal> box) {
box.set(new Dog("멍멍이", 100));
}
}
4. 타입 이레이저(eraser)
- 자바의 제네릭 타입은 컴파일 시점에만 존재하고, 런타임 시에는 제네릭 정보가 지워지는 것을 타입 이레이저라고 한다.
* 이레이저 방식의 한계
- 컴파일 이후에는 제네릭의 타입정보가 존재하지 않는다. .class로 자바를 실행하는 런타임에는 우리가 지정한 Box<Integer>, Box<String> 의 타입 정보가 모두 제거된다. (컴파일 후, 모두 Box<Object> 로 변경한다)
public class EraserBox<T> {
public boolean instanceCheck(Object param) {
// return param instanceof T; // 오류
return false;
}
public void create() {
// return new T(); // 오류
// return null; // 오류
}
}