Java Generics - 놓치기 쉬운 개념들

이강현·2025년 4월 16일

놓치기 쉬운 개념들

목록 보기
12/19

Generic type Erasure

컴파일러는 제네릭 타입을 이용하여 소스파일을 검사하고, 필요한 곳에 형변환을 추가한 뒤에 제네릭 타입을 제거합니다.
컴파일 된 .class 파일에는 제네릭에 대한 정보가 없습니다.
따라서 제네릭을 사용할 때 다음과 같은 제약이 있습니다.

  • static 멤버에 제네릭을 사용할 수 없습니다.
  • 제네릭으로 배열 생성 또는 객체 생성이 불가능합니다.
  • instanceof 우항에 제네릭을 사용할 수 없습니다. 참조값이 와야하는 좌항은 당연하게도 안됩니다.
  • 타입 파라미터의 Class 객체 또한 접근(T.class)이 불가능합니다.



Bounded Generics

  • 제네릭 타입의 상속 계층에서의 범위를 제한하기 위해서 extends 키워드를 사용합니다.
  • 인터페이스에 대한 제약 또한 extends 를 사용합니다.
  • '&' 를 이용해 multi bound 가 가능합니다.
    • multi-catch block'|' 와 마찬가지로 논리 연산자가 아니며, AND 의 의미로 사용된 것입니다.

Shadowing

  • 제네릭 클래스에서의 타입 변수제네릭 메서드에서의 타입 변수독립적입니다.
  • 다만 두 타입 변수의 이름이 겹치는 경우 먼 변수(제네릭 클래스 타입 변수)가 가려져 사용할 수 없습니다.
  • shadowing 을 피하고, 가독성을 높이기 위해 두 변수는 이름을 다르게 지어야 합니다.
public class GenericLevelTest {
    public static void main(String[] args) {
        MyClass1 myClass1 = new MyClass1();
        GenericClass<MyClass1> gc = new GenericClass<>();
        MyClass2 myClass2 = new MyClass2();

        Object o = gc.genericMethod(myClass1, myClass2); // nearest common ancestor -> Object
        System.out.println(o instanceof MyClass1);
        System.out.println(o instanceof MyClass2);
    }
}

class GenericClass<T> {
    public <T> T genericMethod(T classLevelInput, T methodLevelInput) { // shadowing
        System.out.println(classLevelInput.getClass()); // class MyClass1
        System.out.println(methodLevelInput.getClass()); // class MyClass2
        return classLevelInput;
    }
}

class MyClass1 {}
class MyClass2 {}

Wildcards

  • 와일드카드는 제네릭 타입 시스템 내에서 여러 관련 타입묶어 다룰 수 있게 해주는 메커니즘입니다.
  • 와일드카드는 타입 안전성유지하면서 제네릭 타입 사용의 유연성을 높여줍니다.
  • 제네릭에 다형성 기능을 강화했다고 생각해도 좋을 것 같습니다.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class GenericsTest {
    public static void main(String[] args) {
//        FruitBox<? extends Fruit> fruitBox = new FruitBox<>();
        FruitBox<Fruit> fruitBox = new FruitBox<>();
        fruitBox.add(new Apple());
        fruitBox.add(new Grape());

        FruitBox<Apple> appleBox = new FruitBox<>();
        appleBox.add(new Apple());
        appleBox.add(new Apple());

        FruitBox<Grape> grapeBox = new FruitBox<>();
        grapeBox.add(new Grape());
        grapeBox.add(new Grape());

        FruitComparator fruitComparator = new FruitComparator();
        // signature of Collections.sort()
        // public static <T> void sort(List<T> list, Comparator<? super T> c)
        Collections.sort(fruitBox.getList(), fruitComparator);
        Collections.sort(appleBox.getList(), fruitComparator);
        Collections.sort(grapeBox.getList(), fruitComparator);

        System.out.println(Juicer.makeJuice(fruitBox)); // wildcard makes it flexible
        System.out.println(Juicer.makeJuice(appleBox));
        System.out.println(Juicer.makeJuice(grapeBox));
    }
}

class Box<T> {
//    T[] item = new T[5]; // cannot be instantiated directly
//    T item = new T(); // cannot be instantiated directly
    List<T> list = new ArrayList<>();

    void add(T item) { list.add(item); }
    public List<T> getList() { return list; }

//    public void errorTest(Fruit fruit) {
//        boolean b = fruit instanceof T; // compiler error
//        Class<T> c = T.class; // cannot access class object of a type parameter
//    }
}

class FruitBox<T extends Fruit> extends Box<T> {}

class Fruit { public String toString() { return "Fruit"; } }

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

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

class FruitComparator implements Comparator<Fruit> {
    public int compare(Fruit o1, Fruit o2) {
        return o1.toString().compareTo(o2.toString());
    }
}

