요구사항 : 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있어야 한다.
package generic.ex3;
import generic.animal.Cat;
public class CatHostpital {
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;
}
}
package generic.ex3;
import generic.animal.Dog;
public class DogHostpital {
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;
}
}
package generic.ex3;
import generic.animal.Cat;
import generic.animal.Dog;
public class AnimalHotpitalMainV0 {
public static void main(String[] args) {
DogHostpital dogHostpital = new DogHostpital();
CatHostpital catHostpital = new CatHostpital();
Dog dog = new Dog("멍멍이1", 100);
Cat cat = new Cat("냐옹이1", 300);
// 개 병원
dogHostpital.set(dog);
dogHostpital.checkUp();
// 고양이 병원
catHostpital.set(cat);
catHostpital.checkUp();
// // 문제1: 개 병원에 고양이 전달
// dogHostpital.set(cat); // 다른 타입 입력 : 컴파일 오류
// 문제2: 개 타입 반환
dogHostpital.set(dog);
Dog biggerDog = dogHostpital.bigger(new Dog("멍멍이2", 200));
System.out.println("biggerDog = " + biggerDog);
}
}
문제점
코드 재사용 X : 개 병원과 고양이 병원은 중복이 많아 보인다.
타입 안정성 O : 타입 안정성이 명확하게 지켜진다.
타입 매개변수 제한2 - 다형성 시도
package generic.ex3;
import generic.animal.Animal;
public class AnimalHospitalV1 {
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.ex3;
import generic.animal.Cat;
import generic.animal.Dog;
public class AnimalHospitalMainV1 {
public static void main(String[] args) {
AnimalHospitalV1 dogHostpital = new AnimalHospitalV1();
AnimalHospitalV1 catHostpital = new AnimalHospitalV1();
Dog dog = new Dog("멍멍이1", 100);
Cat cat = new Cat("냐옹이1", 300);
// 개 병원
dogHostpital.set(dog);
dogHostpital.checkUp();
// 고양이 병원
catHostpital.set(cat);
catHostpital.checkUp();
// 문제1: 개 병원에 고양이 전달
dogHostpital.set(cat); // 매개변수 체크 실패 : 컴파일 오류 발생 X
// 문제2: 동물 타입 반환
dogHostpital.set(dog);
Dog biggerDog = (Dog) dogHostpital.bigger(new Dog("멍멍이2", 200)); // 다운캐스팅 필요 ..
System.out.println("biggerAnimal = " + biggerDog);
}
}
문제점
코드 재사용 O : 다형성을 통해서 AnimalHospitalV1 하나로 개와 고양이를 모두 처리한다.
타입 안정성 X
개 병원에 고양이를 전달하는 문제가 발생한다.
Animal 타입을 반환하기 때문에, 다운 캐스팅을 해야 한다.
실수로 고양이를 입력했는데, 개를 반환하는 상황이라면 캐스팅 예외가 발생한다.
타입 매개변수 제한3 - 제네릭 도입과 실패
제네릭 타입을 선언하면 자바 컴파일러 입장에서 T 에 어떤 값이 들어올지 예측할 수 없다.
우리는 Animal 타입의 자식이 들어오기를 기대했지만, 여기 코드 어디에도 Animal 에 대한 정보는 없다.
T 에는 타입 인자로 Integer, Dog, Object 등.. 어떤 타입이 들어와도 이상하지 않다.
package generic.ex3;
public class AnimalHospitalV2<T> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public void checkUp() {
// T 의 타입을 메서드를 정의하는 시점에는 알 수 없다. Object 기능만 사용 가능
// 때문에, Animal 객체의 기능을 사용하려 하면 컴파일 오류 발생
// 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;
}
}
package generic.ex3;
import generic.animal.Cat;
import generic.animal.Dog;
public class AnimalHospitalMainV2 {
public static void main(String[] args) {
AnimalHospitalV2<Dog> dogHostpital = new AnimalHospitalV2<>();
AnimalHospitalV2<Cat> catHostpital = new AnimalHospitalV2<>();
AnimalHospitalV2<Integer> integerHostpital = new AnimalHospitalV2<>(); // ? (의도와 맞는가?)
AnimalHospitalV2<String> stringHostpital = new AnimalHospitalV2<>(); // ? (의도와 맞는가?)
}
}
문제점
제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다.
따라서 타입 매개변수를 어떤 타입이든 수용할 수 있는 Object 라고 가정하고, Object 의 기능만 사용할 수 있다.
타입 매개변수 제한4 - 타입 매개변수 제한
여기서 핵심은 <T extends Animal> 이다.
타입 매개변수 T 를 Animal 과 그 자식만 받을 수 있도록 제한을 두는 것이다. 즉 T 의 상한이 Animal 이 되는 것이다.
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() {
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.ex3;
import generic.animal.Cat;
import generic.animal.Dog;
public class AnimalHospitalMainV3 {
public static void main(String[] args) {
AnimalHospitalV3<Dog> dogHostpital = new AnimalHospitalV3<>();
AnimalHospitalV3<Cat> catHostpital = new AnimalHospitalV3<>();
// AnimalHospitalV3<Integer> integerHostpital = new AnimalHospitalV3<>(); // 컴파일 오류
// AnimalHospitalV3<String> stringHostpital = new AnimalHospitalV3<>(); // 컴파일 오류
Dog dog = new Dog("멍멍이1", 100);
Cat cat = new Cat("냐옹이1", 300);
// 개 병원
dogHostpital.set(dog);
dogHostpital.checkUp();
// 고양이 병원
catHostpital.set(cat);
catHostpital.checkUp();
// // 문제1: 개 병원에 고양이 전달
// dogHostpital.set(cat); // 타입 체크 : 컴파일 오류 발생
// 문제2: 동물 타입 반환
dogHostpital.set(dog);
Dog biggerDog = dogHostpital.bigger(new Dog("멍멍이2", 200)); // 다운캐스팅 불필요!
System.out.println("biggerAnimal = " + biggerDog);
}
}
기존 문제 해결
타입 안정성 X 문제
개 병원에 고양이를 전달하는 문제가 발생한다. -> 해결
Animal 타입을 반환하기 때문에 다운 캐스팅을 해야 한다. -> 해결
실수로 고양이를 입력했는데, 개를 반환하는 상황이라면 캐스팅 예외가 발생한다. -> 해결
제네릭 도입 문제
제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다. -> 해결
그리고 어떤 타입이든 수용할 수 있는 Object 로 가정하고, Object 의 기능만 사용할 수 있다. -> 해결
여기서는 Animal 을 상한으로 두어서 Animal 의 기능을 사용할 수 있다.
정리
제네릭에 타입 매개변수 상한을 사용해서 타입 안정성을 지키면서 상위 타입의 원하는 기능까지 사용할 수 있었다.
덕분에 코드 재사용과 타입 안정성이라는 두 마리 토끼를 동시에 잡을 수 있었다.
제네릭 메서드
앞서 살펴본 제네릭 타입과 지금부터 살펴볼 제네릭 메서드는 둘 다 제네릭을 사용하기는 하지만 서로 다른 기능을 제공한다.
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 t) {
System.out.println("Generic print : " + t);
return t;
}
public static <T extends Number> T numberMethod(T t) {
System.out.println("Number print : " + t);
return t;
}
}
package generic.ex4;
public class MethodMain1 {
public static void main(String[] args) {
Integer i = 10;
Object object = GenericMethod.objMethod(i);
System.out.println("object = " + object);
// Integer integer = (Integer)GenericMethod.objMethod(i);
// 타입 인자(Type Argument) 명시적 전달
System.out.println("타입 인자(Type Argument) 명시적 전달");
Integer result = GenericMethod.<Integer>genericMethod(i);
Integer integerValue = GenericMethod.<Integer>numberMethod(10);
Double doubleValue = GenericMethod.<Double>numberMethod(20.0);
// 타입 추론
System.out.println("타입 추론");
Integer result1 = GenericMethod.genericMethod(i);
Integer integerValue1 = GenericMethod.numberMethod(10);
Double doubleValue1 = GenericMethod.numberMethod(20.0);
}
}
제네릭 타입
정의 : GenericClass<T>
타입 인자 전달 : 객체를 생성하는 시점
ex) new GenericClass<String>
제너릭 메서드
정의 : <T> T genericMethod(T t)
타입 인자 전달 : 메서드를 호출하는 시점
ex) GenericMethod.<Integer>genericMethod(i)
제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용한다.
제네릭 메서드를 정의할 때는 메서드 반환 타입 왼쪽에 다이아몬드를 사용해서 <T> 와 같이 타입 매개변수를 적어준다.
제네릭 메서드는 메서드를 실제 호출하는 시점에 다이아몬드를 사용해서 <Integer> 와 같이 타입을 정하고 호출한다.
제네릭 메서드의 핵심은 메서드를 호출하는 시점에 타입 인자를 전달해서 타입을 지정하는 것이다.
따라서 타입을 지정하면서 메서드를 호출한다.
인스턴스 메서드, static 메서드
제네릭 메서드는 인스턴스 메서드와, static 메서드에 모두 적용할 수 있다.
class Box<T> { //제네릭 타입
static <V> V staticMethod2(V t) {} //static 메서드에 제네릭 메서드 도입
<Z> Z instanceMethod2(Z z) {} //인스턴스 메서드에 제네릭 메서드 도입 가능
}
참고
제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다.
제네릭 타입은 객체를 생성하는 시점(런타임)에 타입이 정해진다. 그런데 static 메서드는 클래스 로딩 시점에 생성된다.
(클래스 로딩 시점에는 런타임에 정해지는 타입을 알 수 없다..)
때문에, static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 한다.
class Box<T> {
T instanceMethod(T t) {} //가능
static T staticMethod1(T t) {} //제네릭 타입의 T 사용 불가능
}
타입 매개변수 제한
제네릭 메서드도 제네릭 타입과 마찬가지로 타입 매개변수를 제한할 수 있다.
public static <T extends Number> T numberMethod(T t) {}
제네릭 메서드 타입 추론
제네릭 메서드를 호출할 때 <Integer> 와 같이 타입 인자를 계속 전달하는 것은 매우 불편하다.
Integer i = 10;
Integer result = GenericMethod.<Integer>genericMethod(i);
자바 컴파일러는 genericMethod() 에 전달되는 인자 i 의 타입이 Integer 라는 것을 알 수 있다.
또한, 반환 타입이 Integer result 라는 것을 알 수 있다. 이런 정보를 통해서 자바 컴파일러는 타입 인자를 추론할 수 있다.
제네릭 메서드 활용
제네릭 메서드 활용
앞서 제네릭 타입으로 만들었던 AnimalHospitalV3 의 주요 기능을 제네릭 메서드로 다시 만들어보자
package generic.ex4;
import generic.animal.Animal;
public class AnimalMethod {
public static <T extends Animal> void checkUp(T t) {
System.out.println("동물 이름 : " + t.getName());
System.out.println("동물 크기 : " + t.getSize());
t.sound();
}
public static <T extends Animal> T bigger(T t1, T t2) {
return t1.getSize() > t2.getSize() ? t1 : t2;
}
}
package generic.ex4;
import generic.animal.Cat;
import generic.animal.Dog;
public class MethodMain2 {
public static void main(String[] args) {
Dog dog = new Dog("멍멍이", 100);
Cat cat = new Cat("냐옹이", 100);
AnimalMethod.checkUp(dog);
AnimalMethod.checkUp(cat);
Dog targetDog = new Dog("큰 멍멍이", 200);
Dog bigger = AnimalMethod.bigger(dog, targetDog);
System.out.println("bigger = " + bigger);
}
}
제네릭 타입과 제네릭 메서드의 우선순위
정적 메서드는 제네릭 메서드만 적용할 수 있지만, 인스턴스 메서드는 제네릭 타입, 제네릭 메서드 둘다 적용할 수 있다.
여기서 제네릭 타입, 제네릭 메서드의 타입 매개변수를 같은 이름으로 사용하면 어떻게 될까?
제네릭 메서드의 타입 매개변수를 사용한다.
소프트웨어는 대부분 더 자세한 것, 구체적인 것에 우선순위가 높다.
와일드카드1
이번에는 제네릭 타입을 조금 더 편리하게 사용할 수 있는 와일드카드에 대해 알아보자.
참고로, 와일드카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다.
와일드카드는 이미 만들어진 제네릭 타입을 활용할 때 사용한다.
package generic.ex5;
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
아래 두 메서드는 비슷한 기능을 하는 코드이다.
하나는 제네릭 메서드를 사용하고 하나는 일반적인 메서드에 와일드카드를 사용했다.
와일드카드는 제네릭 타입이나 제네릭 메서드를 정의할 때처럼 사용하는 것이 아니다.
Box<Dog>, Box<Cat> 처럼 타입 인자가 정해진 제네릭 타입을 전달 받아서 사용할 때 사용한다.
와일드카드인 ? 는 모든 타입을 다 받을 수 있다는 의미이다.
다음과 같이 해석할 수 있다. ? == <? extends Object>
이렇게 ? 만 사용해서 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드카드라고 한다.
//이것은 제네릭 메서드이다.
//Box<Dog> dogBox를 전달한다. 타입 추론에 의해 타입 T가 Dog가 된다.
static <T> void printGenericV1(Box<T> box) {
System.out.println("T = " + box.get());
}
//이것은 제네릭 메서드가 아니다. 일반적인 메서드이다.
//Box<Dog> dogBox를 전달한다. 와일드카드 ?는 모든 타입을 받을 수 있다.
static void printWildcardV1(Box<?> box) {
System.out.println("? = " + box.get());
}
제네릭 메서드 실행 예시
메서드 호출 시점에 타입 매개변수에 타입 인자를 받아 타입을 추론한다.
이런 과정은 복잡하다..
//1. 전달
printGenericV1(dogBox)
//2. 제네릭 타입 결정 dogBox는 Box<Dog> 타입, 타입 추론 -> T의 타입은 Dog
static <T> void printGenericV1(Box<T> box) {
System.out.println("T = " + box.get());
}
//3. 타입 인자 결정
static <Dog> void printGenericV1(Box<Dog> box) {
System.out.println("T = " + box.get());
}
//4. 최종 실행 메서드
static void printGenericV1(Box<Dog> box) {
System.out.println("T = " + box.get());
}
와일드카드 실행 예시
단순히 매개변수로 제네릭 타입을 받을 수 있는 것 뿐이다.
타입 추론을 하지 않는다! (단순한 프로세스)
제네릭 타입 or 제네릭 메서드가 필요한 상황이 아니라면 와일드 카드 사용이 유리
//1. 전달
printWildcardV1(dogBox)
//이것은 제네릭 메서드가 아니다. 일반적인 메서드이다.
//2. 최종 실행 메서드, 와일드카드 ? 는 모든 타입을 받을 수 있다.
static void printWildcardV1(Box<?> box) {
System.out.println("? = " + box.get());
}
제네릭 메서드 vs 와일드카드
printGenericV1() 제네릭 메서드를 보자.
제네릭 메서드에는 타입 매개변수가 존재한다. 그리고 메서드를 호출하는 시점에 타입 인자 전달 또는 타입 추론이 일어나며,
이 과정은 내부적으로 타입 추론 로직이 복잡하게 작동한다.
반면에 printWildcardV1() 메서드를 보자.
와일드카드는 일반 메서드에 사용할 수 있으며, 단순히 제네릭 타입을 받을 수 있는 매개변수를 선언하는 것일 뿐이다.
제네릭 메서드처럼 타입을 결정하거나 복잡한 추론이 필요하지 않다.
타입을 정의하지 않고, 타입을 사용하지도 않으므로 단순하다.
결론적으로,
제네릭 타입이나 제네릭 메서드를 반드시 정의해야 하는 상황이 아니라면,보다 단순한 와일드카드 사용을 권장한다.
와일드카드2
상한 와일드카드
제네릭 메서드와 마찬가지로 와일드카드에도 상한 제한을 둘 수 있다.
여기서는 ? extends Animal 을 사용했다.
Animal 과 그 하위 타입만 입력 받는다. 만약 다른 타입을 입력하면 컴파일 오류가 발생한다.
box.get() 을 통해서 꺼낼 수 있는 타입의 최대 부모는 Animal 이 된다. 따라서 Animal 타입으로 조회할 수 있다.