[14주차] 제네릭

janjanee·2022년 8월 1일
0
post-thumbnail

2021.02.22 작성글 이전

14. 제네릭

학습 목표 : 자바의 제네릭에 대해 학습하세요.

14-0. 제네릭?

다양한 타입의 객체들을 다루는 메소드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능

  • 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

  • 타입 안정성을 높인다는 것은 의도하지 않은 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의

    타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄어준 다는 뜻이다.

14-1. 제네릭 사용법

클래스의 선언

class Box {
    Object item;

    void setItem(Object item) { this.item = item; }
    Object getItem() { return item; }
}

Box라는 클래스가 있는데 이 클래스를 제네릭 클래스로 변경해보자.

class Box<T> {  // 제네릭 타입 T 선언
    T item;

    void setItem(T item) { this.item = item; }
    T getItem() { return item; }
}

클래스 옆에 <T>를 붙이면 된다.
그리고 'Object' 를 모두 'T' 로 변경한다.

  • T'타입 변수(type variable)' 라고 하며, 'Type'의 첫 글자에서 따온 것이다.
  • 꼭 T일 필요는 없다. ArrayList<E> 인 경우, 타입 변수 E는 'Element(요소)'의 첫 글자를 사용한 예이다.
  • 타입 변수가 여러개라면 Map<K, V>과 같이 콤마를 구분자로 사용한다.
    • K는 Key를 의미하고 V는 Value를 의미하는 것과 같이 무조건 T를 사용하기 보다는 상황에 맞는 의미있는 문자를 사용하자.

기호의 종류만 다를 뿐 모두 '임의의 참조형 타입' 을 의미한다.

Box<String> b = new Box<String>();  // 타입 T 대신 실제 타입 지정
b.setItem(new Object());            // Error! String 이외의 타입은 지정불가
b.setItem("Hello~~");               // OK. String 타입이므로 가능
String item = b.getItem();          // 형변환 필요없음

제네릭 타입 T대신 String을 지정해서 사용하는 예제이다.
T 대신 Box<String> String 타입을 지정했기 떄문에, 제네릭 클래스 Box<T>
아래와 같이 정의된 것이다.

class Box {
    String item;

    void setItem(String item) { this.item = item; }
    String getItem() { return item; }
}

다시한번 제네릭의 용어를 먼저 정리하고 넘어가자.

class Box<T> {}
  • Box<T>
    • 제네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다.
  • T
    • 타입 변수 또는 타입 매개변수(T는 타입 문자)
  • Box
    • 원시 타입(raw type)
Box<String> b = new Box<String>();
  • Box<String>
    • 타입 변수에 타입을 지정하는 것을 지네릭 타입 호출 이라 한다.
  • String
    • 지정된 타입 String을 대입된 타입 or 매개변수화된 타입이라고 한다.

제네릭 제한

class Box<T> {
    static T item;  // Error!
    static int compare(T t1, T t2) { ... }  // Error!
}
  • 모든 객체에 대해 동일하게 동작해야하는 static 멤버에 타입 변수 T를 사용할 수 없다.
class Box<T> {
    T[] itemArr;        // OK. T타입의 배열을 위한 참조변수
    ...
    T[] toArray() {
        T[] tmpArr = new T[itemArr.length];     // Error! 제네릭 배열 생성불가
        return tepArr;
    }
}
  • 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하다.

  • new T[10]과 같이 배열을 생성하는 것은 에러이다.

    • new 연산자는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 한다. 위 코드에서 Box 클래스

      컴파일 시점에 new T[10]의 T가 어떤 타입일지 알 수 없기 때문에 에러발생.

    • instanceof 연산자도 new와 동일한 이유로 T를 피연산자로 사용할 수 없다.

  • 꼭 제네릭 배열 생성이 필요하다면, 리플렉션의 newInstatnce() 와 같이 동적으로 객체 생성하는 메소드,

    Object 배열을 생성해서 복사한 다음에 'T[]' 로 형변환하는 방법 등을 사용.

제네릭 클래스 - 객체 생성과 사용

Box<Apple> appleBox = new Box<Apple>();     // OK.
Box<Apple> appleBox = new Box<Kiwi>();     // ERROR!
  • 참조변수와 생성자에 대입된 타입이 일치해야 한다. 일치하지 않으면 에러 발생!
