제네릭

고동현·2024년 8월 1일
0

JAVA

목록 보기
20/22

제네릭이 필요한 이유

단순하게 data를 보관하고 꺼내는 class를 만든다고 치자.

Integer를 집어넣고 꺼내는 class이다.

public class IntegerBox {
    private Integer value;
    
    public void set(Integer value){
        this.value = value;
    }
    
    public Integer get(){
        return value;
    }
}

String을 집어넣고 꺼내는 class이다.

public class StringBox {
    private String value;
    public void set(String object){
        this.value=object;
    }
    public String get(){
        return value;
    }
}
public class BoxMain {
    public static void main(String[] args) {
        IntegerBox integerBox = new IntegerBox();
        integerBox.set(19);
        Integer integer = integerBox.get();
        System.out.println("integer = " + integer);

        StringBox stringBox = new StringBox();
        stringBox.set("hello");
        String s = stringBox.get();
        System.out.println("s = " + s);
    }
}

이렇게하면 컴파일 시점에 integerBox.set("string은안돼"); 를 하면 잡을 수 있다.
그러나 BooleanBox,DoubleBox등 계속 타입별로 클래스를 만들어야하는 단점이 있다.

그럼 어떻게 할까? Object는 모든 타입의 부모이므로 다형성을 이용해서 해결해보자.

public class ObjectBox {
    private Object value;
    
    public void set(Object object){
        this.value=object;
    }
    
    public Object get(){
        return value;
    }
}

다형성을 해결하기 위해서 Object를 사용하였다. 모든 타입의 부모가 Object이므로 어떤 타입이던지 받을 수 있다.

public class BoxMain2 {
    public static void main(String[] args) {
        ObjectBox integerBox = new ObjectBox();
        integerBox.set(10);
        Integer integer = (Integer) integerBox.get();
        System.out.println("integer = " + integer);

        ObjectBox stringBox = new ObjectBox();
        integerBox.set("hello");
        String string = (String) integerBox.get();
        System.out.println("string = " + string);

        //잘못된타입
        integerBox.set("문자는 안돼");
        Integer result = (Integer) integerBox.get();//오류발생
    }
}

ObjectBox를 만들고 해당 value 타입으로 바꾸고 싶을때는 다운캐스팅을 진행하면된다.
그러나 마지막처럼 문자를 넣고 Integer 타입으로 캐스팅 시에 오류가 발생할 수 있는데, 이걸 컴파일 시점에 잡지 못한다는것이 문제이다.
즉, Object와 다형성을 활용하면 타입 안전성이 떨어지는 문제가 발생한다.

제네릭사용

해당 문제는 제네릭을 사용하면된다.

public class GenericBox<T> {
    private T value;
    
    public void set(T value){
        this.value = value;
    }
    
    public T get(){
        return value;
    }
}
public class BoxMain3 {
    public static void main(String[] args){
        GenericBox<Integer> integerGenericBox = new GenericBox<>();
        integerGenericBox.set(10);
        Integer integer = integerGenericBox.get();
        System.out.println("integer = " + integer);

        GenericBox<Double> doubleGenericBox = new GenericBox<>();
        doubleGenericBox.set(10.4);
        Double doubleValue = doubleGenericBox.get();
        System.out.println("doubleValue = " + doubleValue);
    }
}

제네릭 클래스를 사용하면 생성시점에 원하는 타입을 지정할 수 있다.
T를 Integer로 지정하면, T가 전부 Integer로 바뀐다.

당연히 이제는 integerBox.set("문자는안돼"); 하면 컴파일 오류로 잡아낼 수 있다.

제네릭을 도입한다고 GenericBox < String>, GenericBox< Integer>와 같은 코드가 생기는게 아니다.
자바 컴파일러가 우리가 입력한 타입정보를 기반으로 이런 코드가 있다고 가정하고 컴파일 과정에 타입 정보를 반영한다.

제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 것이다.
클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 결정하는 것이 아니라, 실제 사용하는 생성 시점에 타입을 결정하는 것이다.

용어

  • GenericBox<T'>에서 T -> 타입 매개변수
  • GenericBox<Integer'>에서 Integer -> 타입 인자

제네릭은 한번에 여러 타입 매개변수를 선언 할 수 있다.
class Data<K,V>{}

타입 인자로 기본형(int,double)은 사용 할 수 없다.

