타입 매개변수 제한 1 - 시작
요구사항 : 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있다.
public class DogHospital {
private Dog animal;
public void set(Dog animal) {
this.animal = animal;
}
public void checkup() {
System.out.println("동물 이름 " + animal.getName());
System.out.println("동물 크기 " + animal.getSize());
animal.sound();
}
public Dog bigger(Dog target) {
return animal.getSize() > target.getSize() ? animal : target;
}
}
- 개 병원 내부은 Dog 타입을 가진다.
- checkup 개의 이름과 크기를 출력하고 sound() 메서드를 호출한다.
- bigger 다른 개와 크기를 비교한다. 둘 중에 큰 개를 반환한다.
package generic.test;
import generic.animal.Cat;
public class CatHospital {
private Cat animal;
public void set(Cat animal) {
this.animal = animal;
}
public void checkup() {
System.out.println("동물 이름 " + animal.getName());
System.out.println("동물 크기 " + animal.getSize());
animal.sound();
}
public Cat bigger(Cat target) {
return animal.getSize() > target.getSize() ? animal : target;
}
}
- 고양이 병원은 내부에 Cat 타입을 가진다.
- checkup() : 고양이의 이름과 크기를 출력하고 sound() 메서드를 호출한다.
- bigger(): 다른 고양이와 크기를 비교한다. 둘 중에 큰 고양이를 반환한다.
package generic.test;
import generic.animal.Cat;
import generic.animal.Dog;
public class AnimalHostpitalMainV0 {
public static void main(String[] args) {
DogHospital dogHospital = new DogHospital();
CatHospital catHospital = new CatHospital();
Dog dog = new Dog("멍멍이1", 100);
Cat cat = new Cat("냐옹이1", 300);
// 개병원
dogHospital.set(dog);
dogHospital.checkup();
// 고양이 병원
catHospital.set(cat);
catHospital.checkup();
// 문제 1: 개 병원에 고양이 전달
dogHospital.set(cat); // 다른 타입을 입력 : 컴파일 오류
// 문제 2: 개 타입 반화
dogHospital.set(dog);
Dog biggerDog = dogHospital.bigger(new Dog("멍멍이2", 200));
System.out.println("biggerDog = " + biggerDog);
}
}
요구사항을 잘 지키고 있다
요구사항 : 개병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있어야 한다.
여기서 개 병원과 고양이 병원을 각각 별도의 클래스로 만들었다.
각 클래스 별로 타입이 명확하기 때문에 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있다. 따라서 개병원에 고양이를 전달하면 컴파일오류가 발생한다.
그리고 개병원에서 bigger()로 다른 개를 비교하는 경우 더 큰 개를 Dog 타입으로 반환한다.
문제
코드 재사용 X: 개병원과 고양이 병원은 중복이 많아 보인다.
타입안정성 O : 타입 안정성이 명확하게 지켜진다.
타입 매개변수 제한 2 - 다형성 시도
Dog, Cat 은 Animal이라는 명확한 부모 타입이 있다. 다형성을 사용해서 중복을 제거해 보자.
package generic.test;
import generic.animal.Animal;
public class AnimalHostpictalV1 {
private Animal animal;
public void set(Animal animal){
this.animal = animal;
}
public void checkup() {
System.out.println("동물 이름 " + animal.getName());
System.out.println("동물 크기 " + animal.getSize());
animal.sound();
}
public Animal bigger(Animal target) {
return animal.getSize() > target.getSize() ? animal : target;
}
}
package generic.test;
import generic.animal.Cat;
import generic.animal.Dog;
public class AnimalHospitalMainV1 {
public static void main(String[] args) {
AnimalHostpictalV1 dogHospital = new AnimalHostpictalV1();
AnimalHostpictalV1 catHospital = new AnimalHostpictalV1();
Dog dog = new Dog("멍멍이1", 100);
Cat cat = new Cat("냐옹이1", 300);
// 개병원
dogHospital.set(dog);
dogHospital.checkup();
// 고양이 병원
catHospital.set(cat);
catHospital.checkup();
// 문제 1: 개 병원에 고양이 전달
dogHospital.set(cat); // 매개변수에 체크 실패 : 컴파일 오류가 발생하지 않음
// 문제 2: 개 타입 반화
dogHospital.set(dog);
Dog biggerDog = (Dog ) dogHospital.bigger(new Dog("멍멍이2", 200));
System.out.println("biggerDog = " + biggerDog);
}
}
문제
코드 재사용 O : 다형성을 통해 AnimalHostpictalV1 하나로 개와 고양이를 모두 처리한다.
타입 안정성 X :
- 개 병원에 고양이를 전달하는 문제가 발생한다.
- Animal 타입을 반환하기 때문에 다운캐스팅해야 한다.
- 실수로 고양이를 입력했는데, 개를 반환하는 상황이라면 캐스팅 예외가 발생한다.
타입 매개변수 제한 3 - 제네릭 도입과 실패
제네릭을 도입해 코드의 재사용을 늘리고, 타입 안정성 문제를 해결해 보자.
package generic.test;
import generic.animal.Animal;
public class AnimalHostpictalV2<T> {
private T animal;
public void set(T animal){
this.animal = animal;
}
public void checkup() {
// T의 타입을 메서드를 정의하는 시점에서 알 수 없다. Object의 기능만 사용
// 컴파일 오류
// System.out.println("동물 이름 " + animal.getName());
// System.out.println("동물 크기 " + animal.getSize());
// animal.sound();
}
public T bigger(T target) {
// 컴파일 오류
// return animal.getSize() > target.getSize() ? animal : target;
return null;
}
}
<T>를 사용해서 제네릭 타입을 선언했다.
제네릭 타입을 선언하면 자바 컴파일러 입장에서 T에 어떤 타입이 들어올지 예측할 수 없다. 우리는 Animal 타입 자식이 들어오기를 기대했지만, 여기 코드에는 Animal에 대한 정보가 없다. T에는 타입 인자로 Integer 가 들어올 수 있고, Dog 가 들어올 수도 있다. 물론 Object 가 들어올 수도 있다.
다양한 타입인자
AnimalHostpictalV2<Dog> dogHospital = new AnimalHostpictalV2<>();
AnimalHostpictalV2<Cat> catHospitalt = new AnimalHostpictalV2<>();
AnimalHostpictalV2<Integer> intergerHospital = new AnimalHostpictalV2<>();
AnimalHostpictalV2<Object> objectHospital = new AnimalHostpictalV2<>();
자바 컴파일러는 어떤 타입이 들어올지 알 수 없기 때문에 T를 어떤 타입이든 받을 수 있는 모든 객체의 최종 부모인 Object 타입으로 가정한다. 따라서 Object 가 제공하는 메서드만 호출할 수 있다.
원하는 기능을 사용하려면 Animal 타입이 제공하는 기능들이 필요한데, 이 기능을 모두 사용할 수 없다.
여기서 추가적으로 동물 병원에 Integer, Object 같은 동물과 전혀 관계없는 타입을 타입인자로 전달할 수 있다. 우리는 최소한 Animal이나 그 자식을 타입인자로 제한하고 싶다.
문제
제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다.
따라서 타입 매개변수를 어떤 타입이든 수용할 수 있는 Object로 가정하고 Object의 기능만 사용할 수 있다.
타입 매개변수를 Animal과 그 자식만 들어올 수 있게 제한한다면 어떨까?
package generic.test;
import generic.animal.Animal;
public class AnimalHostpictalV3<T extends Animal> {
private T animal;
public void set(T animal){
this.animal = animal;
}
public void checkup() {
System.out.println("동물 이름 " + animal.getName());
System.out.println("동물 크기 " + animal.getSize());
animal.sound();
}
public T bigger(T target) {
return animal.getSize() > target.getSize() ? animal : target;
}
}
package generic.test;
import generic.animal.Cat;
import generic.animal.Dog;
public class AnimalHospitalMainV3 {
public static void main(String[] args) {
AnimalHostpictalV3<Dog> dogHospital = new AnimalHostpictalV3<>();
AnimalHostpictalV3<Cat> catHospital = new AnimalHostpictalV3<>();
Dog dog = new Dog("멍멍이1", 100);
Cat cat = new Cat("냐옹이1", 300);
// 개병원
dogHospital.set(dog);
dogHospital.checkup();
// 고양이 병원
catHospital.set(cat);
catHospital.checkup();
// 문제 1: 개 병원에 고양이 전달
//dogHospital.set(cat); // 다른 타입 입력 : 컴파일 오류
// 문제 2: 개 타입 반화
dogHospital.set(dog);
Dog biggerDog = dogHospital.bigger(new Dog("멍멍이2", 200));
System.out.println("biggerDog = " + biggerDog);
}
}
여기서 핵심은 <T extends Animal>이다.
타입 매개변수 T를 Animal과 그 자식만 받을 수 있도록 제한을 두는 것이다.
즉 T의 상한이 Animal 이 되는 것이다.
이렇게 하면 타입인자로 들어올 수 있는 값이 Animal과 그 자식으로 제한된다.
AnimalHostpictalV3<Animal>
AnimalHostpictalV3<Dog>
AnimalHostpictalV3<Cat>
이제 자바 컴파일러는 T에 입력될 수 있는 값의 범위를 예측할 수 있다.
타입 매개변수 T 에 타입인자로 Animal , Dog, Cat 만 들어올 수 있다. 이를 모두 수용할 수 있는 Animal을 T 타입으로 가정해도 문제가 없다.
따라서 Animal 이 제공하고 getName(), getSize() 같은 기능을 사용할 수 있다.
타입 매개변수에 입력될 수 있는 상한을 지정해서 문제를 해결했다.
- AnimalHospitalV3 <Integer>와 같이 동물과 전혀 관계없는 타입 인자를 컴파일 시점에 막는다.
- 제네릭 클래스 안에서 Animal의 기능을 사용할 수 있다.
기존 문제 해결
타입 안정성 X 문제
- 개 병원에 고양이를 전달하는 문제 발생 ->해결
- Animal 타입을 반환하기 때문에 다운 캐스팅을 해야 한다. -> 해결
- 실수로 고양이를 입력했는데, 개를 반환하는 상황이라면 캐스팅 예외가 발생한다. -> 해결
제네릭 도입 문제
- 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다.
- 그리고 어떤 타입이든 수용할 수 있는 Object로 가정하고 Object 기능만 사용할 수 있다.
- 여기서는 Animal을 상한으로 두어서 Animal의 기능을 사용할 수 있다.
정리
제네릭에 타입 매개변수 상한을 이용해서 타입안정성을 지키면서 상위 타입의 원하는 기능까지 사용할 수 있었다.
덕분에 코드 재사용성과 타입 안정성 모두를 지킬 수 있었다.
제네릭 메서드
특정 메서드에 제네릭을 적용하는 제네릭 메서 드을 알아보자.
제네릭 타입과 제네릭 메서드는 둘 다 제네릭을 사용하긴 하지만 서로 다른 기능을 제공한다.
package generic.test;
public class GenericMethod {
public static Object objectMthod(Object object) {
System.out.println("object print = " + object);
return object;
}
public static <T> T genericMthod(T t) {
System.out.println("genericMthod = " + t);
return t;
}
public static <T extends Number> T numberMthod(T t) {
System.out.println("bound print = " + t);
return t;
}
}
package generic.test;
public class MethodMain1 {
public static void main(String[] args) {
Integer i = 10;
Object o = GenericMethod.objectMthod(i);
// 타입 인자 (Type Argument) 명시적 전달
System.out.println(" 명시적 타입 인자 전달");
Integer integer = GenericMethod.<Integer>genericMthod(i);
Integer integer1 = GenericMethod.<Integer>numberMthod(22);
System.out.println(" 타입 추론");
Double aDouble = GenericMethod.genericMthod(20.0);
String s = GenericMethod.genericMthod("test");
}
}
제네릭 타입
- 정의 :GenericClass <T>
- 타입 인자 전달 : 객체를 생성하는 시점
- 예) new GenericClass <String>
제네릭 메서드
- 정의 : <T> T genericMehod(T t)
- 타입 인자 전달 : 메서드를 호출하는 시점
- GenericMethod. <Integer> genericMethod(i);
제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용한다.
제네릭 메서드를 정의할 때 메서드의 반환 타입 왼쪽에 다이아몬드를 사용해서 <T>와 같이 타입 매개변수를 적어준다.
제네릭 메서드는 메서드를 호출하는 시점에 다이아몬드를 사용해서<Integer>와 같이 타입을 정하고 호출한다.
제네릭 메서드의 핵심은 메서드를 호출하는 시점에 타입인자를 전달해서 타입을 지정하는 것이다. 따라서 타입을 지정하면서 메서드를 호출한다.
인스턴스 메서드, static 메서드
제네릭 메서드는 인스턴스 메서드와 static 메서드에 모두 적용될 수 있다.
public static <V> V staticMethod(V t){ // static 메서드에 제네릭 메서드 도입
return t;
}
public <V> V instanceMethod(V t){ // 인스턴스 메서드에 제네릭 메서드 도입
return t;
}
참고
제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다. 제네릭 타입은 객체를 생성한느 시점에 타입이 정해진다.
그런데 static 메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문에 제네릭 타입과는 무관한다.
따라서 static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 한다.
T instanceMethod2(T t) { // 가능
return t;
}
static T staticMethod2(T t){ // 제네릭 타입의 T 사용 불가능
return t;
}
타입 매개변수 제한
제네릭 메서드도 제네릭 타입과 마찬가지로 타입 매개변수를 제한할 수 있다.
타입 매개변수를 Number로 제한했다. 따라서 Number와 그 자식(Integer, Double , Long과 같은 숫자 타입)만 받을 수 있다.
static <T extends Number> T numberMthod(T t) {}
GenericMethod.numberMthod("hello"); // 컴파일 오류 Number 의 자식만 입력 가능
제네릭 메서드 타입 추론
제네릭 메서드를 호출할 때 Integer와 같이 타입인자를 계속 전달하는 것은 불편하다.
Integer i = 10;
Integer result = GenericMethod.<Integer>genericMthod(i);
자바 컴파일러는 genericMethod에 전달되는 인자 i의 타입이 Integer라는 것을 알 수 있다.
또한 반환 타입이 Integer result라는 것도 알 수 있다. 이런 정보를 통해 자바 컴파일러는 타입인자를 추론할 수 있다.
System.out.println(" 타입 추론");
Integer result2 = GenericMethod.genericMthod(i);
Double aDouble = GenericMethod.genericMthod(20.0);
String s = GenericMethod.genericMthod("test");
이 경우 컴파일러가 타입을 추론해서 대신 처리하기 때문에 타입을 전달하지 않는 것처럼 보인다.
하지만 실제로는 타입인자가 전달된다.
제네릭 메서드 활용
제네릭 타입으로 만들었던 AnimalHospitalV3의 주요 기능을 제네릭 메서드로 다시 만들어 보자
public class AnimalMethod {
public static <T extends Animal> void checkup(T animal) {
System.out.println("동물 이름 " + animal.getName());
System.out.println("동물 크기 " + animal.getSize());
animal.sound();
}
public static <T extends Animal> T bigger(T t1, T t2) {
return t1.getSize() > t2.getSize() ? t1 : t2;
}
}
checkup(), bigger라는 두 개의 제네릭 메서드를 정의했다. 둘 다 Animal로 상한으로 제한한다.
public class MethodMain2 {
public static void main(String[] args) {
Dog dog = new Dog("멍멍이", 100);
Dog dog2 = new Dog("큰 멍멍이", 300);
Cat cat = new Cat("야옹~", 100);
AnimalMethod.<Dog>checkup(dog);
AnimalMethod.<Cat>checkup(cat);
Dog bigger = AnimalMethod.<Dog>bigger(dog, dog2);
System.out.println("bigger = " + bigger);
}
}
제네릭 타입과 제네릭 메서드의 우선순위
정적 메서드는 제네릭 메서드만 적용할 수 있지만, 인스턴스 메서드는 제네릭 타입도 제네릭 메서드로 둘 다 적용할 수 있다.
여기에 제네릭 타입과 제네릭 메서드의 타입 매개변수를 같은 이름으로 사용하면 어떻게 될까?
public class ComplexBox <T extends Animal>{
private T animal;
public void set(T animal) {
this.animal = animal;
}
public <T> T printAndReturn(T z) {
System.out.println("animal getClass= " + animal.getClass().getName());
System.out.println("z.getClass().getName() = " + z.getClass().getName());
return z;
}
}
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 cat1 = hospital.printAndReturn(cat);
System.out.println("cat1 = " + cat1);
}
}
제네릭 타입설정
class ComplexBox <T extends Animal>
제네릭 메서드 설정
public <T> T printAndReturn(T z)
제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가진다.
따라서 printAndReturn 은 제네릭 타입과는 무관하고 제네릭 메서드가 적용된다.
여기서 적용된 제네릭 메서드의 타입 매개변수 T는 상한이 없다. 따라서 Object로 취급된다.
Object로 취급되게 때문에 z.getName과 같이 Animal 이 존재하는 메서드를 호출할 수 없다.
실전에서는 둘의 이름이 겹치지 않게 하여 모호성을 없애자!
public class ComplexBox <T extends Animal>{
private T animal;
public void set(T animal) {
this.animal = animal;
}
public <Z> Z printAndReturn(Z z) {
...
}
}
와일드카드 1
제네릭 타입을 조금 더 편리하게 사용할 수 있는 와일드카드에 대해 알아보자!
컴퓨터 프로그래밍에서 * ,? 와 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다.
즉 여러 타입이 들어올 수 있다는 뜻이다.
public class Box <T>{
private T value;
public void set(T value){
this.value = value;
}
public T get(){
return value;
}
}
- 단순히 데이터를 넣고 반환할 수 있는 제네릭 타입을 하나 만들었다.
public class WildcardEx {
static <T> void printGenericV1(Box<T> box) {
T t = box.get();
System.out.println("box.get() = " + box.get());
}
static <T extends Animal> void printGenericV2(Box<T> box){
T t = box.get();
System.out.println("t.getName() = " + t.getName());
}
static <T extends Animal> T printAndReturnGenericV3(Box<T> box){
T t = box.get();
System.out.println("t.getName() = " + t.getName());
return t;
}
static Animal printAndReturnWildCard(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println(" 이름 = " + animal.getName());
return animal;
}
}
제네릭 메서드와 와일드카드를 비교할 수 있게 같은 기능을 각각 하나씩 배치해 두었다.
와일드카드는? 를 사용해서 정의하였다.
public class WhildCardMain1 {
public static void main(String[] args) {
Box<Object> objectBox = new Box<>();
Box<Dog> dogBox = new Box<>();
Box<Cat> catBox = new Box<>();
dogBox.set(new Dog("멍멍이", 100));
catBox.set(new Cat("냐옹이", 50));
WildcardEx wildcardEx = new WildcardEx();
wildcardEx.printGenericV1(objectBox);
wildcardEx.printGenericV1(dogBox);
wildcardEx.printGenericV1(catBox);
// wildcardEx.printGenericV2(objectBox); 컴파일 오류
// Animal 클래스의 getName 메서드 사용가능
wildcardEx.printGenericV2(dogBox);
wildcardEx.printGenericV2(catBox);
Dog dog = WildcardEx.printAndReturnGenericV3(dogBox);
System.out.println("dog = " + dog);
Cat cat = WildcardEx.printAndReturnGenericV3(catBox);
System.out.println("cat = " + cat);
}
}
참고!
와일드카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다. 와일드카드는 이미 만들어진 제네릭 타입을 활용할 때 사용한다.
// 이것은 제네릭 메서드이다
// Box<Dog> dogBox 를 전달한다. 타입 추론에 의해 T 가 dog 가 된다.
static <T> void printGenericV1(Box<T> box) {
System.out.println("box.get() = " + box.get());
}
// 이것은 제네릭 메서드가 아니다. 일반적인 메서드이다.
// Box<Dog> dogBox 를 전달한다. 와일드카드 ? 는 모든 타입을 받을 수 있다.
static void printWildcardV1(Box<?> box) {
System.out.println("box.get() = " + box.get());
}
- 두 메서드는 비슷한 기능을 하는 코드이다. 하나는 제네릭 메서드를 사용하고 하나는 일반적인 메서드에 와일드카드를 사용했다.
- 와일드카드는 제네릭 타입이나 제네릭 메서드를 정의할 때 사용하는 것이 아니다. Box <Dog>, Box <Cat>처럼 타입 인자가 정해진 제네릭 타입을 전달받아서 활용할 때 사용한다.
- 와일드카드인? 는 모든 타입을 다 받을 수 있다는 뜻이다
- 즉? == <? extends Object>
- 이렇게? 만 사용해서 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드카드라 한다.
- 여기에는 Box <Dog> dogBox, Box <Cat> catBox, Box <Object> objBox 가 모두 입력될 수 있다.
제네릭 메서드 실행 예시
// 1. 전달
WildcardEx.printGenericV1(dogBox);
// 2. 제네릭 메서드 타입 결정 dogBox 는 Box<Dog> 타입,타입 추론 - T 타입은 도그
static <T> void printGenericV1(Box<T> box) {
System.out.println("box.get() = " + box.get());
}
// 3. 타입 인자 결정
static <Dog> void printGenericV1(Box<Dog> box) {
System.out.println("box.get() = " + box.get());
}
// 4. 최종 실행 메서드
static void printGenericV1(Box<Dog> box) {
System.out.println("box.get() = " + box.get());
}
// 1.전달
WildcardEx.printWildcardV1(dogBox);
// 이것은 제네릭 메서드가 아니다. 일반적인 메서드이다
// 2. 최종 실행 메서드, 와일드카드 ? 은 모든 타입을 받을 수 있다.
static void printWildcardV1(Box<?> box) {
System.out.println("? = " + box.get());
}
제네릭 메서드 VS 와일드카드
- printGenericV1 제네릭 메서드를 보자. 제네릭 메서드에는 타입 매개변수가 존재한다. 그리고 특정 시점에 타입 매개변수에 타입 인자를 전달하여 타입을 결정해야 한다. 이런 과정은 매우 복잡하다.
- 반면에 printWildcardV1 메서드를 보자. 와일드카드는 일반적인 메서드에 사용할 수 있고, 단순히 매개변수로 제네릭 타입을 받을 수 있는 것뿐이다. 제네릭 메서드처럼 타입을 결정하거나 복잡하게 작동하지 않는다.
- 단순히 일반 메서드에 제네릭 타입을 받을 수 있는 매개변수가 하나 있는 것 뿐이다.
제네릭 타입이나 제네릭 메서드를 정의하는 게 꼭 필요한 상황이 아니라면, 더 단순한 와일드카드 사용을 권장한다.
static <T extends Animal> void printGenericV2(Box<T> box){
T t = box.get();
System.out.println("t.getName() = " + t.getName());
}
static void printAndReturnGenericV2(Box<? extends Animal> box){
Animal t = box.get();
System.out.println("t.getName() = " + t.getName());
}
- 제네릭 매서드와 마찬가지로 와일드카드에도 상한 제한을 둘 수 있다.
- 여기서는? extends Animal을 지정했다.
- Animal과 그 하위 타입만 입력받는다. 만약 다른 타입을 입력하면 컴파일 오류가 발생한다.
- box.get()을 통해서 꺼낼 수 있는 타입의 최대 부모는 Animal이다. 따라서 Animal 타입으로 조회할 수 있다.
- 결과적으로 Animal 타입의 기능을 호출할 수 있다.
타입 매개변수가 꼭 필요한 경우
와일드카드는 제네릭을 정의할 때 사용하는 것이 아니다. Box <Dog>, Box <Cat>처럼 타입 인자가 전달된 제네릭 타입을 활용할 때 사용한다. 따라서 다음과 같은 경우에는 제네릭 타입이나 제네릭 메서드를 사용해야 문제를 해결할 수 있다.
static <T extends Animal> T printAndReturnGenericV3(Box<T> box){
T t = box.get();
System.out.println("t.getName() = " + t.getName());
return t;
}
static Animal printAndReturnWildCard(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println(" 이름 = " + animal.getName());
return animal;
}
printAndReturnGenericV3 은 다음과 같이 전달한 타입을 명확하게 반환할 수 있다.
Dog dog = WildcardEx.printAndReturnGenericV3(dogBox);
반면에 printAndReturnWildCard의 경우 전달한 타입을 명확하게 반환할 수 없다. 여기서 Animal 타입으로 반환한다.
Animal animal = WildcardEx.printAndReturnWildCard(dogBox);
메서드의 타입들을 특정 시점에 변경하려면 제네릭 타입이나, 제네릭 메서드를 사용해야 한다.
와일드카드는 이미 만들어진 제네릭 타입을 전달받아서 활용할 때 사용한다. 따라서 메서드의 타입들을 타입인자를 통해 변경할 수 없다. 즉 일반적인 메서드를 사용한다고 생각하면 된다.
제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이면 <T>를 사용하고 그렇지 않으면 와일드카드 사용을 권장한다.
하한 와일드카드
와일드카드는 상한 뿐만 아니라 하한도 지정할 수 있다.
public class WildCardMain2 {
public static void main(String[] args) {
Box<Object> objectBox = new Box<>();
Box<Animal> animalBox = new Box<>();
Box<Dog> dogBox = new Box<>();
Box<Cat> catBox = new Box<>();
writeBox(objectBox);
writeBox(animalBox);
// writeBox(dogBox); // 하한이 Animal
// writeBox(catBox); // 하한이 Animal
Animal animal = animalBox.get();
System.out.println("animal = " + animal);
}
static void writeBox(Box<? super Animal> box){
box.set(new Dog("멍멍이!", 100));
}
}
실행결과
animal = Animal{name='멍멍이!', size=100}
Box<? super Animal> box
이 코드는? 가 Animal 타입을 포함한 Animal 타입의 상위 타입만 입력받을 수 있다는 뜻이다.
Box<Object> objectBox = new Box<>(); // 허용
Box<Animal> animalBox = new Box<>(); // 허용
Box<Dog> dogBox = new Box<>(); // 불가
Box<Cat> catBox = new Box<>(); // 불가
하한을 Animal로 제한했기 때문에 Animal 타입의 하위 타입인 Box <Dog>을 전달할 수 없다.
타입 이레이저 (eraser - 지우개)
제네릭은 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다.
제네릭에 사용한 타입 매개변수가 모두 사라지는 것이다. 쉽게 이야기해서 컴파일 전인. java 에는 제네릭의 타입매개변수가 존재하지만, 컴파일 이후인 자바 바이트코드. class 에는 타입 매개변수가 존재하지 않는다.
어떻게 변하는지 코드로 설명한다. 100% 정확한 코드는 아니고 대략 이런 방식으로 동작한다고 이해하자.
public class Box<T> {
private T value;
public Box() {
}
public void set(T value) {
this.value = value;
}
public T get() {
return this.value;
}
}
제네릭 타입을 선언했다.
제네릭 타입에 Integer 타입 인자 전달
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(10);
Integer integer = integerBox.get();
이렇게 자바 컴파일러는 컴파일 시점에 타입 매개변수와 타입인자를 포함한 제네릭 정보를 활용해서 new Box <Integer> integerBox에 해대 다음과 같이 이해한다.
public class Box <Integer>{
private Integer value;
public void set(Integer value){
this.value = value;
}
public Integer get(){
return value;
}
}
컴파일이 모두 끝나면 자바는 제네릭과 관련된 정보를 삭제한다. 이때. class에 생성된 정보는 다음과 같다,
public class Box {
private Object value;
public void set(Object value){
this.value = value;
}
public Object get(){
return value;
}
}
상한 제한 없이 선언한 타입 매개변수 T는 Object로 변환된다.
Box integerBox = new Box<>();
integerBox.set(10);
Integer integer = (Integer)integerBox.get(); // 컴파일러가 캐스팅 추가
- 값을 반환받는 부분을 Object로 받으면 안 된다. 자바 컴파일러는 제네릭에서 타입인자로 지정한 Integer로 캐스팅하는 코드를 추가해 준다.
- 이렇게 추가된 코드는 자바 컴파일러가 이미 검증하고 추가했기 때문에 문제가 발생하지 않는다.
타입 매개변수 제한의 경우
다음과 같이 타입 매개변수를 제한하면 제한한 타입으로 코드를 변경한다.
컴파일 전
AnimalHospitalV3.java
public class AnimalHostpictalV3<T extends Animal> {
private T animal;
public void set(T animal){
this.animal = animal;
}
public void checkup() {
System.out.println("동물 이름 " + animal.getName());
System.out.println("동물 크기 " + animal.getSize());
animal.sound();
}
public T bigger(T target) {
return animal.getSize() > target.getSize() ? animal : target;
}
}
// 사용코드 예시
AnimalHostpictalV3<Dog> dogHospital = new AnimalHostpictalV3<>();
...
Dog biggerDog = dogHospital.bigger(new Dog("멍멍이2", 200));
컴파일 후
AnimalHospitalV3.class
public class AnimalHostpictalV3 {
private Animal animal;
public void set(Animal animal){
this.animal = animal;
}
public void checkup() {
System.out.println("동물 이름 " + animal.getName());
System.out.println("동물 크기 " + animal.getSize());
animal.sound();
}
public Animal bigger(Animal target) {
return animal.getSize() > target.getSize() ? animal : target;
}
}
- T 타입 정보가 제거되어도 상한으로 지정한 Animal 타입으로 대체되기 때문에 Animal 타입의 메서드를 사용하는 데는 아무런 문제가 없다.
AnimalHostpictalV3<Dog> dogHospital = new AnimalHostpictalV3();
Dog dog = new Dog("멍멍이1", 100);
dogHospital.set(dog);
Dog biggerDog = (Dog)dogHospital.bigger(new Dog("멍멍이2", 200));
반환받는 부분을 Animal로 받으면 안 되기 때문에 자바 컴파일러가 타입 인자로 지정한 Dog로 캐스팅하는 코드를 넣어준다.
자바의 제네릭은 단순하게 생각하면 개발자가 직접 캐스팅하는 코드를 컴파일러가 대신 처리해 주는 것이다.
자바는 컴파일 시점에 제네릭을 사용한 코드에 문제가 없는지 완벽하게 검증하기 때문에 자바 컴파일러가 추가하는 다운캐스팅에는 문제가 발생하지 않는다.
자바의 제네릭 타입은 컴파일 시점에만 존재하고, 런타임시에는 제네릭 정보가 지워지는데, 이것을 타입 이레이저라 한다.
타입 이레이저 방식의 한계
컴파일 이후에는 제네릭의 타입 정보가 존재하지 않는다. class로 자바를 실행하는 런타임에는 우리가 지정한 Box <Integer>, Box <String>의 타입정보가 모두 제거된다,
따라서 런타임에 타입을 활용하는 다음과 같은 코드는 작성할 수 없다.
소스 코드
public class EraserBox<T> {
public boolean instanceCheck(Object param){
return param instanceof T; // 오류
}
public T create(){
return new T(); // 오류
}
}
런타임
public class EraserBox {
public boolean instanceCheck(Object param){
return param instanceof Object; // 오류
}
public Object create(){
return new Object(); // 오류
}
}
- 여기서 T는 런타임에 모두 Object 가 되어버린다.
- instanceof는 항상 Object와 비교하게 되어 항상 참이 반환된다. 자바는 이런 문제 때문에 타입매개변수에 instanceof을
- new T는 항상 new Object 가 되어버린다. 개발자가 의도한 것과 다르다. 따라서 타입 매개변수는 new를 허용하지 않는다.