일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch12
- 스프링 mvc1 - 스프링 mvc
- 스프링 입문(무료)
- @Aspect
- jpa 활용2 - api 개발 고급
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch1
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch6
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch5
- 자바 중급2편 - 컬렉션 프레임워크
- 자바의 정석 기초편 ch4
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch13
- jpa - 객체지향 쿼리 언어
- 2024 정보처리기사 시나공 필기
- 스프링 mvc2 - 검증
- 게시글 목록 api
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch7
- 스프링 mvc1 - 서블릿
- 자바 중급1편 - 날짜와 시간
- 자바의 정석 기초편 ch11
- 자바 기본편 - 다형성
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch14
- 2024 정보처리기사 수제비 실기
- Today
- Total
나구리의 개발공부기록
제네릭, 타입 매개변수 제한(시작, 다형성 시도, 제네릭 도입과 실패, 타입 매개변수 제한), 제네릭 메서드, 제네릭 메서드 활용, 와일드 카드, 타입 이레이저 본문
제네릭, 타입 매개변수 제한(시작, 다형성 시도, 제네릭 도입과 실패, 타입 매개변수 제한), 제네릭 메서드, 제네릭 메서드 활용, 와일드 카드, 타입 이레이저
소소한나구리 2025. 2. 2. 18:57출처 : 인프런 - 김영한의 실전 자바 - 중급2편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 타입 매개변수 제한
1) 시작
(1) 요구사항
- 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있는 동물 병원 작성
(2) DogHospital
- 개 병원은 내부에 Dog 타입을 가지며 개의 이름과 크기를 출력하고 개의 sound()메서드를 호출하는 checkup()메서드와 다른 개와 크기를 비교하는 bigger() 메서드를 가지고 있음
package generic.ex3;
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;
}
}
(3) CatHospital
- 타입만 Cat으로 변경되었을 뿐 DogHospital 완전히 동일함
(4) AnimalHospitalMainV0
- 요구사항대로 개 병원은 개만 받을 수 있고 고양이 병원은 고양이만 받을 수 있도록 요구사항이 지켜져서 개발되었음
- 개 병원과 고양이 병원을 각각 별도의 클래스로 만들었기 때문에 개 병원에 고양이를 전달하면 컴파일 오류가 발생함
- 개 병원에서 bigger()로 다른 개를 비교하는 경우 더 큰 개를 Dog 타입으로 반환함
package generic.ex3;
public class AnimalHospitalMainV0 {
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);
}
}
/* 실행 결과
동물 이름: 멍멍이1
동물 크기: 100
멍멍
동물 이름: 야옹이1
동물 크기: 300
야옹
biggerDog = Animal{name='멍멍이2', size=200}
*/
(5) 문제
- 타입 안정성은 명확하게 지켜졌으나 개 병원과 고양이 병원의 코드는 중복이 많으며 코드 재사용성이 없음
2) 다형성 시도
(1) AnimalHospitalV1
- Dog, Cat은 Animal이라는 명확한 부모 타입이 있기 때문에 다형성을 사용하여 중복 제거를 시도
- Animal 타입을 받아서 처리하며, checkup(), getBigger()에서 사용하는 getName(), getSize(), sound()메서드는 모두 Animal 타입이 제공하는 메서드이기 때문에 아무 문제 없이 모두 호출할 수 있음
package generic.ex3;
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;
}
}
(2) AnimalHospitalMainV1
- 다형성을 통해 AnimalHospitalV1 하나로 개와 고양이를 모두 처리할 수 있어서 코드 재사용성이 좋아졌음
- 그러나 요구사항과는 다르게 개 병원에 고양이를 전달이 가능해지는 문제가 발생하게 됨
- bigger()메서드의 반환 타입이 Animal이기 때문에 다운 캐스팅을 해야하고, 실수로 고양이를 입력 후 개를 반환해야하는 상황이라면 캐스팅 예외가 발생함
package generic.ex3;
public class AnimalHospitalMainV1 {
public static void main(String[] args) {
AnimalHospitalV1 dogHospital = new AnimalHospitalV1();
AnimalHospitalV1 catHospital = new AnimalHospitalV1();
// ... 기존 코드 동일 생략
// 문제1: 개 병원에 고양이 전달
dogHospital.set(cat); // 매개변수 체크 실패, 컴파일 오류가 발생하지 않음
// 문제2: 개 타입 변환 -> 다운 캐스팅 필요
dogHospital.set(dog);
Dog biggerDog = (Dog) dogHospital.bigger(new Dog("멍멍이2", 200));
System.out.println("biggerDog = " + biggerDog);
}
}
3) 제네릭 도입과 실패
(1) AnimalHospitalV2
- <T>를 사용하여 제네릭 타입을 선언했지만 기존의 메서드들을 모두 사용할 수 없어 컴파일 오류가 발생함
- 제네릭 타입을 선언하면 자바 컴파일러 입장에서 T에 어떤 값이 들어올지 예측할 수 없음
- Animal 타입의 자식이 들어오기를 기대했지만 Animal에 대한 정보가 어디에도 없으므로 어떤 타입이든 모두 들어올 수 있게 되고, 자바 컴파일러는 어떤 타입이 들어올 지 알 수 없는 경우에는 모든 객체의 최종 부모인 Object 타입으로 가정하게되어 Object가 제공하는 메서드만 호출할 수 있게 됨
- 즉, 원하는 기능을 사용하려면 Animal 타입이 제공하는 기능들이 필요하지만 이 기능들을 모두 사용할 수 없음
package generic.ex3;
public class AnimalHospitalV2<T> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public void checkup() {
// T의 타입을 메서드를 정의하는 시점에는 알 수 없음, Object의 기능만 사용 가능
animal.toString();
animal.equals(null);
// 컴파일 오류 발생
// 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;
}
}
(2) AnimalHospitalMainV2
- 여기에 추가로 문제가 더있는데, 동물 병원에 Integer, Object 같은 동물과 전혀 관계 없는 타입을 타입 인자로 전달이 가능하게 됨
- 최소한 Animal이나 그 자식의 타입만으로 제한하고 싶지만 모든 타입이 들어올 수 있도록 변경됨
package generic.ex3;
public class AnimalHospitalMainV2 {
public static void main(String[] args) {
AnimalHospitalV2<Dog> dogHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Cat> catHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Integer> integerHospital = new AnimalHospitalV2<>();
AnimalHospitalV2<Object> objectHospital = new AnimalHospitalV2<>();
}
}
(3) 문제 정리와 해결방안
- 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있음
- 타입 매개변수를 어떤 타입이든 수용할 수 있는 Object로 가정하고 Object의 기능만 사용할 수 있게됨
- 발생한 문제들을 생각해보면 타입 매개변수를 Animal로 제한하지 않았기 때문이므로 만약 타입 인자가 모두 Animal과 그 자식만 들어올 수 있게 제한할 수 있다면 문제를 해결할 수 있음
4) 타입 매개변수 제한
(1) AnimalHospitalV3<T extends Animal>
- 타입 매개변수 T를 Animal과 그 자식만 받을 수 있도록 제한을 둠
- T의 상한을 Animal으로 제한을 두었기 때문에 타입 인자로 들어올 수 있는 타입이 Animal과 그 자식으로 제한되어 자바 컴파일러가 T에 입력될 수 있는 값의 범위를 예측할 수 있게 됨
- 타입 매개변수 T에는 Animal과 그의 자식인 Dog, Cat만 들어올 수 있으므로 이를 모두 수용할 수 있는 Animal을 T의 타입으로 가정해도 문제가 없어지게 되고, Animal이 제공하는 메서드들도 모두 사용할 수 있게 됨
package generic.ex3;
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;
}
}
(2) AnimalHospitalMainV3
- 타입 매개변수에 입력될 수 있는 상한을 지정하여 기존의 문제를 모두 해결하면서 요구사항을 지킬 수 있게 됨
- dogHospital.set()에 Dog가 아닌 다른타입을 입력하면 컴파일 오류가 발생하고, bigger()메서드를 사용할 때에도 각 타입에 맞게 타입이 반환됨
- 거기에 AnimalHospitalV3<Integer>와 같이 동물과 전혀 관계없는 타입 인자를 컴파일 시점에 막을 수 있게 됨
package generic.ex3;
public class AnimalHospitalMainV3 {
public static void main(String[] args) {
AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3<>();
AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3<>();
// Animal, Dog, Cat 외의 타입이 인자로 들어오면 컴파일 오류 발생
// AnimalHospitalV3<Integer> integerHospital = new AnimalHospitalV3<>();
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);
}
}
5) 기존 문제와 해결
(1) 타입 안정성이 없는 문제들 해결
- 개 병원에 고양이를 전달하는 문제
- Animal 타입을 반환하여 다운 캐스팅을 해야하는 문제
- 실수로 고양이를 입력했는데 개를 반환하는 상황이 되면 캐스팅 예외가 발생하는 문제
(2) 제네릭 도입 문제들 해결
- 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있는 문제
- 어떤 타입이든 수용할 수 있는Object로 가정하게되어 Object 기능만 사용하게되는 문제
(3) 정리
- 제네릭에 타입 매개변수 상한을 사용하여 타입 안전성을 지키면서 상위 타입의 원하는 기능까지 사용할 수 있게 되어 코드 재사용과 타입 안전성이라는 두 마리 토끼를 동시에 잡을 수 있게 됨
2. 제네릭 메서드
1) 제네릭 메서드
(1) GenericMethod
- 제네릭을 클래스에사용하지 않고 메서드에 적용할 수 있는데 이를 제네릭 메서드라고 하며 제네릭 타입과는 다른 기능을 제공함
- 마찬가지로 타입 매개변수의 상한도 적용할 수 있음
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("bound print: " + t);
return t;
}
}
(2) MethodMain1
- 제네릭 메서드를 사용하는 코드들로 메서드를 호출할 때 타입을 전달함
package generic.ex4;
public class MethodMain1 {
public static void main(String[] args) {
Integer i = 10;
Object object = GenericMethod.objMethod(i);
// 타입 인자(Type Argument) 명시적 전달
Integer result = GenericMethod.<Integer>genericMethod(i);
Integer integerValue = GenericMethod.<Integer>numberMethod(10);
Double doubleValue = GenericMethod.<Double>numberMethod(20.0);
}
}
/* 실행 결과
Object print: 10
generic print: 10
bound print: 10
bound print: 20.0
*/
(3) 제네릭 타입
- 정의: GenericClass<T>
- 타입 인자 전달: 객체를 생성하는 시점
(4) 제네릭 메서드
- 정의: <T> T genericMethod(T t)
- 타입 인자 전달: 메서드를 호출하는 시점
- 제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용함
- 제네릭 메서드를 정의할 때는 메서드의 반환 타입 왼쪽에 다이아몬드를 사용하여 <T>와 같이 타입 매개변수를 적어주며 메서드를 실제 호출하는 시점에 다이아몬드를 사용하여 <Integer>와 같이 타입을 정하고 호출함
- 즉, 제네릭 메서드의 핵심은 메서드를 호출하는 시점에 타입 인자를 전달해서 타입을 지정하는 것임
(5) 인스턴스 메서드, static 메서드에서의 제네릭 메서드
- 제네릭 메서드는 인스턴스 메서드와 static 메서드에 모두 적용할 수 있음
- 제네릭 타입은 static 메서드에 타입 매개변수를 적용할 수 없는데, 제네릭 타입은 객체를 생성하는 시점에 타입이 정해지기 때문에 클래스 단위로 동작하는 static 메서드는 제네릭 타입과 무관하므로 static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 함
class Box<T> { //제네릭 타입
static <V> V staticMethod2(V t) {} //static 메서드에 제네릭 메서드 도입
<Z> Z instanceMethod2(Z z) {} //인스턴스 메서드에 제네릭 메서드 도입 가능
T instanceMethod(T t) {} //가능
static T staticMethod1(T t) {} //제네릭 타입의 T 사용 불가능
}
(6) 타입 매개변수 제한
- 제네릭 메서드도 제네릭 타입과 마찬가지로 타입 매개변수를 제한할 수 있음
- numberMethod()의 타입 매개변수를 Number로 제한했기 때문에 Number와 그의 자식만 받을 수 있어 String을 인자로 전달하거나 문자열을 메서드의 매개변수로 입력하면 오류가 발생함
** 참고
- Integer, Double, Long과 같은 숫자 타입이 Number의 자식임
public static <T extends Number> T numberMethod(T t) {}
//GenericMethod.numberMethod("Hello"); // 컴파일 오류 Number의 자식만 입력 가능
(7) 제네릭 메서드 타입 추론
- 제네릭 메서드를 호출할 때 <Integer>와 같이 타입 인자를 계속 전달하게되면 불편함
- 자바 컴파일러는 genericMethod()에 전달되는 인자 i의 타입이 Integer라는 것을 알 수 있고, 반환 타입이 Integer result라는 것도 알 수 있으므로, 이런 정보를 통해 자바 컴파일러가 타입 인자를 추론할 수 있어서 타입 인자를 생략할 수 있음
- 타입을 추론하게 되면 컴파일러가 대신 처리하기 때문에 타입을 전달하지 않는 것 처럼 보이지만 실제로는 타입 인자가 전달된다는 것을 기억하면 됨
// 타입 인자 생략
Integer result2 = GenericMethod.genericMethod(i);
Integer integerValue2 = GenericMethod.numberMethod(10);
Double doubleValue2 = GenericMethod.numberMethod(20.0);
3. 제네릭 메서드 활용
1) 제네릭 타입으로 만든 AnimalHospitalV3을 제네릭 메서드로 변경
(1) AnimalMethod
- checkup(), bigger()의 메서드를 제네릭 메서드로 정의하고 사용하기 쉽게 static으로 변경
- 두 메서드 모두 Animal을 상한으로 제한함
package generic.ex4;
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;
}
}
(2) MethodMain2
- 타입 추론을 활용하여 제네릭 메서드를 호출할 때 타입 인자를 생략하고 호출함
- bigger()메서드를 호출 시 동일한 타입을 메서드의 매개변수로 입력해주어야 정상적으로 호출이 가능하고, 서로 다른 타입을 입력하면 타입 추론에서 컴파일 오류가 발생하여 타입 안정성을 지킬 수 있게됨
- 제네릭 타입으로 만들었던 코드와 동일한 출력결과를 얻을 수 있는 것을 확인할 수 있음
package generic.ex4;
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);
}
}
/* 실행 결과
동물 이름: 멍멍이
동물 크기: 100
멍멍
동물 이름: 야옹이
동물 크기: 100
야옹
bigger = Animal{name='큰 멍멍이', size=200}
*/
(3) 제네릭 타입과 제네릭 메서드의 우선순위
- 정적 메서드는 제네릭 메서드만 적용할 수 있지만 인스턴스 메서드는 제네릭 타입과 제네릭 메서드를 둘다 적용할 수 있음
- 제네릭 타입도 T고 제네릭 메서드도 T라고 되어있으면 제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가지기 때문에 printAndReturn()은 제네릭 타입은 무시가 되고 제네릭 메서드가 적용됨
- 항상 프로그래밍에서는 구체적인것이 우선순위가 되기 때문에 클래스레벨보다 더 구체적인 메서드 레벨의 단위가 먼저 적용된다고 생각하면 됨
- 여기서 적용된 제네릭 메서드의 타입 매개변수 T는 상한 제한이 없으므로 Object로 취급이 되기 때문에 제네릭 메서드에서 t.getName()처럼 Animal에 존재하는 메서드는 호출할 수 없게됨
- 참고로 프로그래밍에서 이렇게 모호하게 작성하는 것은 매우 좋지 않기 때문에 이렇게 이름이 겹치면 제네릭 타입이든, 제네릭 메서드든 무엇이든 둘중에 하나를 이름을 다른것으로 변경하는 것이 좋음
package generic.ex4;
import generic.animal.Animal;
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());
// t.getName(); // 제네릭 메서드는 <T> 타입이므로 Object로 취급되어 호출 불가능
return t;
}
}
package generic.ex4;
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);
}
}
/* 실행 결과
animal.className: generic.animal.Dog
t.className: generic.animal.Cat
returnCat = Animal{name='야옹이', size=50}
*/
4. 와일드 카드
1) 와일드 카드
(1) 설명
- 제네릭 타입을 조금 더 편리하게 사용할 수 있는 기능
- 컴퓨터 프로그래밍에서 *, ?와 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻하는데, 이를 제네릭에 적용하면 여러 타입이 들어올 수 있다는 뜻임
(2) Box<T>
- 단순히 데이터를 넣고 반환할 수 있는 제네릭 타입
package generic.ex5;
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
(3) WildcardEx
- 제네릭 메서드와 와일드 카드를 비교할 수 있도록 같은 기능을 하나씩 배치
- 와일드 카드는 ?를 사용하여 정의함
package generic.ex5;
public class WildcardEx {
static <T> void printGenericV1(Box<T> box) {
System.out.println("T = " + box.get());
}
static void printWildcardV1(Box<?> box) {
System.out.println("? = " + box.get());
}
static <T extends Animal> void printGenericV2(Box<T> box) {
T t = box.get();
System.out.println("이름 = " + t.getName());
}
static void printWildcardV2(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println("이름 = " + animal.getName());
}
static <T extends Animal> T printAndReturnGeneric(Box<T> box) {
T t = box.get();
System.out.println("이름 = " + t.getName());
return t;
}
static Animal printAndReturnWildcard(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println("이름 = " + animal.getName());
return animal;
}
}
(4) WildcardMain1
- 와일드 카드를 사용할 때와 제네릭 메서드를 사용할 때 모두 동일하게 동작하도록 메서드를 정의했으므로 모두 똑같은 기능으로 출력되는 것을 확인할 수 있음
package generic.ex5;
public class WildcardMain1 {
public static void main(String[] args) {
Box<Object> objBox = new Box<>();
Box<Dog> dogBox = new Box<>();
Box<Cat> catBox = new Box<>();
dogBox.set(new Dog("멍멍이", 100));
WildcardEx.printGenericV1(dogBox);
WildcardEx.printWildcardV1(dogBox);
WildcardEx.printGenericV2(dogBox);
WildcardEx.printWildcardV2(dogBox);
Dog dog = WildcardEx.printAndReturnGeneric(dogBox);
Animal animal = WildcardEx.printAndReturnWildcard(dogBox);
}
}
/* 실행 결과
T = Animal{name='멍멍이', size=100}
? = Animal{name='멍멍이', size=100}
이름 = 멍멍이
이름 = 멍멍이
이름 = 멍멍이
이름 = 멍멍이
*/
** 참고
- 와일드카드는 제네릭 타입이나 제네릭 메서드를 선언하는 것이 아니라 이미 만들어진 제네릭 타입을 활용할 때 사용하는 것임
(5) 비제한 와일드 카드
- 두 메서드는 비슷한 기능을 하는 코드이지만 하나는 제네릭 메서드를 사용하고 하나는 일반적인 메서드에 와일드카드를 사용한 것임
- 와일드 카드는 제네릭 타입이나 제네릭 메서드를 정의할 때 사용하는 것이 아니라 Box<Dog>, Box<Cat> 처럼 타입 인자가 정해진 제네릭 타입을 전달 받아서 활용할 때 사용함
- 와일드 카드인 ?는 모든 타입을 다 받을 수 있다는 뜻으로 ?는 <? extends Object>처럼 해석할 수 있으며 아래의 printWildcardV1(Box<?> box) 메서드에서 box.get()의 반환타입을 받아보면 Object 타입으로 반환 되는 것을 확인할 수 있음
- 이렇게 ?만 사용하여 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드 카드라고 함
// 제네릭 메서드, Box<Dog> dogBox를 전달하면 타입 추론에 의해 타입 T가 Dog가 됨
static <T> void printGenericV1(Box<T> box) {
System.out.println("T = " + box.get());
}
// 제네릭 메서드가 아닌 일반적인 메서드, 와일드 카드 ?는 모든 타입을 받을 수 있음
static void printWildcardV1(Box<?> box) {
System.out.println("? = " + box.get());
Object object = box.get();
}
(6) 제네릭 메서드 vs 와일드 카드
- 제네릭 메서드 실행 예시: 인자 전달 -> 제네릭 타입 결정(타입 추론) -> 타입 인자 결정 -> 최종 실행
- 와일드 카드 실행 예시: 인자 전달 -> 전달 된 인자로 메서드 실행
- 제네릭 메서드는 타입 매개변수가 존재하고 특정 시점에 타입 매개변수에 타입인자를 전달하여 타입을 결정해야하는 등, 과정이 와일드카드에 비해 복잡함
- 와일드 카드는 일반적인 메서드에 사용하며 단순히 매개변수로 제네릭 타입을 받을 수 있는 것이므로 제네릭 메서드처럼 타입을 결정하거나 복잡하게 작동하지 않음
- 제네릭 타입이나 제네릭 메서드를 정의하는 꼭 필요한 상황이 아니라면 더 단순한 와일드카드 사용을 권장함
(7) 상한 와일드카드
- 제네릭 메서드와 마찬가지로 와일드카드에도 상한 제한을 둘 수 있으며 여기서는 ? extend Animal로 지정하였음
- 이렇게 되면 Animal과 그 하위 타입만 입력받을 수 있으므로 다른 타입을 입력하면 컴파일 오류가 발생하게 됨
- box.get()을 통해서 꺼낼 수 있는 타입의 최대 부모는 Animal이 되므로 Animal 타입으로 조회할 수 있고 그 기능을 호출 할 수 있음
static <T extends Animal> void printGenericV2(Box<T> box) {
T t = box.get();
System.out.println("이름 = " + t.getName());
}
static void printWildcardV2(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println("이름 = " + animal.getName());
}
(8) 타입 매개변수가 꼭 필요한 경우
- 와일드 카드는 제네릭을 정의할 때 사용하는 것이 아니고 타입 인자가 전달된 제네릭 타입을 활용할 때 사용되므로 반환타입이 있는 경우에는 제네릭 타입이나 제네릭 메서드를 사용해야 문제를 해결할 수 있음
- 제네릭 메서드를 활용한 printAndReturnGeneric(dogBox)메서드를 호출한 경우 정확하게 전달한 타입을 명확하게 반환하지만, printAndReturnWildcard()의 경우 전달한 타입을 명확하게 반환하지 않고 Animal 타입으로 반환하고 있음
- 메서드의 타입들을 특정 시점에 변경하려면 제네릭 타입이나 제네릭 메서드를 사용해야 인자로 전달된 타입으로 타입을 결정 지을 수 있음
- 와일드카드는 이미 만들어진 제네릭 타입을 전달받아서 활용할 때 사용하기 때문에 메서드의 타입들을 타입 인자를 통해 변경할 수가 없고 단순히 일반 메서드에 사용한다고 생각하면 됨
- 제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황에서만 <T>를 사용하고, 그렇지 않은 일반적인 상황에서는 와일드 카드를 사용하는 것을 권장함
static <T extends Animal> T printAndReturnGeneric(Box<T> box) {
T t = box.get();
System.out.println("이름 = " + t.getName());
return t;
}
static Animal printAndReturnWildcard(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println("이름 = " + animal.getName());
return animal;
}
Dog dog = WildcardEx.printAndReturnGeneric(dogBox);
Animal animal = WildcardEx.printAndReturnWildcard(dogBox);
(9) 하한 와일드 카드
- 와일드카드는 상한 뿐만 아니라 하한도 지정할 수 있음
- writeBox(Box<? super Animal> box)처럼 와일드 카드에 Animal 타입을 포함한 Animal타입의 상위 타입만 입력 받을 수 있음
- 즉, 하한을 Animal로 제한 했기 때문에 writeBox() 메서드를 호출할 때 Animal의 하위 타입인 dogBox와 catBox를 전달하려고 하면 컴파일 오류가 발생함
- 참고로 제네릭 타입이나 제네릭 메서드에는 사용할 수 없고 와일드 카드에만 하한을 적용할 수 있음
package generic.ex5;
public class WildcardMain2 {
public static void main(String[] args) {
Box<Object> objBox = new Box<>();
Box<Animal> animalBox = new Box<>();
Box<Dog> dogBox = new Box<>();
Box<Cat> catBox = new Box<>();
// Animal 포함 상위 타입 전달 가능
writeBox(objBox);
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}
*/
5. 타입 이레이저
1) 타입 이레이저
(1) 설명
- 이레이저(eraser)의 뜻인 지우개 처럼 제네릭은 자바 컴파일 단계에서만 사용되고 컴파일 이후에는 제네릭 정보가 삭제됨
- 컴파일 전인 .java에는 제네릭 타입 매개변수가 존재하지만, 컴파일 이후인 자바 바이트코드 .class에는 타입 매개변수가 존재하지 않음
- 아래에 설명하는 코드는 100% 정확한 코드는 아니고 대략 이런 방식으로 동작한다고 보여주는 코드임
(2) 제네릭 타입 선언 - GenericBox.java
public class GenericBox<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
(3) 제네릭 타입에 Integer 타입 인자 전달 - Main.java
- 이렇게 하면 자바 컴파일러는 컴파일 시점에 타입 매개변수와 타입 인자를 포함한 제네릭 정보를 활용하여 new GenericBox<Integer>()에 대해 선언된 제네릭 박스를 아래처럼 이해하게 됨
void main() {
GenericBox<Integer> box = new GenericBox<Integer>();
box.set(10);
Integer result = box.get();
}
public class GenericBox<Integer> {
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
}
(4) 컴파일 후 - GenericBox.class, Main.class
- 컴파일이 모두 끝나면 자바는 제네릭과 관련된 정보를 삭제하며 .class에 생성된 정보는 아래와 같음
- 상한 제한 없이 선언한 타입 매개변수 T는 Object로 변환되고, 자바 컴파일러가 제네릭 타입에서 타입 인자로 지정한 Integer로 캐스팅하는 코드를 추가해줌
- 이렇게 추가된 코드들은 자바 컴파일러가 이미 검증하고 추가했기 때문에 문제가 발생하지 않음
public class GenericBox {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
void main() {
GenericBox box = new GenericBox();
box.set(10);
Integer result = (Integer) box.get(); //컴파일러가 캐스팅 추가
}
(5) 타입 매개변수 제한의 경우
- 컴파일 전에 적용한 타입 매개변수 T가 컴파일 후에는 상한으로 지정된 Animal 타입으로 대체되기 때문에 타입 정보가 제거되어도 Animal 타입의 메서드를 사용하는데 아무런 문제가 없음
- 반환 받는 부분도 자바 컴파일러가 타입 인자로 지정한 Dog로 다운 캐스팅하는 코드를 자동으로 넣어줌
// AnimalHospitalV3.java - 컴파일 전
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 getBigger(T target) {
return animal.getSize() > target.getSize() ? animal : target;
}
}
// AnimalHospitalV3.class - 컴파일 후
public class AnimalHospitalV3 {
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 getBigger(Animal target) {
return animal.getSize() > target.getSize() ? animal : target;
}
}
// 사용코드 컴파일 전,후
Dog dog = animalHospitalV3.getBigger(new Dog()); // 컴파일 전
Dog dog = (Dog) animalHospitalV3.getBigger(new Dog()); // 컴파일 후
(6) 정리
- 자바의 제네릭은 단순하게 생각하면 개발자가 직접 캐스팅 하는 코드를 컴파일러가 대신 처리해주는 것이라고 보면 됨
- 자바는 컴파일 시점에 제네릭을 사용한 코드에 문제가 없는지 완벽하게 검증하기 때문에 자바 컴파일러가 추가하는 다운 캐스팅에는 문제가 발생하지 않음
- 자바의 제네릭 타입은 컴파일 시점에만 존재하고, 런타임 시에는 제네릭 정보가 지워지는데 이것을 타입 이레이저라고 함
(7) 타입 이레이저 방식의 한계
- 컴파일 이후에는 제네릭의 타입 정보가 존재하지 않기 때문에 런타임에 타입을 활용하는 코드들은 사용할 수 없음
- T는 런타임에 모두 Object가 되어버리면 instanceof T는 항상 Object와 비교하게되고 항상 참이 반환되어버리고, new T는 항상 new Object가 되어버림
- 이렇게 동작하는 것은 개발자가 의도하는 것과 다르게 동작하게 되는 것이므로 자바는 타입 매개변수에 instanceof와 new를 허용하지 않음
// 소스 코드
class EraserBox<T> {
public boolean instanceCheck(Object param) {
return param instanceof T; // 오류
}
public void create() {
return new T(); // 오류
}
}
// 런타임
class EraserBox {
public boolean instanceCheck(Object param) {
return param instanceof Object; // 오류
}
public void create() {
return new Object(); // 오류
}
}
6. 문제와 풀이
1) 준비
(1) 준비 코드
- BioUnit은 유닛들의 부모 클래스
- BioUnit의 자식 클래스로 Marine, Zealot, Zergling이 있음
BioUnit
package generic.test.ex3.unit;
public class BioUnit {
private String name;
private int hp;
public BioUnit(String name, int hp) {
this.name = name;
this.hp = hp;
}
public String getName() {
return name;
}
public int getHp() {
return hp;
}
@Override
public String toString() {
return "BioUnit{" +
"name='" + name + '\'' +
", hp=" + hp +
'}';
}
}
Marine, Zealot, Zergling
package generic.test.ex3.unit;
public class Marine extends BioUnit {
public Marine(String name, int hp) {
super(name, hp);
}
}
package generic.test.ex3.unit;
public class Zealot extends BioUnit{
public Zealot(String name, int hp) {
super(name, hp);
}
}
package generic.test.ex3.unit;
public class Zergling extends BioUnit{
public Zergling(String name, int hp) {
super(name, hp);
}
}
2) 제네릭 메서드와 상한
(1) 문제 설명
- 다음 코드와 실행 결과를 참고하여 UnitUtil 클래스를 생성
- UnitUtil.maxHp()메서드의 조건은 아래와 같음
- 1. 두 유닛을 입력 받아서 체력이 높은 유닛을 반환하고, 체력이 같은 경우 둘 중 아무나 반환해도 됨
- 2. 제네릭 메서드를 사용해야 함
- 3. 입력하는 유닛의 타입과 반환하는 유닛의 타입이 같아야 함
package generic.test.ex3;
import generic.test.ex3.unit.Marine;
import generic.test.ex3.unit.Zealot;
public class UnitUtilTest {
public static void main(String[] args) {
Marine m1 = new Marine("마린1", 40);
Marine m2 = new Marine("마린2", 50);
Marine resultMarine = UnitUtil.maxHp(m1, m2);
System.out.println("resultMarine = " + resultMarine);
Zealot z1 = new Zealot("질럿1", 100);
Zealot z2 = new Zealot("질럿2", 150);
Zealot resultZealot = UnitUtil.maxHp(z1, z2);
System.out.println("resultZealot = " + resultZealot);
}
}
실행 결과
resultMarine = BioUnit{name='마린2', hp=50}
resultZealot = BioUnit{name='질럿2', hp=150}
(2) 정답
package generic.test.ex3;
public class UnitUtil {
public static <T extends BioUnit> T maxHp(T t1, T t2) {
return t1.getHp() > t2.getHp() ? t1 : t2;
}
}
3) 제네릭 타입과 상한
(1) 문제 설명
- 다음 코드와 실행 결과를 참고하여 Shuttle 클래스를 생성
- Shuttle 클래스의 조건은 아래와 같음
- 1. 제네릭 타입을 사용해야 함
- 2. showInfo() 메서드를 통해 탑승한 유닛의 정보를 출력
package generic.test.ex3;
import generic.test.ex3.unit.Marine;
import generic.test.ex3.unit.Zealot;
import generic.test.ex3.unit.Zergling;
public class ShuttleTest {
public static void main(String[] args) {
Shuttle<Marine> shuttle1 = new Shuttle<>();
shuttle1.in(new Marine("마린", 40));
shuttle1.showInfo();
Shuttle<Zergling> shuttle2 = new Shuttle<>();
shuttle2.in(new Zergling("저글링", 35));
shuttle2.showInfo();
Shuttle<Zealot> shuttle3 = new Shuttle<>();
shuttle3.in(new Zealot("질럿", 100));
shuttle3.showInfo();
}
}
실행 결과
이름: 마린, HP: 40
이름: 저글링, HP: 35
이름: 질럿, HP: 100
(2) 정답
package generic.test.ex3;
public class Shuttle<T extends BioUnit> {
private T unit;
public void in(T unit) {
this.unit = unit;
}
public T out() {
return unit;
}
public void showInfo() {
System.out.println("이름: " + unit.getName() + ", HP: " + unit.getHp());
}
}
4) 제네릭 메서드와 와일드 카드
(1) 문제 설명
- 앞서 만든 문제에서 만든 Shuttle을 활용하고 다음 코드와 실행 결과를 참고하여 UnitPrinter 클래스를 생성
- UnitPrinter 클래스의 조건은 다음과 같음
- 1. UnitPrinter.printV1()은 제네릭 메서드로 구현해야함
- 2. UnitPrinter.printV2()는 와일드카드로 구현해야 함
- 이 두 메서드는 셔틀에 들어있는 유닛의 정보를 출력함
package generic.test.ex3;
import generic.test.ex3.unit.Marine;
import generic.test.ex3.unit.Zealot;
import generic.test.ex3.unit.Zergling;
public class UnitPrinterTest {
public static void main(String[] args) {
Shuttle<Marine> shuttle1 = new Shuttle<>();
shuttle1.in(new Marine("마린", 40));
Shuttle<Zergling> shuttle2 = new Shuttle<>();
shuttle2.in(new Zergling("저글링", 35));
Shuttle<Zealot> shuttle3 = new Shuttle<>();
shuttle3.in(new Zealot("질럿", 100));
UnitPrinter.printV1(shuttle1);
UnitPrinter.printV2(shuttle1);
}
}
실행 결과
이름: 마린, HP: 40
이름: 마린, HP: 40
(2) 정답
package generic.test.ex3;
public class UnitPrinter {
static <T extends BioUnit> void printV1(Shuttle<T> shuttle) {
T t = shuttle.out();
System.out.println("이름: " + t.getName() + ", HP: " + t.getHp());
}
static void printV2(Shuttle<?> shuttle) {
BioUnit unit = shuttle.out();
System.out.println("이름: " + unit.getName() + ", HP: " + unit.getHp());
}
}
5) 정리
- 실무에서 직접 제네릭을 사용해서 무언가를 설계하거나 만드는 일은 드물며 대부분 이미 제네릭을 통해 만들어진 프레임워크나 라이브러리들을 가져다 사용하는 경우가 훨씬 많음
- 그래서 이미 만들어진 코드의 제네릭을 읽고 이해하는 정도면 충분하며 실무에서 직접 제네릭을 사용하더라고 어렵고 복잡하게 사용하는 것이 아니라 보통 단순하게 사용하므로 지금까지 학습한 정도면 실무에서 필요한 제네릭은 충분이 이해했다고 볼 수 있음
- 제네릭은 지금까지 설명한 내용보다 더 복잡하고 더 어려운 공변(covariant), 반변(contravariant)과 같은 개념들도 있는데, 이런 개념들을 이해하면 와일드카드가 존재하는 이유도 더 깊이있게 알 수 있음
- 그러나 제네릭을 사용해서 매우 복잡한 라이브러리나 프레임워크를 직접 설계하지 않는 이상 이런 개념들을 꼭 이해할 필요는 없음
- 이러한 부분은 실무에서 많은 경험을 쌓고 본인이 필요하다고 느껴질 때 따로 공부하는 것을 권장함
- 제네릭은 이후에 설명하는 컬렉션 프레임워크에서 가장 많이 사용되기 때문에 컬렉션 프레임워크를 통해서 제네릭이 어떻게 활용되는지 자연스럽게 학습할 수 있음