참고
GenericVox integerBox = new GenricBox();
이런식으로 <>를 지정하지 않을 수 있는데 이런것을 로 타입, 원시타이비라고 하며, 내부의 타입 매개변수가 Object가 사용된다.
Object를 사용하면 다운캐스팅을 진행해야하고, 타입 안전성이 떨어지므로 사용하지 않는것이 좋다.

제네릭 예제

동물 인터페이스

public interface Animal {
    public String getName();
    public Integer getSize();

    public void sound();
}
public class Cat implements Animal{
    private String name;

    public Cat(String name, Integer size) {
        this.name = name;
        this.size = size;
    }

    private Integer size;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Integer getSize() {
        return size;
    }

    @Override
    public void sound() {
        System.out.println("야옹");
    }
}
public class Dog implements Animal {
    private String name;
    private Integer size;
    public Dog(String name, Integer size) {
        this.name = name;
        this.size = size;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Integer getSize() {
        return size;
    }

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

제네릭 클래스

public class Box <T>{
    private T value;
    public void setValue(T value){
        this.value = value;
    }

    public T get(){
        return value;
    }
}
public class AnimalMain {
    public static void main(String[] args) {
        Dog dog = new Dog("멍멍이",100);
        Cat cat = new Cat("야옹이",10);

        Box<Dog> dogBox = new Box<>();
        dogBox.setValue(dog);
        Dog findDog = dogBox.get();

        Box<Cat> catBox = new Box<>();
        catBox.setValue(cat);
        Cat findCat = catBox.get();

        Box<Animal> animalBox = new Box<>();
        animalBox.setValue(dog);
        animalBox.setValue(cat);
    }
}

다형성을 활용하였다.
Box<Dog'>, Box<Cat'>을 사용하면 Box의 T가 Dog,Cat으로 바뀌기 때문에
dogBox.setvalue(cat)에서 컴파일 오류로 잡아 낼 수 있다.

인터페이스를 활용하면 Box에 Animal 타입을 넣을 수 있고, 그 하위 자식인 dog,cat을 모두 넣을 수도 있어, 다형성을 활용 할 수도 있다.

제네릭 extends

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 getBigger(Animal target){
        if(animal.getSize()> target.getSize()){
            return animal;
        }
        return target;
    }
}

동물병원에 Animal 필드를 설정하여서 Dog,Cat을 전부 받을 수 있게 했다.

public class AnimalHospitalV2 {
    public static void main(String[] args) {
        AnimalHospitalV1 dogHospital = new AnimalHospitalV1();
        AnimalHospitalV1 catHospital = new AnimalHospitalV1();
        
        Dog dog = new Dog("멍멍이",100);
        Cat cat = new Cat("고양이",50);
        Cat cat2 = new Cat("고양이2",30);
        
        dogHospital.set(dog);
        catHospital.set(cat);
        
        //문제: 개병원에 고양이 전달
        dogHospital.set(cat);
        
        
        //문제2 고양이 타입을 반환,캐스팅 필요
        Cat cat3 = (Cat) catHospital.getBigger(cat2);
    }
}

이렇게 Animal 부모 인터페이스를 이용해 다형성을 활용하면, 앞에서 했던것과 같이 부모 타입으로 받으므로 개 병원에 Cat 타입을 전달하거나, Animal 타입을 반환받으므로 다운캐스팅을 해야한다는 불편한 점이 있다.

제네릭을 배웠으니 제네릭을 활용해보자.

public class AnimalHospitalV3<T> {
    private T animal;
    
    public void set(T animal){
        this.animal = animal;
    }
    
    public void checkup(){
        //animal.getName(); 현재 T라는 타입이므로 getName호출불가
    }
    
    public T getBigger(T target){
//        return animal.getSize() > target.getsize() ? animal : target;
        return null;
    }
}

제네릭을 도입하면 문제가있다.
이전에는 필드를 Animal로 딱 정해놔서 .getName(), .getSize()메서드를 호출 할 수 있었으나,
이제는 T라는 제네릭 타입이므로 정확한 타입을 알지 못하기에 메서드를 호출 할 수 가 없다.

또한,

main메서드에서 제네릭타입이므로

AnimalHospitalV3<Dog>dogHospital = new AnimalHospitalV3<>();
AnimalHospitalV3<Integer>iHospital = new AnimalHospitalV3<>();
AnimalHospitalV3<Double>dHospital = new AnimalHospitalV3<>();

우리가 생각하는 Dog,Cat 뿐만아니라 Integer,Double등의 전혀 관계없는 타입들도 들어갈 수 있다.

해결방안
타입 매개변수를 특정 타입으로 제한 할수 있다.

public class AnimalHospitalV4 <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;
    }
}

