제네릭
정리
제네릭에 타입 매개변수 상한을 사용해서 타입 안정성을 지키면서 상위 타입의 원하는 기능까지 사용할 수 있었다.
제네릭 타입
- 정의: GenericClass
- 타입 인자 전달: 객체를 생성하는 시점
- 예) new GenericClass<String>
제네릭 메서드
- 정의: <T> T genericMethod(T t)
- 타입 인자 전달: 메서드를 호출하는 시점
- 예) GenericMethod.<Integer>genericMethod(i)
- 제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용한다.
- 제네릭 메서드를 정의할 때는 메서드의 반환 타입 왼쪽에 다이아몬드를 <T>와 같이 타입 매개변수를 적어준다.
- 제네릭 메서드는 메서드르 실제 호출하는 시점에 다이아몬드를 사용해서 <Integer>와 같이 타입을 정하고 호출한다.
인스턴스 메서드, static메서드
class Box<T> { //제네릭 타입
static <V> V staticMethod2(V t) {} //static 메서드에 제네릭 메서드 도입
<Z> Z instanceMethod2(Z z) {} //인스턴스 메서드에 제네릭 메서드 도입 가능
}
class Box2<T> {
T instanceMethod(T t) {} //가능
static T staticMehtod1(T t) {} //제네릭 타입의 T 사용 불가능
}
제네릭 메서드 타입 추론
package generic.ex4;
public class MethodMain1 {
public static void main(String[] args) {
Integer i = 10;
Object obj = GenericMethod.objMethod(i);
// 타입 인자(Type Argument) 명시적 전달
System.out.println("명시적 타입 인자 전달");
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);
}
}
제네릭 메서드 활용
제네릭 클래스와 제네릭 메서드의 우선순위
제네릭 타입 설정
class ComplexBox<T extends Animal>
제네릭 메서드 설정
<T> T printAndReturn(T t)
제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가진다.
따라서 rpintAndReturn()은 제네릭 타입과는 무관하고 제네릭 메서드가 적용된다.
여기서 적용된 제네릭 메서드의 타입 매개변수 T는 상한이 없다. 따라서 Object로 취급된다
Object로 취급되기 때문에 t.getNem()과 같은Animal에 존재하는 메서드는호출할 수 없다.
참고로 프로그래밍에서 이렇게 이름이 모호한 것은 좋지 않으므로 둘 중 하나를 바꿔야한다.
와일드 카드
와일드 카드(whildcard)란 프로그램에서 *, ?와 같이 하나 이상의 문자들을 상징하는 특수문자를 뜻한다. 쉽게 이야기해서 여러 타입이 들어올 수 있다는 뜻이다.
package generic.test.ex5;
import generic.animal.Animal;
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;
}
}
참고
와일드카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다. 와일드카드는 이미 만들어진 제네릭 타입을 활용할 대 사용한다.
비제한 와일드카드
//이것은 제네릭 메서드이다.
//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());
}
?만 사용해서 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드카드라 한다.
제네릭 메서드 vs 와일드카드
제네릭 메서드는 타입 추론을 하고 타입 인자를 전달하는 과정이 복잡하다.
이에 반해 와일드카드는 단순히 매개변수로 제네릭 타입을 받을 수 있는 것뿐이다.
제네릭 타입이나 제네릭 메서드를 정의하는 꼭 필요한 상황이 아니라면, 더 단순한 와일드카드 사용을 권장한다.
상한 와일드 카드
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());
}
- 제네릭 메서드와 마찬가지로 와일드카드에도 상한 제한을 둘 수 있다.
- 여기서는 ? extends Animal을 지정했다.
- 이를 통해 Animal 타입의 기능을 호출할 수 있다.
타입 매개변수가 꼭 필요한 경우
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);
제네릭 메서드는 전달한 타입을 명확하게 반환할 수 있다.
반면 와일드카드는 전당한 타입을 명확하게 반환할 수 없다.
제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이면 <T>를 사용하고, 그렇지 않은 상황이면 와일드카드를 사용하는 것을 권장한다.
하한 와일드카드
package generic.test.ex5;
import generic.animal.Animal;
import generic.animal.Cat;
import generic.animal.Dog;
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));
}
}
타입 이레이저
이레이저(eraser)는 지우개라는 뜻이다.
제네릭은 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다. 제네릭에 사용한 타입 매개변수가 모두 사라지는 것이다. 쉽게 이야기해서 컴파일 전인 .java에는 제네릭 타입매개변수가 존재하지만, 컴파일 이후인 자바 바이트코드 .class에는 타입 매개변수가 존재하지 않는 것이다.
자바의 제네릭 타입은 컴파일 시점에만 존재하고, 런타임 시에는 제네릭 정보가 지워지는데 이것을 타입 이레이져라고 한다.
타입 이레이저 방식의 한계
package generic.test.ex5;
public class EraserBox<T> {
public boolean instanceCheck(Object param) {
return param instanceof T; //오류
}
public void create() {
return new T(); //오류
}
}
- 여기서 T는 런타임에 모두 Object가 된다.
- instanceof는 항상 Object와 비교하게 된다. 이러헥 되면 항상 참이 반환되는 문제가 발생한다. 자바는 이런 문제 때문에 타입 매개변수에 instanceof를 허용하지 않는다.
- new T는항상 new Object가 되어버린다. 갭라자가 의도한 것과는 다르다. 따라서 자바는 타입 매개변수에 new를 허용하지 않는다.
출처 - 김영한의 실전 자바 중급 2편
'Backend > Java' 카테고리의 다른 글
컬렉션 프레임워크 - LinkedList (0) | 2024.08.18 |
---|---|
컬렉션 프레임워크 - ArraryList (0) | 2024.08.18 |
제네릭 1 (0) | 2024.08.14 |
예외 처리2 (0) | 2024.08.13 |
예외 처리 1 (0) | 2024.08.13 |