다양한 타입을 처리하는 같은 로직이 필요할 때
제네릭을 사용하여 처리할 수 있다
Object 모든 타입의 부모이기 때문에
Object 타입을 이용한 다형성으로 다양한 타입을 처리할 수 있을 것 같다
코드 재사용성을 높이기 위해 타입 안정성이 매우 떨어진다
제네릭을 사용하면 코드 재사용과 타입 안정성 모두 만족하도록 코드를 작성할 수 있다
public class GenericBox<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
GenericBox<String> stringBox = new GenericBox<String>(); // 타입 결정
제네릭을 사용하면 자바 컴파일러가 입력한 타입 정보를 기반으로
코드를 가정하고 컴파일 과정에 타입 정보를 반영한다
이 과정에서 타입과 맞지 않는 행동을 하면 컴파일 오류가 발생한다
new 생성자()
작성 시 제네릭은 생략할 수 있다
생략하면 자바에서 타입 선언 정보를 기반으로 타입을 추론한다
주변에 읽을 수 있는 타입 정보가 주변에 있어야만 추론할 수 있다
GenericBox<String> stringBox = new GenericBox<String>();
GenericBox<String> stringBox = new GenericBox<>(); // 타입 추론
제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 것이다
타입을 클래스 정의 시가 아닌 인스턴스 생성 시점에 결정하는 것이다
class Generic<K, V> {}
GenericBox rowBox = new GenericBox();
<> 를 생략하면 row type 제네릭이 생성된다
row type 은 제네릭에 Object 를 주는 것과 비슷하다
row type 은 사용하지 않는다
레거시 버전의 코드와의 호환을 위한 기능
Object 를 꼭 제네릭에서 사용해야 한다면
명시적으로 작성해주는 것이 좋다
GenericBox<Object> rowBox = new GenericBox<Object>();
제네릭 타입을 선언하면 컴파일러 입장에서 T 에 어떤 값이 사용될지 알 수 없다
떄문에 특정 타입의 행동을 하는 메서드가 필요하다면
해당 타입으로 한정지어줄 수 있도록 제한해야 한다
이 때 제네릭 대신 다형성을 이용해 부모 클래스를 넣어주면
재사용성은 좋지만 타입 안정성이 현저히 떨어진다
아래와 같이 부모 타입을 상속받는 타입 매개변수를 사용하여
타입 매개변수를 특정 타입에 제한할 수 있다
class BoundedGeneric<T extends Animal> {}
타입 매개변수 T 를 Animal 과 그 자식만 받을 수 있도록 제한을 두는 것으로
T 의 상한이 Animal 타입으로 제한된다
제네릭 타입과 제네릭 메서드는 모두 제네릭을 사용하지만
서로 다른 기능을 제공한다
public static <T extends Object> T genericMethod(T generic) {
return generic
}
// 명시적 타입 인자 전달
Integer result = GenericPractice.<Integer>genericMethod(10);
// 타입 추론
Integer result = GenericPractice.genericMethod(10);
주로 타입 추론으로 사용한다
전달하는 인자로 충분히 타입을 추론할 수 있다
GenericClass<T>
public static <T extends Object> T genericMethod(T generic)
제네릭 타입은 클래스 전체 타입에 제네릭을 도입할 떄 사용
제네릭 메서드는 메서드 단위로 제네릭을 도입할 때 사용
제네릭 메서드는 인스턴스 메서드와 static 메서드 모두 적용 가능하다
제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다
제네릭 타입은 객체 생성 시점에 타입이 정해지지만 static 메서드는 인스턴스 단위가 아니라
클래스 단위로 작동하기 때문에 제네릭 타입과 무관하다
따라서 static 에 제네릭을 도입하려면 제네릭 메서드를 사용한다
제네릭 타입도 타입 매개변수를 제한할 수 있다
제네릭 타입과 제네릭 메서드에서 우선순위는
다른 변수들과 마찬가지로 스코프가 좁은 쪽이 우선순위를 갖는다
따라서 클래스에 적용되는 제네릭 타입보다
메서드에서 적용되는 제네릭 메서드가 우선순위를 갖는다
*, ?
별이나 물음표 같이 특수문자를 사용하여 여러 타입을 받는 타입을 만들 수 있다
static <T> void printGenericV1(Box<T> box) {
sout(box.get());
}
static void printWildCardV1(Box<?> box) {
sout(box.get());
}
static <T exdends Animal> void printGenericV2(Box<T> box) {
T t = box.get();
sout(t.getName());
}
static void printWildCardV2(Box<? extends Animal> box) {
Animal animal = box.get();
sout(animal.getName());
}
static <T exdends Animal> T printReturnGeneric(Box<T> box) {
T t = box.get()
sout(t.getName());
return t;
}
static Animal printReturnWildCard(Box<? extends Animal> box) {
Animal animal = box.get();
sout(animal.getName());
return animal;
}
와일드 카드는 물음표 ?
를 사용해서 정의한다
와일드 카드는 제네릭 타입이나 제네릭 메서드를 사용하는 것이 아니라
이미 만들어진 제네릭 타입을 메게변수로 활용할 수 있는 것이다
와일드카드는 모든 타입을 다 받을 수 있다
특정 타입으로 타입 매개변수 제한을 하지 않은 와일드 카드를
비제한 와일드카드라고 한다
와일드카드도 제네릭과 같이 extends 로 타입 상한을 둘 수 있다
와일드카드는 일반적인 메서드에 사용하여, 단순히 매개변수로 제네릭 타입을 받을 수 있는 것으로
제네릭 메서드처럼 타입을 경정하거나 복잡하게 작동하지 않는다
제네릭으로 타입을 정의하는게 꼭 필요하지 않으면 와일드 카드를 사용하는 편이 좋다
와일드카드는 제네릭을 정의하는 것이 아니기 떄문에 사용할 수 없는 경우가 있다
대체로 와일드카드를 사용하는 것이 좋지만
와일드카드는 입력 받은 타입을 그대로만 반환할 수 있다
타입 상한의 기준이 되는 최상위 타입을 그대로 반환하기 때문에
Animal 타입 상한을 두고 Dog 나 Cat 으로 반환하지 못한다
전달한 인자의 타입을 정확히 반환하고 싶을 떄는 제네릭 메서드를 사용해야만 한다
와일드카드는 타입의 하한도 정할 수 있다 (제네릭 타입이나 제네릭 메서드에는 없는 기능)
static void printWildCardV2(Box<? super Animal> box) {
Animal animal = box.get();
sout(animal.getName());
}
super 키워드를 사용하면 와일드카드가 최소 Animal 타입이 되어야 한다
Animal 의 자식은 받을 수 없지만
Object 등 상위 타입은 받을 수 있다
타입 지우개라는 뜻으로
제네릭은 컴파일 단계에서만 사용되고, 컴파일 이후 제네릭과 관련된 정보가 모두 삭제된다
제네릭에 사용한 타입 매개변수가 컴파일 시 전달된 타입으로 변경되어
컴파일 이후 자바 바이트 코드인 .class 파일에는
모든 제네릭 설정등이 사라지고 매개변수 타입은 Object 로 변경되어 있다
값을 받는 쪽에서는 Object 지만 값을 사용하는 쪽에서 타입 캐스팅을 하여
실제 사용하는 부분에는 문제가 되지 않는다
타입 매개변수를 제한하면 Object 대신에 제한된 상한 타입으로 코드가 변경된다
자바의 제네릭은 개발자가 직접 캐스팅 하는 코드를 컴파일러가 대신 처리해주는 것이다
컴파일 시점에 제네릭이 사용된 코드를 검증하기 때문에 자바 컴파일러가 추가하는 다운 캐스팅에 문제가 발생하지 않는다
컴파일 이후 제네릭의 타입 정보가 존재하지 않는다
따라서 런타임에 타입을 활용하는 다음과 같은 코드는 작성할 수 없다
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(); // 오류 - 항상 Object 인스턴스만 생성하게 됨
}
}
타입 이레이져가 컴파일 타임에 실행되어 제네릭을 지우기 때문에
타입 메개변수에 instanceof 나 new 를 허용하지 않는다
제네릭 타입 사용 시 해당 클래스의 메서드에서
특정 타입을 매개변수로 한 메서드가 데이터 set 및 추가를 한다면
주의해야 한다
제네릭에 설정한 타입의 하위 타입을 매개변수로 한다면
다형성에 의해 값은 추가되겠지만
매개변수의 타입을 정확히 사용할 수 없게 된다
설정 타입의 하위 타입이 아닌 매개변수를 사용해 값을 추가한다면
런타임에 오류가 발생하게 된다