<T extends Animal'> 타입 매개변수 T를 Animal과 그 자식만 받을수 있도록 제한을 두는 것이다.
즉 T의 상한이 Animal이 되는것이다.
따라서, Animal이 제공하는 getName(),getSize()같은 기능도 사용할 수 있다.

AnimalHospitalV4<Dog> dogHospital = new AnimalHospitalV4();

dogHospital.set(dog);
//dogHospital.set(cat);-> 컴파일 시점에 오류 잡을 수 있음

Dog findDog = dogHospital.getBigger(dog2);
//개타입을 반환 받을 수 있다. 캐스팅 하지 않아도된다.

타입매개변수에 입력 될수 있는 상한을 지정해서 문제를 해결했다.

  • AnimalHospitalV4<Integer'>와 같이 동물과 전혀 관계없는 타입인자를 컴파일 시점에 막는다.
  • 제네릭 클래스 안에서 Animal의 기능을 사용할 수 있다.
  • 해당 제네릭 타입으로 반환받으므로 캐스팅을 하지 않아도 된다.

제네릭 메서드

특정 메서드에도 제네릭을 사용 할 수 있다.

public class GenericMethod{
	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;
    }
}

제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할때 사용,
핵심은 메서드를 호출하는 시점에 타입인자를 전달해서 타입을 지정하는것이다. 따라서 타입을 지정하면서 메서드를 호출한다.

또한, 제네릭 메서드도 제네릭 타입과 마찬가지로 타입 매개변수를 제한 할 수 있다.

제네릭 메서드는 제네릭 클래스와 별개로 인스턴스 메서드와 static 메서드에 모두 적용할 수 있다.

class Box<T>{
	static <V> V staticMethod2(V t){} // staitc메서드에 제네릭 메서드 도입
    <Z> Z instanceMethod(Z z) {} //인스턴스 메서드에 제네릭 메서드 도입 기능
}

제네릭타입은 static메서드에 타입 매개변수를 사용할 수 없다.
제네릭 타입은 객체를 생성하는 시점에 타입이 정해진다.
그러나, static메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문에 제네릭 타입과는 무관하다.

class Box<T>{
	T instanceMethod(T t){} //가능
    static T staticMethod(T t){} //제네릭 타입 T 사용 불가능
}

제네릭 메서드 타입 추론가능

Integer result1 = GenericMethod.<Integer>genericMethod(10);
Integer result1 = GenericMethod.genericMethod(10);

제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가진다.

public class ComplexBox<T extends Animal>{
	private T animal;
    
	public <T> T method(T t){
    	t.getName();//호출 불가, 해당 메서드는 <T>타입이다. .getName()은 Animal의 메서드 이므로 <T extends Animal>타입이 아니다.
      
    }
}

항상 원래 자세한것이 우선순위를 높게 가진다. 그러므로 제네릭 클래스의 Animal을 상속받은 T가 아니라 메서드 내부에 있는 제네릭 타입 T로 설정이 된다.

이렇게 모호하게 작성할 빠에는

public class ComplexBox<T extends Animal>{
	private T animal;
    
	public <Z> Z method(Z z){
    	//내부 인스턴스 메서드는 다른 제네릭 타입으로 지정
    }
}

제네릭 타입을 다르게 쓰는것이 맞다.

와일드 카드

먼저, 공변과 불공변에 대해서 알아보자

  • 공변: S가 T의 하위타입이면,
    S[]는 T[]의 하위 타입이다.
  • 불공변: S와 T는 서로 관계가 없다.

보기에 다형성의 업캐스팅, 다운캐스팅과 비슷한데 그거 맞다.

제네릭은 불공변이다.
Object타입으로 선언한 parent 변수와 Integer 타입으로 선언한 child변수가 있으면 객체지향 프로그래밍에서는 서로 캐스팅이 가능하다.
Object<-Number<-Integer

그러나 제네릭 타입 파라미터 끼리는 아무리 상속관계에 놓여있다 한들 캐스팅이 불가능하다.
제네릭은 무공변이기 때문이다. 고로, 제네릭은 전달받은 딱 그타입만 사용이 가능하다.

List<Object> parent = new ArrayList<>();
List<Integer> child = new ArrayList<>();

parent = child; //업캐스팅 불가능
child = parent; //다운캐스팅 불가능