Box<Fruit> appleBox = new Box<Apple>();     // ERROR! 대입된 타입이 다름
  • 두 타입이 상속관계(Apple의 부모가 Fruit)에 있더라도 타입이 일치하지 않으므로 에러!
Box<Apple> appleBox = new FruitBox<Apple>();    // OK. 다형성
  • 두 제네릭 클래스 타입이 상속관계(FruitBox의 부모가 Box)에 있고, 대입된 타입이 같다면 괜찮다.
Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<>();          // OK. 위와 동일. Java 7부터 생략가능
  • JDK 1.7부터는 추정이 가능한 경우 타입 생략이 가능해졌다.
Box<Fruit> fruitBox = new Box<>();
fruitBox.add(new Fruit());              // OK.
fruitBox.add(new Apple());              // OK.
  • Apple이 Fruit의 자손일 때 'void add(T item)' 메소드의 매개변수로 자손인 Apple도 가능하다.
class Fruit {
    @Override
    public String toString() {
        return "Fruit";
    }
}

class Apple extends Fruit {
    @Override
    public String toString() {
        return "Apple";
    }
}

class Grape extends Fruit {
    @Override
    public String toString() {
        return "Grape";
    }
}

class Toy {
    @Override
    public String toString() {
        return "Toy";
    }
}

class Box<T> {
    ArrayList<T> list = new ArrayList<>();
    void add(T item) { list.add(item); }
    T get (int i) { return list.get(i); }
    int size()  { return list.size(); }

    @Override
    public String toString() {
        return list.toString();
    }
}

public class FruitBoxEx1 {
    public static void main(String[] args) {
        Box<Fruit> fruitBox = new Box<>();
        Box<Apple> appleBox = new Box<>();
        Box<Toy> toyBox = new Box<>();

        fruitBox.add(new Fruit());
        fruitBox.add(new Apple());

        appleBox.add(new Apple());
        appleBox.add(new Apple());

        toyBox.add(new Toy());

        System.out.println(fruitBox);
        System.out.println(appleBox);
        System.out.println(toyBox);

    }
}

위에서 배운 개념을 이용하여 제네릭 클래스 생성 및 사용 예제를 작성한 것이다.

14-2. 제네릭 주요 개념 (바운디드 타입, 와일드 카드)

제한된 제네릭 클래스

class FruitBox<T extends Fruit> {   // Fruit의 자손만 타입으로 지정가능
    ArrayList<T> list = new ArrayList<T>();
    ...
}
  • 제네릭 타입에 'extends'를 사용하면 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
FruitBox<Apple> appleBox = new FruitBox<>();    // OK.
FruitBox<Toy> toyBox = new FruitBox<>();        // ERROR!   Toy는 Fruit의 자손이 아님
  • Toy는 Fruit의 자손이 아니므로 에러가 발생한다.
interface Eatable {}
class FruitBox<T extends Eatable> { ... }
  • 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면,

    이때도 'implements'가 아닌 'extends'를 사용한다.

class FruitBox<T extends Fruit & Eatable> { ... }
  • 클래스 Fruit의 자손이면서 Eatable 인터페이스도 구현해야 한다면 위와 같이 '&' 기호로 연결이 가능하다.
public class FruitBox <T extends Fruit & Eatable> extends Box<T> {}

public class Fruit implements Eatable {
    @Override
    public String toString() {
        return "Fruit";
    }
}

public interface Eatable {}

public class FruitBoxEx2 {

    public static void main(String[] args) {
        FruitBox<Fruit> fruitBox = new FruitBox<>();
        FruitBox<Apple> appleBox = new FruitBox<>();
        FruitBox<Grape> grapeBox = new FruitBox<>();
//        FruitBox<Toy> toyBox = new FruitBox<Toy>();     // ERROR.

        fruitBox.add(new Fruit());
        fruitBox.add(new Apple());
        appleBox.add(new Apple());
//        appleBox.add(new Grape());  // ERROR.
        grapeBox.add(new Grape());

        System.out.println("fruit-box - " + fruitBox);
        System.out.println("apple-box - " + appleBox);
        System.out.println("grape-box - " + grapeBox);
    }

}

처음 만든 예제의 Fruit 클래스를 조금 수정하고 FruitBox 클래스와 Eatable 인터페이스를 생성 후
위에서 배운 제한된 제네릭 클래스에 대해 테스트하는 예제이다.

와일드 카드