class Juice {
    String name;
    Juice(String name) { this.name = name + "Juice"; }
    public String toString() { return name; }
}

class Juicer {
    static <T extends Fruit> Juice makeJuice(FruitBox<T> box) { // wildcard
        StringBuilder tmp = new StringBuilder();
        for (Fruit fruit : box.getList()) {
            tmp.append(fruit).append(" ");
        }
        return new Juice(tmp.toString());
    }
}

Casting

  • 제네릭 타입과 원시 타입간에는 언제나 형변환이 가능하지만 경고가 발생합니다.

제네릭 클래스에 타입 매개변수로 와일드 카드까지 들어간 경우에 형변환을 생각하기 굉장히 어려울 수 있습니다.
Optional 클래스 소스 코드의 일부를 확인해봅시다.

private static final Optional<?> EMPTY = new Optional<>();

public static<T> Optional<T> empty() {
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

Optional<?> EMPTY = new Optional<>();
Optional<? extends Object> EMPTY = new Optional<Object>(); 가 생략된 것입니다.
Optional<Object> -> Optional<?> -> Optional<T>의 순서로 형변환 되었습니다.
이렇게 여러 단계에 거쳐 형변환 한 것은
Optional<Object> -> Optional<T>
이렇게 바로 형변환이 불가능 하기 때문입니다.

  • 와일드 카드가 들어간 형변환 문제는 일반적인 형변환을 범위의 문제확장했다고 생각하면 편합니다.
  • 타입 매개변수와일드 카드가 들어가면 잠재적으로 지정한 범위를 모두 가질수 있는 타입이라고 보고,
    그렇게 선언된 참조변수에는 범위에 포함되는 타입대입될 수 있습니다.
  • 따라서 와일드 카드를 매개변수로 갖는 타입도 범위를 벗어날 수 있는 경우 형변환이 불가능합니다.
  • up-casting가능성이 있다면 수동적인 형변환을 통해 대입이 가능합니다.

다음 코드를 통해 여러 경우를 확인해 볼 수 있습니다.

class GenericClazz<T> {}

class GrandParent {}
class Parent extends GrandParent {}
class Child extends Parent {}
class GrandChild extends Child {}

public class CastingTest {
    public static void main(String[] args) {
//        GenericClazz<? extends Object> objectE = null;
        GenericClazz<?> objectE = null;
        GenericClazz<? extends GrandParent> grandParentE = null;
        GenericClazz<? extends Parent> parentE = null;
        GenericClazz<? extends Child> childE = null;
        GenericClazz<? extends GrandChild> grandChildE = null;

        GenericClazz<? super GrandChild> grandChildS = null;
        GenericClazz<? super Child> childS = null;
        GenericClazz<? super Parent> parentS = null;
        GenericClazz<? super GrandParent> grandParentS = null;
        GenericClazz<? super Object> objectS = null;

        objectE = grandParentE; // OK
        objectE = parentE; // OK
        objectE = childE; // OK
        objectE = grandChildE; // OK
        objectE = grandChildS; // OK
        objectE = childS; // OK
        objectE = parentS; // OK
        objectE = grandParentS; // OK
        objectE = objectS; // OK

        grandParentE = (GenericClazz<? extends GrandParent>) objectE; // consider up-casting
        grandParentE = parentE; // OK
        grandParentE = childE; // OK
        grandParentE = grandChildE; // OK
        grandParentE = (GenericClazz<? extends GrandParent>) grandChildS; // consider up-casting
        grandParentE = (GenericClazz<? extends GrandParent>) childS; // consider up-casting
        grandParentE = (GenericClazz<? extends GrandParent>) parentS; // consider up-casting
        grandParentE = (GenericClazz<? extends GrandParent>) grandParentS; // consider up-casting
//        grandParentE = (GenericClazz<? extends GrandParent>) objectS; // no possibility

        parentE = (GenericClazz<? extends Parent>) objectE; // consider up-casting
        parentE = (GenericClazz<? extends Parent>) grandParentE; // consider up-casting
        parentE = childE; // OK
        parentE = grandChildE; // OK
        parentE = (GenericClazz<? extends Parent>) grandChildS; // consider up-casting
        parentE = (GenericClazz<? extends Parent>) childS; // consider up-casting
        parentE = (GenericClazz<? extends Parent>) parentS; // consider up-casting
//        parentE = (GenericClazz<? extends Parent>) grandParentS; // no possibility
//        grandParentE = (GenericClazz<? extends GrandParent>) objectS; // no possibility

        childE = (GenericClazz<? extends Child>) objectE; // consider up-casting
        childE = (GenericClazz<? extends Child>) grandParentE; // consider up-casting
        childE = (GenericClazz<? extends Child>) parentE; // consider up-casting
        childE = grandChildE; // OK
        childE = (GenericClazz<? extends Child>) grandChildS; // consider up-casting
        childE = (GenericClazz<? extends Child>) childS; // consider up-casting
//        childE = (GenericClazz<? extends Child>) parentS; // no possibility
//        childE = (GenericClazz<? extends Child>) grandParentS; // no possibility
//        childE = (GenericClazz<? extends Child>) objectS; // no possibility

        grandChildE = (GenericClazz<? extends GrandChild>) objectE; // consider up-casting
        grandChildE = (GenericClazz<? extends GrandChild>) grandParentE; // consider up-casting
        grandChildE = (GenericClazz<? extends GrandChild>) parentE; // consider up-casting
        grandChildE = (GenericClazz<? extends GrandChild>) childE; // consider up-casting
        grandChildE = (GenericClazz<? extends GrandChild>) grandChildS; // consider up-casting
//        grandChildE = (GenericClazz<? extends GrandChild>) childS; // no possibility
//        grandChildE = (GenericClazz<? extends GrandChild>) parentS; // no possibility
//        grandChildE = (GenericClazz<? extends GrandChild>) grandParentS; // no possibility
//        grandChildE = (GenericClazz<? extends GrandChild>) objectS; // no possibility

        grandChildS = (GenericClazz<? super GrandChild>) objectE; // consider up-casting
        grandChildS = (GenericClazz<? super GrandChild>) grandParentE; // consider up-casting
        grandChildS = (GenericClazz<? super GrandChild>) parentE; // consider up-casting
        grandChildS = (GenericClazz<? super GrandChild>) childE; // consider up-casting
        grandChildS = (GenericClazz<? super GrandChild>) grandChildE; // consider up-casting
        grandChildS = childS; // OK
        grandChildS = parentS; // OK
        grandChildS = grandParentS; // OK
        grandChildS = objectS; //OK

        childS = (GenericClazz<? super Child>) objectE; // consider up-casting
        childS = (GenericClazz<? super Child>) grandParentE; // consider up-casting
        childS = (GenericClazz<? super Child>) parentE; // consider up-casting
        childS = (GenericClazz<? super Child>) childE; // consider up-casting
//        childS = (GenericClazz<? super Parent>) grandChildE; // no possibility
        childS = (GenericClazz<? super Child>) grandChildS; // consider up-casting
        childS = parentS; // OK
        childS = grandParentS; // OK
        childS = objectS; // OK

        parentS = (GenericClazz<? super Parent>) objectE; // consider up-casting
        parentS = (GenericClazz<? super Parent>) grandParentE; // consider up-casting
        parentS = (GenericClazz<? super Parent>) parentE; // consider up-casting
//        parentS = (GenericClazz<? super Parent>) childE; // no possibility
//        parentS = (GenericClazz<? super Parent>) grandChildE; // no possibility
        parentS = (GenericClazz<? super Parent>) grandChildS; // consider up-casting
        parentS = (GenericClazz<? super Parent>) childS; // consider up-casting
        parentS = grandParentS; // OK
        parentS = objectS; // OK

        grandParentS = (GenericClazz<? super GrandParent>) objectE; // consider up-casting
        grandParentS = (GenericClazz<? super GrandParent>) grandParentE; // consider up-casting
//        grandParentS = (GenericClazz<? super GrandParent>) parentE; // no possibility
//        grandParentS = (GenericClazz<? super GrandParent>) childE; // no possibility
//        grandParentS = (GenericClazz<? super GrandParent>) grandChildE; // no possibility
        grandParentS = (GenericClazz<? super GrandParent>) grandChildS; // consider up-casting
        grandParentS = (GenericClazz<? super GrandParent>) childS; // consider up-casting
        grandParentS = (GenericClazz<? super GrandParent>) parentS; // consider up-casting
        grandParentS = objectS; // OK

        objectS = (GenericClazz<? super Object>) objectE; // consider up-casting
//        objectS = (GenericClazz<? super Object>) grandParentE; // no possibility
//        objectS = (GenericClazz<? super Object>) parentE; // no possibility
//        objectS = (GenericClazz<? super Object>) childE; // no possibility
//        objectS = (GenericClazz<? super Object>) grandChildE; // no possibility
        objectS = (GenericClazz<? super Object>) grandChildS; // consider up-casting
        objectS = (GenericClazz<? super Object>) childS; // consider up-casting
        objectS = (GenericClazz<? super Object>) parentS; // consider up-casting
        objectS = (GenericClazz<? super Object>) grandParentS; // consider up-casting
    }
}
profile
백엔드 개발자 지망생입니다.

0개의 댓글