//참고: List<T>여기서 T가 제네릭임

그런데 이렇게 공변성이 없으면 문제가 있다.
매개변수로 제네릭을 사용할때, 외부에서 대입되는 인자의 캐스팅문제로 애로사항이 있기 때문이다.

public static void print(Object[] arr){
	for(Object e : arr){
    	System.out.println(e);
    }
}

public static void main(String[] args){
	Integer[] integers = {1,2,3};
    print(integers);
}

Integer가 오던 아님, Dog가 오던, Cat이 오던 print메서드의 파라미터의 타입은 Object이기 때문에 다 받을 수 있다.

그러나, 이걸 리스트의 제네릭 객체로 넘겨보면, 메서드 호출부분에서 에러가 발생한다.

public static void print(List<Object> arr){
	for(Object e : arr){
    	System.out.println(e);
    }
}

public static void main(String[] args){
	List <Integer> integers = {1,2,3};
    print(integers);
}

왜냐하면 리스트 제네릭같은 경우 타입 파라미터가 오로지 똑같은 타입만 받기 때문에 캐스팅이 불가능 하기 때문이다.

그렇다면 외부로부터 값을 받는 매개변수의 제네릭 타입 파라미터를 Integer로 고정된 타입으로 print메서드를 바꿔줘야하는데, 프로그램에서 반드시 Integer만 들어온다는 보장도 없고, Double,Boolean같이 타입마다 메서드를 작성하는것 또한 사실 어려운 일이다.

고로, 자바의 특징이라는 객체 지향을 전혀 이용하지 못하게 되는데, 이를 해결하기위해 나온 기능이 제네릭 와일드 카드인 것이다.

제네릭 와일드 카드

자바 제네릭을 이용하는 클래스 정의문을 보면, 간혹 꺾쇠 괄호 ? 기호가 있는것을 볼 수 가 있다.
이 물음표가 와일드 카드이고, 어떤 타입이든 될 수 있다는것이다.
단순히 <?>로 사용하면 Object와 다를게 없으므로 보통 제네릭 타입 한정 연산자와 함께 쓴다.

  • <?>: 제한 없음 - 모든 타입이 가능
  • <? extends U>: 상위 클래스 제한, U가 상한임
  • <? super U>: 하위 클래스 제한, U가 하한임
class MyArrayList<T>{
	Object[] element = new Object[5];
	int index =0;
	
    //외부로부터 리스트를 받아와 매개변수의 모든 요소를 내부 배열에 추가하여 인스턴스화 하는 생성자.
    public MyArrayList(Collection<T> in){
    	for(T elem : in){
        	element[index++] = elem;
        }
    }
    
    //외부로부터 리스트를 받아와 내부 배열의 요소를 모두 매개변수에 추가해주는 메서드
    public void clone(Collection<T> out){
    	for(Object elem : element){
        	out.add((T) elem);
        }
    }
    
    //배열 요소를 모두 출력
    @Override
    public String toString(){
    	return Arrays.toString(element);
    }
}

와일드 카드를 쓰지 않은 단순 제네릭 코드는 유연하지 않다.

만약 MyArrayList에다가 Integer와 Double을 둘다 넣고싶다고 하자, 그래서 제네릭타입을 상위 타입인 Number로 지정했다고 치자.

public static void main(String [] args){
	//T타입이 Number
    MyArrayList<Number> list;
    
    //MyArrayList 생성
    Collection<Integer> col = Arrays.asList(1,2,3,4,5);
    list = new MyArrayList<>(col); //에러
    
    Collection<Number> col1 = Arrays.asList(1,2,3,4,5);
    list = new MyArrayList<>(col);
    
    List<Object> tmp = new LinkedList<>();
    temp = list.clone(temp);//error
}

T타입이 지정되버리면, Integer 컬렉션을 넣지도 못하고, 해당 리스트에서 반환받아서 Object 리스트에 대입하는것도 안된다.

그럼 어떻게 MyArrayList에다가 Collection<Integer>와 Collection<Double>을 생성자의 인수로 받아 배열에 넣을 수 있을까?
제네릭에 상한 경계 와일드 카드를 적용시키는것이다.

public MyArrayList(Collection<? extends T> in){
	for(T elem : in){
    	element[index ++ ] = elem;
    }
}
public static void main(String [] args){
	//T타입이 Number
    MyArrayList<Number> list;
    
    //MyArrayList 생성
    Collection<Integer> col = Arrays.asList(1,2,3,4,5);
    list = new MyArrayList<>(col);
    
    System.out.println(list);//1,2,3,4,5
}