class Juicer {
    static Juice makeJuice(FruitBox<Fruit> box) {
        String tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }
}
  • 매개변수에 과일박스를 대입하면 주스를 만들어서 반환하는 Juicer라는 클래스가 있다.
  • 이 클래스에 과일을 주스로 만들어서 반환하는 makeJuice()라는 static 메소드가 정의되어 있다.
  • Juicer 클래스는 제네릭 클래스도 아니고, static 메소드에는 타입 매개변수 T를 사용할 수 없다.
    • 제네릭을 적용하지 않던가, makeJuice(FruitBox<Fruit>) 처럼 특정 타입을 지정해야 한다.

그런데, 'FruitBox<Fruit>' 처럼 고정하면, 'FruitBox<Apple>' 타입의 객체는
makeJuice()의 매개변수가 될 수 없다.

static Juice makeJuice(FruitBox<Fruit> box) {
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

static Juice makeJuice(FruitBox<Apple> box) {
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

그럼 위와같이 여러 타입의 매개변수를 갖는 makeJuice()를 여러개 오버로딩 하면될까?
-> 컴파일 에러가 발생한다.

💡 제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않음

그래서 위의 결과는 메소드 오버로딩이 아니라, '메소드 중복 정의'가 된다.

이 문제를 해결하기 위해 와일드 카드 '?' 를 사용할 수 있다.

<? extends T> 와일드 카드의 상한 제한, T와 그 자손들만 가능
<? super T> 와일드 카드의 하한 제한, T와 그 조상들만 가능
<?> 제한 없음. 모든 타입이 가능. <? extends Object>와 동일

💡 제네릭 클래스와 달리 와일드 카드에는 '&'를 사용하지 못함. <? extends T & E> 이런 표현이 안됨.

와일드 카드를 사용해서 위의 문제가 있는 부분을 해결해보자.

static Juice makeJuice(FruitBox<? extends Fruit> box) {
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}
  • 이제 이 메소드의 매개변수로 FruitBox<Fruit>, FruitBox<Apple>, FruitBox<Grape>를 사용할 수 있다.
  • extends를 사용해서 Fruit와 그 자손들이 올 수 있다.
public class Juice {

    String name;

    Juice(String name) {
        this.name = name + "Juice";
    }

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

public class Juicer {

    static Juice makeJuice(FruitBox<? extends Fruit> box) {
        String tmp = "";

        for (Fruit f : box.getList()) {
            tmp += f + " ";
        }
        return new Juice(tmp);
    }

}

public class FruitBoxEx3 {
    public static void main(String[] args) {
        FruitBox<Fruit> fruitBox = new FruitBox<>();
        FruitBox<Apple> appleBox = new FruitBox<>();

        fruitBox.add(new Apple());
        fruitBox.add(new Grape());
        appleBox.add(new Apple());
        appleBox.add(new Apple());

        System.out.println(Juicer.makeJuice(fruitBox));
        System.out.println(Juicer.makeJuice(appleBox));
    }
}
// 결과

Apple Grape Juice
Apple Apple Juice

위의 설명에 나온 예제들을 테스트한 결과이다.
Fruit<Apple>도 makeJuice의 매개변수로 들어가서 잘 동작하는 것을 볼 수 있다.

다음은 'super'로 와일드카드를 제한한 경우의 예제이다.

public class Fruit {

    String name;
    int weight;

    public Fruit () {}

    public Fruit(String name, int weight) {
        this.name = name;
        this.weight = weight;
    }

    @Override
    public String toString() {
        return name + "(" + weight + ")";
    }
}

public class Apple extends Fruit {

    public Apple() {
    }

    public Apple(String name, int weight) {
        super(name, weight);
    }

}

public class Grape extends Fruit {

    public Grape() {
    }

    public Grape(String name, int weight) {
        super(name, weight);
    }

}

public class AppleComp implements Comparator<Apple> {

    @Override
    public int compare(Apple a1, Apple a2) {
        return a2.weight - a1.weight;
    }
}

public class GrapeComp implements Comparator<Grape> {

    @Override
    public int compare(Grape g1, Grape g2) {
        return g2.weight - g1.weight;
    }
}

public class FruitComp implements Comparator<Fruit> {

    @Override
    public int compare(Fruit f1, Fruit f2) {
        return f1.weight - f2.weight;
    }
}

public class FruitBoxEx4 {
    public static void main(String[] args) {

        FruitBox<Apple> appleBox = new FruitBox<>();
        FruitBox<Grape> grapeBox = new FruitBox<>();

        appleBox.add(new Apple("GreenApple", 300));
        appleBox.add(new Apple("GreenApple", 100));
        appleBox.add(new Apple("RedApple", 200));

        grapeBox.add(new Grape("GreenGrape", 400));
        grapeBox.add(new Grape("GreenGrape", 300));
        grapeBox.add(new Grape("RedGrape", 200));

        Collections.sort(appleBox.getList(), new AppleComp());
        Collections.sort(grapeBox.getList(), new GrapeComp());

        System.out.println(appleBox);
        System.out.println(grapeBox);

        System.out.println();

        Collections.sort(appleBox.getList(), new FruitComp());
        Collections.sort(grapeBox.getList(), new FruitComp());
        System.out.println(appleBox);
        System.out.println(grapeBox);
    }
}
// 결과

[GreenApple(300), RedApple(200), GreenApple(100)]
[GreenGrape(400), GreenGrape(300), RedGrape(200)]

[GreenApple(100), RedApple(200), GreenApple(300)]
[RedGrape(200), GreenGrape(300), GreenGrape(400)]

Collections.sort()를 이용해서 appleBox와 grapeBox에 담긴 과일을 무게별로 정렬하는 예제이다.

sort() 메소드를 열어보면 아래와 같이 생겼다.

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}
  • 첫 번째 매개변수는 정렬할 대상

  • 두 번째 매개변수는 정렬할 방법이 정의된 Comparator 인데, 제네릭 타입에 하한 제한(super)

    걸려있는 와일드 카드가 사용되었다.

만약, sort()에서 와일드 카드를 사용하지 않았다고 가정해보자.

public static <T> void sort(List<T> list, Comparator<T> c) {
    list.sort(c);
}

그리고, 타입 매개변수 T에 Apple이 대입되면, 위의 정의는 아래와 같이 바뀐다.

public static void sort(List<Apple> list, Comparator<Apple> c) {
    list.sort();
}

따라서, List<Apple>을 정렬하고 싶다면 Comparator<Apple>이 필요하다.

여기까지는 별 문제가 없는데, List<Grape>를 정렬하고 싶어진다면?
-> Comparator<Apple>로는 List<Grape>를 정렬할 수 없어서 Comparator<Grape>가 필요하다.

그런데 위의 작성된 코드를 보면 Comparator<Apple>Comparator<Grape>의 코드는 동일하다.
타입만 다를 뿐 완전히 똑같다! 코드의 중복이 발생하고 중복을 넘어서 새로운 List<Kiwi>같은
Fruit의 자손이 생길 때 마다 Comparator<Kiwi>같은 코드를 새로 만들어야하는 문제가 발생한다.

이 문제를 해결하기 위해 sort()가 하한 제한(super)의 와일드 카드를 사용 하고 있는 것이다.

public static <T> void sort(List<Apple> list, Comparator<? super Apple> c) {
    list.sort(c);
}

원래 정의되어있는 sort()에 타입으로 Apple이 대입되면 위와 같다.

  • 매개변수 타입이 Comparator<? super Apple> 이니까 Apple과 그 조상들이 가능한 것이다.
    • Comparator<Apple>
    • Comparator<Fruit>
    • Comparator<Object>
  • Comparator<? super Grape>도 마찬가지이다.
    • Comparator<Grape>
    • Comparator<Fruit>
    • Comparator<Object>

이제 문제를 해결해보자.
예제 코드를 보면 FruitComp를 만들어서 List<Apple>List<Grape>
동시에 정렬한 코드를 확인 할 수 있다.

Collections.sort(appleBox.getList(), new FruitComp());
Collections.sort(grapeBox.getList(), new FruitComp());

14-3. 제네릭 메소드 만들기

제네릭 메소드?
메소드의 선언부에 제네릭 타입이 선언된 메소드

class FruitBox<T> {
    static <T> void sort(List<T> list, Comparartor<? super T> c) {
        ...
    }
}
  • 코드의 sort()가 제네릭 메소드이다. 제네릭 타입의 선언 위치반환 타입 바로 앞이다.
  • 제네릭 클래스에 정의된 타입 매개변수와 제네릭 메소드에 정의된 타입 매개변수는 전혀 별개다.
    • 코드의 FrutBox<T>static <T> void sort() 의 T는 문자만 같을 뿐 다르다.
  • static 멤버에 타입 매개변수를 사용할 수 없지만, 메소드에는 제네릭 타입을 선언하고 사용하는 것은 가능
  • 메소드에 선언된 제네릭 타입은 지역 변수를 선언한 것과 비슷하다.
    • 이 타입 매개변수가 메소드 내에서만 지역적으로 사용될 것이기 때문에 static이어도 무관
  • 제네릭 메소드는 제네릭 클래스가 아닌 클래스에서도 정의될 수 있다.
// 이전 코드
static Juice makeJuice(FruitBox<? extends Fruit> box) {
    ...
}

// 제네릭 메소드 변경
static <T extends Fruit> Juice makeJuice(FruitBox<? extends T> box) {
    ...
}

앞에 등장했던 makeJuice()를 제네릭 메소드로 변경한 코드이다.

위 메소드를 호출할 때는 아래와 같이 타입 변수에 타입을 대입해야한다.

FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();

System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));

