public class AnimalHospitalV3<T> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public void checkup() {
// T의 타입을 메서드를 정의하는 시점에는 알 수 없다.
System.out.println("동물 이름: " + animal.getName());
System.out.println("동물 크기: " + animal.getSize());
animal.sound();
}
public T bigger(T target) {
return animal.getSize() > target.getSize() ? animal : target;
// 비교로직이 true일 경우 animal을 반환, false일 경우 target을 반환
}
}
위와 같은 동물병원 제네릭 클래스를 만들려고 했다. 이전 포스팅에서 느낄 수 있듯이 만약 동물병원의 set()의 매개변수를 Dog이나 Cat으로 제한한다면, type마다 동물병원 클래스를 만들어줘야 할 것이다. 이를 방지하고자 Dog,Cat의 부모인 Animal로 받는다면, sound()메서드를 사용하지 못하는 것, set에 Cat이 들어왔는데 bigger에서 리턴된 Animal을 Dog로 받도록 코드를 짜도 컴파일 오류를 내지 않는다.
그렇기에 제네릭을 통해 이전처럼 극복하고자 했으나 제네릭 타입변수의 본질은 "타입이 객체가 생성될 때 결정된다"라는 것이다. 그러므로 결정되기 이전에 설계 단계에서 .getName, .getSize등의 메서드를 선언할 수 없다. T에는 String, Integer도 들어올 수 있기 때문이다. 이들은 해당 메서드를 가지고 있지 않다. 그렇기에 위의 캡쳐처럼 컴파일 오류가 뜬다.
실제로 T만 적혀있다면 가능한 메서드는 Object가 가진 메서드만 가능하다. 모든 타입의 부모 타입은 Object이기 때문이다.
만약 들어올 타입의 최상위를 Animal로, 그리고 Animal의 자식들까지만 들어올 수 있게 막는다면 우리는 getName과 getSize등을 사용할 수 있을 것이다.
이것을 타입 매개변수 부분에 extends 선언을 하여 해결할 수 있다.
package generic.ex3;
import generic.animal.Animal;
public class AnimalHospitalV3<T extends Animal> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public void checkup() {
// T의 타입을 메서드를 정의하는 시점에는 알 수 없다.
System.out.println("동물 이름: " + animal.getName());
System.out.println("동물 크기: " + animal.getSize());
animal.sound();
}
public T bigger(T target) {
return animal.getSize() > target.getSize() ? animal : target;
// 비교로직이 true일 경우 animal을 반환, false일 경우 target을 반환
}
}
T extends Animal은 타입변수 T는 Animal이하의 타입만 올 수 있다는 것이다.
extends(확장...)이라는 영어단어가 사용된 이유는 다음과 같이 해석하면 된다.
Animal타입 으로 확장성을 가진 무언가만 T가 될 수 있다.
즉 Animal과 Animal의 하위 자식들로만 T의 확장을 규정하는 것이다.
제네릭 메서드란 메서드 단위에서만 적용되는 제네릭이다.
package generic.ex4;
public class GenericMethod {
public static Object objMethod(Object obj) {
System.out.println("Object print: " + obj);
return obj;
}
public static <T> T genericMethod(T obj) {
System.out.println("Object print: " + obj);
return obj;
}
public static <T extends Number> T numberMethod(T obj) {
System.out.println("bound print: " + obj);
return obj;
}
public <T> T instanceMethod(T obj) {
return obj;
}
}
제네릭 선언은 다이아몬드<>로 이루어진다. 위 코드의 클래스는 일반적인 클래스이지만 메서드에는 다이아몬드 선언이 들어간다. 클래스때와 동일하게 메서드 내부에서 T와 관련된 것들이 메서드 호출이후 타입이 결정된다.
만약 클래스가 제네릭 클래스이며 주어지는 타입변수가 인스턴스 메서드에도 활용된다고 가정할 때 이 타입변수를 스태틱 메서드에도 줄 수 있을까? 결론적으로 그렇지 않다. 제네릭 클래스의 타입은 객체 생성시 결정된다. 그러므로 스태틱에는 클래스의 타입 매개변수를 사용할 수 없다. 스태틱은 객체 생성과 관계가 없기 때문이다.
제네릭 클래스처럼 메서드도 타입 추론을 지원한다.
System.out.println("명시적 타입 인자 전달");
Integer integer = GenericMethod.<Integer>genericMethod(i);
System.out.println("integer = " + integer);
System.out.println("타입 추론");
Integer integer = GenericMethod.genericMethod(i);
System.out.println("integer = " + integer);
위 코드 블럭의 위 코드와 아래 코드는 모두 같은 기능이다. 만약 GenericMethod.genericMethod()가 제네릭 메서드라면 들어오는 i의 타입으로 타입추론이 진행된다.
public class ComplexBox <T extends Animal> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public <T> T printAndReturn(T t) {
System.out.println("animal.className: " + animal.getClass().getName() );
System.out.println("t.className: " + t.getClass().getName());
return t;
}
}
다이아몬드가 두개인 상황이다. 제네릭클래스 안에 제네릭메서드가 존재한다. 이럴때 메서드의 T는 당연하게도 가장 가까운 다이아몬드에 매핑된다.
하지만 이렇게 헷갈리게 코딩해서는 안된다. 원리를 알기 위한 예제일 뿐, 위와 같이 제네릭 메서드와 제네릭 타입을 동일하게 해서 굳이구태여 판단에 복잡성을 줄 필요가 없다.
와일드카드(wildcard)는 문자 그대로 '무엇이든 될 수 있는 카드'를 의미하는데, 제네릭 타입을 사용할 때 특정 타입에 대해 제약을 덜어주어 유연성을 높이는 역할을 한다.
public class WildCardEx {
static <T> void printGenericV1(Box<T> box) {
System.out.println("T = " + box.get());
}
static <T extends Animal> void printGenericV2(Box<T> box) {
T t = box.get();
System.out.println("이름 = " + t.getName());
}
//Box<Dog> Box<Cat> Box<Object>
static void printWildCardV1(Box<?> box) {
System.out.println("? = " + box.get());
}
와일드카드는 제네릭이랑 비슷하게 사용되지만 다르다. <?> ?를 통해 제네릭처럼 변수화될 수 있다. 일반적인 제네릭 선언의 경우 <T>에 영향받는 모든 T의 타입을 결정해준다. 그러나 와일드카드 선언은 와일드카드가 사용된 공간에서만 타입의 범위화를 해줄 뿐이다. 내부에 리턴을 <?>에 맞게 동적으로 설계하거나 내부의 무언가를 ?에 맞게 동적타입화 할 수 없다. 와일드카드는 기존에 하나의 타입만 입력으로 설정할 수 있던 문제를 제네릭을 활용하여 범위화할 수있다는 것이고 와일드카드로 만들어진 메서드는 제네릭메서드가 아니라 일반메서드일 뿐이다.
extends, super를 통해 상,하한을 설정할 수 있다. super의 경우
<Transp super Car>는 Car의 super방향으로 그 이상만 가능하다는 것이다. 즉 Car를 포함한 부모들을 가져올 수 있고 자식들은 가져올 수 없다. extends랑 반대된다고 보면 된다.
제네릭은 실제로 컴파일단계 이후 타입이 지워진다. JVM은 실행시점에 컴파일 단계에서 제네릭을 해석하여 <T extends A>에 적힌 최상위 부모(ex.A)로 타입을 고정하고 다운캐스팅하여 적용한다. 제네릭은 자바의 초창기부터 있던 기능이 아니기 때문에 자바진영은 자바의 기존에 있던 기능들로 제네릭을 구현한 것이다.
그렇기에 타입변수로 하여금 클래스를 생성하거나 타입을 클래스처럼 보아 활용할 수 없다.
실무에서는 직접 제네릭을 사용하여 무언가를 설계하는 일은 드물며, 이미 제네릭을 통해 만들어진 프레임워크나 라이브러리를 가져다 사용하는 경우가 많다. 그러므로 이미 만들어진 코드의 제네릭을 읽고 이해하는 정도면 충분하다. 직접 사용하더라도 단순하게 사용하는 경우가 많다.
실제로 와일드카드의 존재이유가 현재단계에서는 깊숙히 와닿지 않지만 공변, 반공변등의 개념을 알면 와일드카드의 존재필요성을 더 잘 느낄 수 있으나 프레임워크등을 자신이 설계해야하는 상황이 아니라면 이러한 개념들을 꼭 이해할 필요가 없으니, 나중에 필요시 공부하도록 하자.