여기서는 <? extends Number>가 되고, Collection<Integer> col에서 Integer가 ? 에 들어가게되면 Collection<Integer>가 Collection<? extends Number>의 하위타입이 되므로, 공변성이 성립된다.

하한경계 와일드 카드

MyArrayList clone메서드르 설계한 개발자는 MyArrayList의 제네릭 타입 파라미터가 무엇이든 인자로 받은 컬렉션 파라미터에 요소를 모두 집어넣고싶은것이다.

Objet[] element = new Object[5];

public void clone (Collection<? super T> out){
	for(Object elem : elemnet){
    	out.add((T)elem);
    }
}
public static void main(String[] args) {
    // MyArrayList의 제네릭 T 타입은 Number
    MyArrayList<Number> list = new MyArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

    // LinkedList 에 MyArrayList 요소들 복사하기
    List<Object> temp = new LinkedList<>();
    temp = list.clone(temp);

	// LinkedList 출력
    System.out.println(temp); // [1, 2, 3, 4, 5]
}

현재 temp의 제네릭 타입인 Object는 MyArrayList의 제네릭타입인 Number보다 상위 타입이다.
? super T에서 T인 Number의 수퍼타입, 즉, Number타입 또는 Number의 상위타입인 ?를 받을 수 있는데 ?가 Object인 것이다.
즉 Collection<? super T>는 T타입의 사위타입을 받을수있는 컬렉션인 것이다.

즉, 인스턴스화 된 클래스의 제네릭보다 상위 타입의 데이터를 적재해야할때 사용한다고 이해하면된다.

비한정적 와일드 카드

제네릭에 extends,super의 범위를 따지지 않고 <?> 비한정적 와일드 카드로만 구성한다면 어떻게될까?
어떠한 타입도 올 수 있으니까 치트키 일것 같지만, 동시에 어떠한 타입도 올 수 있다는 문제때문에 매개변수를 꺼내거나 저장하는 로직은 논리적 에러가 발생한다.

public MyArrayList<Collection<?> in>{
	//매개변수 in의 서브타입이 ? 이므로,
    원소를 꺼낼때 어떤 타입인지 알수가 없어서 논리 에러 발생
    만약 MyArrayList의 T타입이 Number이고 ?가 Object라면 컴파일 오류발생
	for(T elem : in){
    	element[index++]=elem;
    }
}

public void clone(Collection<?> out){
	//매개변수 out의 서브타입이 ?이므로
    어떤 원소를 저장할 수 있는지 알수없어, 논리 에러발생
    Collection<String> out이 파라미터로 넘어왔다면, T가 Number인경우에, Ojbect를 Number로 캐스팅을 해서 String 컬렉션에 넣어야하는데 이는 컴파일 오류가 발생할 수 있다.
    for(Object elem : element){
    	out.add((T) elem);
    }
}

와일드 카드 경계

하한경계

상한경계

비경계

와일드 카드 경계 꺼내기,넣기

List<? extends U>
꺼낸 타입은 U,저장은 NO

class FruitBox{
	public static void method(List<? extends Fruit> item){
    	//안전하게 꺼내려면 Fruit타입으로만 받아야함
        Fruit f1 = item.get(0);
        
        Apple f2 = (Apple) item.get(0);//오류 발생가능
        Banana f3 = (Banana) item.get(0);//오류 발생가능
    }
}

꺼내는걸 왜 Fruit타입으로 꺼내야하면,
만약 매개변수에 List<Banana>타입으로 들어오면 f2에 형제 캐스팅이 불가능하다.

class FruitBox{
	public static void method(List<? extends Fruit> item){
    	//저장은 NO
        item.add(new Fruit());
        item.add(new Apple());
    }
}

만약 매개변수에 List<Banana>로 들어온경우 형제 타입으로 new Apple()을 넣을 수 없다.
또한 바나나 타입인데 new Fruit를 하려하면 바나나 타입만 들어갈수있으므로 추가가 불가능하다.
따라서, 매개변수에 값을 넣고 싶다면 무조건 super를 써야한다.

List<? super U>
꺼낸 타입은 Object / 저장은 U와 그 자식만