그러나, 대부분 컴파일러가 타입 추정을 할 수 있어서 아래와 같이 생략해도 된다.

System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));

주의할 점은 제네릭 메소드를 호출 할 때, 대입된 타입을 생략할 수 없는 경우에는
아래와 같이 참조변수나 클래스 이름을 생략할 수 없다.

System.out.println(<Fruit>.makeJuice(fruitBox));    // ERROR. 클래스 이름 생략불가
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));     // OK.
System.out.println(this.<Fruit>makeJuice(fruitBox));     // OK.
  • 같은 클래스 내에 있는 멤버들끼리는 참조변수나 클래스 이름, 즉 'this', '클래스이름'

    생략하고 메소드 명으로만 호출이 가능하나, 대입된 타입이 있을 때는 반드시 써줘야 한다.

제네릭 메소드는 매개변수의 타입이 복잡할 때도 유용하게 쓰일 수 있다.

// 이전 코드
public static void printAll(ArrayList<? extends Product> list, 
                            ArrayList<? extends Produce> list2) {
    ...
}

// 제네릭 메소드로 변경
public static <T extends Product> void printAll(ArrayList<T> list, 
                            ArrayList<T> list2) {
    ...
}
  • 타입을 별도로 선언함으로써 코드를 간략하게 할 수 있다.