class FruitBox{
	public static void method(List<? super Fruit> item) {
        // 안전하게 꺼내려면 Object 타입으로만 받아야한다
        Object f1 = item.get(0);

        Food f2 = (Food) item.get(0); // ! 잠재적 ERROR
        Fruit f3 = (Fruit) item.get(0); // ! 잠재적 ERROR
        Apple f4 = (Apple) item.get(0); // ! 잠재적 ERROR
        Banana f5 = (Banana) item.get(0); // ! 잠재적 ERROR
    }
}

매개변수에 List FOOD가 들어간경우 item.get(0)을하면 Fruit타입의 부모인 Food가 나오게 되고 지금 다운캐스팅을 진행하려 하므로, classcastException이 터질 수 있다.
고로 와일드 카드 최상위 범위인 Object타입으로만 꺼낸는게 안전하다.

class FruitBox{
	static void method(List<? super Fruit> item){
    	//저장은 무조건 Fruit와 그 자손 타입만 넣을 수 있다.
        item.add(new Fruit());
        item.add(new Apple());
        item.add(new Banana());
        
        item.add(new Object());//에러
        item.add(new Food());//에러
    }
}

저장하는데는 Fruit와 그 자손타입만 넣을 수 있다.
매개변수로 Fruit타입이 올경우 new Food를 저장 할 수 없다.
따라서 어떠한 타입이 와도 업 캐스팅이 가능한 Fruit타입으로만 제한된다.

List<?>
꺼낸 타입은 Object/저장은 NO
모든 타입이 들어갈 수 있으므로 꺼낼때는 최상위인 Object로 List에서 꺼내고,
어떤 타입이 들어갈지 모르니까 다운캐스팅이 불가능하므로 저장은 하지 않는다.

제네릭과 와일드카드 언제 어떤걸 써야할까?

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;
}

printAndReutrnGeneric은 전달한 타입을 명확하게 반환할 수 있다.

Dog dog = WhildcardEx.printAndReturnGeneric(dogBox);

printAndReturnWildcard는 전달한 타입을 명확하게 반환할 수 없다. Animal타입으로 반환한다.

Animal animal = WhildcardEx.printAndReturnWildcard(dogBox);

메서드 타입을 특정 시점에 변경하기 위해서는 제네릭 타입이나, 제네릭 메서드를 사용해야한다.
와일드 카드는 이미 만들어진 제네릭 타입을 전달받아서 활용할 때 사용한다.
따라서 메서드의 타입 인자를 통해 변경 할 수 없다.

정리하자면, 제네릭타입이나 제네릭 메서드가 꼭 필요한 상황이면(해당 타입으로 반환을 받아야한다던지) <T>를 사용하고, 그렇지않으면 와일드카드를 사용하면된다.

타입이레이저.

제네릭은 자바 컴파일 시점에만 사용되고, 컴파일 이후에는 제네릭 정보가 사라진다.
전부 Object로 바뀌는데, 자바가 나중에 캐스팅을 해준다.

public class GenericBox<T>{
	private T value;
    
    public void set(T value){
    	this.value = value;
    }
    
    public T get(){
    	return value;
    }
}

만약 해당 GenericBox에 Integer타입을 전달하면, 자바 컴파일 시점에 컴파일러가

public class GenericBox<Integer>{
	private Integer value;
    
    public void set(Integer value){
    	this.value = value;
    }
    
    public Integer get(){
    	return value;
    }
}

이렇게 이해하고, 컴파일 끝나면 제네릭 정보를 삭제한다. 이때 .class에 생성된 정보는 Object로 바뀌게 된다.

public class GenericBox<Object>{
	private Object value;
    
    public void set(Object value){
    	this.value = value;
    }
    
    public Object get(){
    	return value;
    }
}
Integer result = (Integer) box.get(); //컴파일러가 캐스팅을 추가

box.get을 할경우에는 integer로 받아야하는데 Object로 .class에 생성되므로 이미 자바가 컴파일 시점에 오류가 없음을 확인했으므로 Integer로 받아도 되니까 캐스팅을 해준다.

고로, .class로 자바가 실행하는 런타임 시점에서는 우리가 설정한 Box<Integer>, Box<String>과 같은 정보가 모두 제거된다.

고로, 런타임때는 타입을 활용하는 new, instance of는 사용될수 없다.

class EraserBox<T>{
	public boolean instanceCheck(Object param){
    	return param instanceof T;//오류
        //런타임때 return param instanceof Object로 바뀜 항상 참이므로 논리적 오류
    }
    
    public void create(){
    	return new T();//오류
        //런타임때 항상 new Object()가 됨, 타입 매개변수에 new를 허용하지 않음
    }
}
profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글