public static <T extends Comparable<? super T>> void sort(List<T> list)
  • 위에서 봤던 sort()와 달리 매개변수가 하나짜리인 sort() 제네릭 메소드이다.
  • 위의 제네릭 메소드를 분석하면 다음과 같다.
    • 타입 T를 매개변수로 하는 List를 매개변수로 허용한다.
    • 'T'는 Comparable을 구현한 클래스 이어야 하며(<T extends Comparable>)
    • 'T' 또는 그 조상의 타입을 비교하는 Comparable이어야 한다(Comparable<? super T>)
public class Kiwi extends Fruit implements Comparable<Kiwi> {

    public Kiwi() {
    }

    public Kiwi(String name, int weight) {
        super(name, weight);
    }

    @Override
    public int compareTo(Kiwi o) {
        return o.weight - this.weight;
    }
}

public class FruitBoxEx4 {
    public static void main(String[] args) {

        FruitBox<Kiwi> kiwiBox = new FruitBox<>();

        kiwiBox.add(new Kiwi("Kiwi", 400));
        kiwiBox.add(new Kiwi("Kiwi", 500));
        kiwiBox.add(new Kiwi("GoleKiwi", 200));

        Collections.sort(kiwiBox.getList());
        System.out.println(kiwiBox);

    }
}
// 결과
[Kiwi(500), Kiwi(400), GoleKiwi(200)]

위의 개념을 적용한 예제이다.

  • Kiwi 클래스가 T에 대입될 클래스이다.

  • Kiwi 클래스는 Comparable을 구현하고, Comparable은 Kiwi 또는 그 조상이 가능하므로

    Kiwi로 지정하였다.

14-4. 제네릭 형변환

제네릭 타입과 원시 타입(raw type)간 형변환이 가능할까?

Box box = null;
Box<Object> objectBox = null;

box = (Box)objectBox;           // OK. 제네릭 타입 -> 원시타입. 경고발생
objectBox = (Box<Object>)box;   // OK. 원시 타입 -> 제네릭 타입. 경고발생

제네릭 타입과 넌제네릭(non-generic) 타입은 경고가 발생할 뿐 형변환이 가능하다.

Box<Object> objectBox = null;
Box<String > stringBox = null;

objectBox = (Box<Object>)stringBox;     // ERROR. Box<String> -> Box<Object>
stringBox = (Box<String>)objectBox;     // ERROR. Box<Object> -> Box<String>

대입된 타입이 다른 제네릭 타입 간에는 형변환이 불가능하다.

  • 대입된 타입이 Object 일지라도 불가능

불가능 하다는 사실은 이미 14-1 파트에서 자연스럽게 알 수 있었던 사실이다.

Box<Object> objectBox = new Box<String>();      // ERROR.

위 코드가 에러를 발생한 다는 것을 배웠기 때문이다.

그렇다면 다음의 코드는 어떨까?

Box<? extends Object> wBox = new Box<String>();     // OK.

형변환이 된다.

static Juice makeJuice(FruitBox<? extends Fruit> box) { ... }

FruitBox<? extends Fruit> box = new FruitBox<Fruit>();  // OK
FruitBox<? extends Fruit> box = new FruitBox<Apple>();  // OK
FruitBox<? extends Fruit> box = new FruitBox<Grape>();  // OK

그래서 아까 등장했던 makeJuice 메소드의 매개변수에 다형성이 적용될 수 있었던 것이다.

FruitBox<? extends Fruit> box = null;
FruitBox<Apple> appleBox = (FruitBox<Apple>)box;

위 코드처럼 반대로 형변환이 가능하긴 하지만, 확인되지 않은 형변환이라는 경고가 발생한다.

  • FruitBox<? extends Fruit>에 대입될 수 있는 타입이 여러개이고,
  • FruitBox<Apple>을 제외한 다른 타입은 FruitBox<Apple>로 형변환 될 수 없기 때문이다.
public final class Optional<T> {
    /**
     * Common instance for {@code empty()}.
     */
    private static final Optional<?> EMPTY = new Optional<>(null);

    ...

    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }
    ...
}

조금 더 실질적인 예를 살펴보기 위해 java.util.Optional 클래스의 소스 일부분을 확인해보자.
위의 코드에서 EMPTY의 타입을 Optional<Object>가 아닌 Optional<?>로 한 이유는
Optional<T>로 형변환 이 가능하기 때문이다.

Optional<?> wopt = new Optional<Object>();
Optional<Object> oopt = new Optional<Object>();

Optional<String> sopt1 = (Optional<String>)wopt;    // OK. 형변환 가능
Optional<String> sopt2 = (Optional<String>)oopt;    // ERROR! 형변환 불가
  • empty() 메소드의 반환 타입이 Optional<T> 이므로 EMPTY를 형변환 할 수 있도록

    Optional<?> 와일드 카드가 포함된 제네릭 타입을 사용한 것이다.

14-5. Erasure

컴파일러는 제네릭 타입을 이용하여 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다.
그리고, 제네릭 타입을 제거한다.

  • 컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없다.

이렇게 하는 이유는 제네릭이 도입되기 이전의 소스코드와의 호환성을 유지하기 위해서이다.

1️⃣. 제네릭 타입의 경계(bound)를 제거

  • 제네릭 타입이 <T extends Fruit>라면 T -> Fruit로 치환된다.
  • <T> 인 경우 T -> Object로 치환된다.
  • 클래스 옆의 선언은 제거된다.
// 변경 전
class Box<T extends Fruit> {
    void add(T t) {
        ...
    }
}

// 변경 후
class Box {
    void add(Fruit t) {
        ...
    }
}

2️⃣. 제네릭 타입을 제거 후 타입이 일치하지 않으면, 형변환을 추가

List의 get()은 Object 타입을 반환하므로 형변환이 필요하다.

// 변경 전
T get(int i) {
    return list.get();
}

// 변경 후
Fruit get(int i) {
    return (Fruit)list.get(i);
}

와일드 카드가 포함되어 있는 경우에는 적절한 타입으로 형변환이 추가된다.

// 변경 전
static Juice makeJuice(FruitBox<? extends Fruit> box) {
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

// 변경 후
static Juice makeJuice(FruitBox box) {
    String tmp = "";
    Iterator it = box.getList().iterator();
    while(it.hasNext()) {
        tmp += (Fruit)it.next() + " ";
    }
    return new Juice(tmp);
}

bytecode로 확인

제네릭 타입 제거 테스트를 위해 Box 제네릭 클래스를 이용할 것이다.

public class Box<T extends Fruit> {

    ArrayList<T> list = new ArrayList<>();
    void add(T item) { list.add(item); }
    T get (int i) { return list.get(i); }
    ...
}

위에서 작성한 Box 클래스인데 bytecode를 열어서 타입 매개변수 T가 어떻게 되었는지 확인해보자.

// class version 58.0 (58)
// access flags 0x21
// signature <T:Lcom/jihan/javastudycode/week14/Fruit;>Ljava/lang/Object;
// declaration: com/jihan/javastudycode/week14/Box<T extends com.jihan.javastudycode.week14.Fruit>
public class com/jihan/javastudycode/week14/Box {

  // compiled from: Box.java

  // access flags 0x0
  // signature Ljava/util/ArrayList<TT;>;
  // declaration: list extends java.util.ArrayList<T>
  Ljava/util/ArrayList; list

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 6 L1
    ALOAD 0
    NEW java/util/ArrayList
    DUP
    INVOKESPECIAL java/util/ArrayList.<init> ()V
    PUTFIELD com/jihan/javastudycode/week14/Box.list : Ljava/util/ArrayList;
    RETURN
   L2
    LOCALVARIABLE this Lcom/jihan/javastudycode/week14/Box; L0 L2 0
    // signature Lcom/jihan/javastudycode/week14/Box<TT;>;
    // declaration: this extends com.jihan.javastudycode.week14.Box<T>
    MAXSTACK = 3
    MAXLOCALS = 1

  // access flags 0x0
  // signature (TT;)V
  // declaration: void add(T)
  add(Lcom/jihan/javastudycode/week14/Fruit;)V
    // parameter  item
   L0
    LINENUMBER 7 L0
    ALOAD 0
    GETFIELD com/jihan/javastudycode/week14/Box.list : Ljava/util/ArrayList;
    ALOAD 1
    INVOKEVIRTUAL java/util/ArrayList.add (Ljava/lang/Object;)Z
    POP
    RETURN
   L1
    LOCALVARIABLE this Lcom/jihan/javastudycode/week14/Box; L0 L1 0
    // signature Lcom/jihan/javastudycode/week14/Box<TT;>;
    // declaration: this extends com.jihan.javastudycode.week14.Box<T>
    LOCALVARIABLE item Lcom/jihan/javastudycode/week14/Fruit; L0 L1 1
    // signature TT;
    // declaration: item extends T
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x0
  // signature (I)TT;
  // declaration: T get(int)
  get(I)Lcom/jihan/javastudycode/week14/Fruit;
    // parameter  i
   L0
    LINENUMBER 8 L0
    ALOAD 0
    GETFIELD com/jihan/javastudycode/week14/Box.list : Ljava/util/ArrayList;
    ILOAD 1
    INVOKEVIRTUAL java/util/ArrayList.get (I)Ljava/lang/Object;
    CHECKCAST com/jihan/javastudycode/week14/Fruit
    ARETURN
   L1
    LOCALVARIABLE this Lcom/jihan/javastudycode/week14/Box; L0 L1 0
    // signature Lcom/jihan/javastudycode/week14/Box<TT;>;
    // declaration: this extends com.jihan.javastudycode.week14.Box<T>
    LOCALVARIABLE i I L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2

    ...
  • public class com/jihan/javastudycode/week14/Box

    • 클래스 옆의 선언 <T extends Fruit> 가 지워졌다.
  • add(Lcom/jihan/javastudycode/week14/Fruit;)V

    • 기존의 void add(T item)의 T 값이 Fruit 로 치환됐다.
  • get(I)Lcom/jihan/javastudycode/week14/Fruit;

    • 기존의 T get (int i)의 T 값이 Fruit 로 치환됐다.
  • INVOKEVIRTUAL java/util/ArrayList.get (I)Ljava/lang/Object;

    CHECKCAST com/jihan/javastudycode/week14/Fruit

    • ArrayList.get이 반환하는 타입이 Object 이므로 Fruit 타입으로 형변환을 한다.

References

  • 남궁성, 『자바의 정석』, 도우출판(2016)
profile
얍얍 개발 펀치

0개의 댓글