제너릭을 예시로 이해 해보자!

허준현·2023년 9월 18일
0

JAVA

목록 보기
5/5
post-thumbnail

지난 시간에는 Generic에 대한 전반적인 포스트를 작성하였다. 단순하게 개념으로 이해하기는 어려워 여러군대에 분포되어 있는 예시를 가지고 이해 해보록 하자.

Nested Type Generic

아래와 같은 코드가 있다고 가정해보자

List<? extends ArrayList<? extends Integer>> l1 = new ArrayList<ArrayList<Integer>>();

위의 코드는 정상적으로 작동할까?
정답은 X이다. 자바에서 inner level에서는 invariant무공변성이다.
따라서 동일한 파라미터인 <? extends Integer> 를 사용하거나 다이아몬드 연산자를 통해서 표현 할 수 있다.

List<ArrayList<? extends Integer>> l2 = new ArrayList<ArrayList<? extends Integer>>();
List<ArrayList<? extends Integer>> l2 = new ArrayList<>();

상속관계 클래스 제너릭

class Container<T> {
    public final T t;
    public Container(T t) {
        this.t = t;
    }
}

public Container<B> method(Container<B> param){
  ...
}

위와 같은 코드가 있을 떄 아래의 결과값은 어떻게 나올까?

method(new Container<>(new A())); // ERROR or OK?
method(new Container<>(new B())); // ERROR or OK?
method(new Container<>(new C())); // ERROR or OK?

먼저 B는 상속관계 중에서 자기 자신이므로 OK 인 것은 알겠다.
그러면 부모와 자식인 경우에는 A,C 는 super, extends 를 사용하지 않아서 오류가 날까?
필자가 처음 제너릭을 공부하고 나서 위와 같이 생각을 하였는데 먼저 컬렉션의 경우에는 무공변이기에 상위경계, 하위경계가 필요한 것이고 클래스의 경우에는
공변(Covariance)가 작용하고 있기 때문에 상위 객체 안에 하위 객체를 넣을 수 있다.
하지만 A의 경우에는 B의 상위 객체이기 때문에 불가능하기 때문이다. 따라서 위의 정답은 ERROR, OK , OK 이다.

Functional Interface Generic

public class Foo<E> {

    private List<E> ls;

    public Foo(List<E> ls) {
        this.ls = ls;
    }

    public void add(E l) {
        this.ls.add(l);
    }

    public <F> Foo<F> map(Function<? super E, ? extends F> f) {
        Foo<F> res = new Foo<F>(new ArrayList<F>());
        for (E l : ls) {
            res.add(f.apply(l));
        }
        return res;
    }
}

위 작성자는 Fcuntion(E,F)를 쓰지 않고 Function<? super E, ? extends F>
를 사용하는 이유에 대해서 물었다.
필자는 E는 들어오고 F는 나가는 제너릭이니 IN은 extends , Out은 super !! 이러면서 확인했지만 결과는 처참했다.
단순한 것이지만 PECS나 IN/OUT은 컬렉션에 해당하는 부분이다.
위 같은 경우는 다음 함수 입력과 출력을 보다 유연하게 처리하고 함수 체인을 구성할 때 타입 일치성을 보장하기 위함으로 사용한다. 아래는 해당 답변이다.

class A {}
class B extends A {}
class C extends B {}

Foo<B> foo1 = new Foo<>(new B());
Function<A, C> aToC = a -> new C();  
Foo<B> foo2 = foo1.map(aToC);  //success!

단순하게 경계를 정하지 않으면 Function<B, B> 만 가능하지만 경계를 정해줌으로서 좀 더 자유롭게 형 변환을 할 수 있게 되면서 유연한 변환을 제공하고 있다.

SimpleStepBuilder

이번에 제너릭을 공부하게 된 이유이기도 하다.
SimpleStepBuilder에서 멤버변수로 ItemReader, ItemWriter, ItemProcessor를 가지고 있었고 해당 상위 경계, 하위경계가 포함되어 있는데 초기에 이해할 수 없는 경계를 사용했기 때문이다.
아래는 SimpleStepBuilder의 코드 일부분이다.

public class SimpleStepBuilder<I, O> extends AbstractTaskletStepBuilder<SimpleStepBuilder<I, O>> {

	private static final int DEFAULT_COMMIT_INTERVAL = 1;

	private ItemReader<? extends I> reader;

	private ItemWriter<? super O> writer;

	private ItemProcessor<? super I, ? extends O> processor;

	private Function<? super I, ? extends O> itemProcessorFunction;
}

Collections의 PECS 만 생각하고 Read는 생산자 이므로 extends ! Writer는 소비자 이므로 super ! 라고 생각하고 프로세서를 보고 의야했다.

I는 input 값이고 O는 Output값이니까 extends, O는 super여야 한다! 라고 생각했는데 정 반대였기 때문이다.
하지만 이는 List와 같은 Collection 에서 생각해야하는 문제이며 여기서의 extends 와 super는 Function의 경계처럼 형변환에 다형성을 주기 위해서 사용된 것이고 Input값을 super , Ouput값을 extends로 형성되어 있다.
이는 위에 예시로 나와있는 Function에 대입해 보면 이해 할 수 있으며 Function이 중간부분을 이어주는 역할을 하는 것과 processor도 동일하게 어떻게 보면 Reader와 Writer를 이어주는 역할을 한다.
이처럼 중간다리 역할을 하면서 Collection이 아닌 경우에는 super, extends를 사용한다.

static <T extends Comparable<? super T>> void sort(List<T> list)

Collections.sort() 메소드의 signiture 부분이다.
사용법이 간단하지만 메소드 선언부분은 쉽사리 접근하기 어렵다.
아래의 예시를 통해서 이해해보자.

class Top {
}
class Middle extends Top implements Comparable<Top> {
    @Override
    public int compareTo(Top o) {
        // 
    }
}
class Bottom extends Middle {
}

public static <T extends Comparable<? super T>> T max(List<? extends T> list)

위의 예시에서는 Bottom -> Middle -> Top 의 상속관계를 가지고 있다. Middle에는 Comparable 이구현되어 있고 Bottom에는 구현되어 있지 않다.
만일 이런 상황에서 sort의 타입 변수 가 <T extends Comparable> 인 경우에는 어떻게 될까?

Bottom은 Comaprable 를 구현하지 않아 sort 메소드를 이용하지 못할 것이다. 또 한 앞에서 배웠던 것처럼 Comparable는 안의 객체 T를 비교하면서 소비하므로 Comparable<? super T> 가 적합하다.

<? super T> 는 앞에서 배웠던 것처럼 내가 어떤 내용을 담고 있는지는 모르겠으나 T이거나 T 상위 객체임을 알고 있다. 위의 예시에 비교하게 되면 Top이거나 더 상위인 Object가 될 수 있으므로 모든 것을 만족하는 Middle,Bottom는 comaprable을 상속하고 있으므로 컴파일 가능하다.

static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

위에서 sort() 메소드를 이해했다면 비슷한 유형이기에 이해 할 수 있을 것이다.

Comparable<? super T>> 를 선언 함으로서 T객체가 Comparable를 구현하지 않아도 되며 T 객체를 비교하는 소비자 입장이기에 super를 사용한다는 것을 알 수 있다.

하지만 두 가지 드는 의문

Object 는 왜 붙어있는가.

extends 뒤에 붙은 객체로 컴파일시에 해당 객체로 캐스팅 되어 반환값이 Object이다. 이는 이전 API에 대해서 컴파일 시에 Comparable이 아닌 Object를 반환하기 위함이다.

sort 메소드의 파라미터는 <T> 이고 max 메소드의 파라미터는 왜 <? extends T> 인 것인가.

이는 필자의 생각이지만 반환값이 있는 경우에는 좀 더 유연하게 대응하기 위함이라고 생각 한다. sort() 메소드에서 설명한 Middle, Bottom 코드를 보게 되면

List<Bottom> list;
Middle max = max(list); // T inferred to be Middle

Bottom 객체를 Middle 객체로 자연스럽게 업 케스팅 하는 것을 알 수 있다.

비슷한 예시로는 Collections.unmodifiableList API 에서도 찾아볼 수 있다.
해당 글쓴이는 가 아닌 <? extends T> 를 사용하는 이유에 대해서 물었고 이는 반환값이 존재하기 때문에 유연한 API를 제공하기 위해 extends 를 사용한다.

List<Integer> intList = new ArrayList<Integer>();
// . . . populate intList

// now create an unmodifiable version:
//만약 <? extends T> 가 아닌 <T> 인 경우에는 error가 난다.
List<Number> numList = Collections.unmodifiableList(intList);

java Generics PECS add capture ? super

아래와 같이 Fruit 클래스가 존재하고 Fruit를 상속하는 Orange , Orange를 상속하는 BloodOrange가 있다.

abstract class Fruit implements PlantEatable {

    private boolean isRipe;

    private boolean isEatable;

    public boolean isRipe() {
        return isRipe;
    }

    public void setRipe(boolean ripe) {
        isRipe = ripe;
    }

    @Override
    public boolean isEatable() {
        return isEatable;
    }

    public void setEatable(boolean eatable) {
        isEatable = eatable;
    }
}

public class Orange extends Fruit{

}

public class BloodOrange extends Orange{

}

글쓴이의 질문은 다음과 같다.

		//Producer
        List<? extends Orange> oranges = new ArrayList<>();
        //oranges.add(new Orange()); // Doesn't work because its a producer ?

        //Consumer
        List<? super BloodOrange> objects = new ArrayList<>();
        objects.add(new BloodOrange());
        //objects.add(new Orange()); // Why doesn't it work ?
        //objects.add(new Object()); // Why doesn't it work ?

단순하게 extends를 사용했으니 해당 리스트에는 add를 못하는 것은 이해했지만 왜 super 하위 경계를 사용하고 나서 부모관계인 Orange, Object를 사용하지 못하는 것이냐가 질문이다.

먼저 extends 에는 .add 함수를 사용하지 못하는 것인가?
먼저 제너릭 ? extends Orange 의 의미는 내가 어떤 것을 가지고 있는 지는 모르겠으나 확실한 건 Orange의 자식이라는 것이다.
만일 new Orange만 추가하게 되면 상관없지만 자식 관계에 있는 BloodOrange와 위의 코드에는 없지만 WhiteOrange가 list에 들어가고 해당 리스트를 List 로 변환하게 된다면 Heap pollution (힙 오염)이 발생하게 된다. 따라서 extends 상위경계를 사용시에는 삽입이 불가능하고 오로지 다른 객체를 참조만 가능하다.

그러면 super인 경우에는 Orange와 Object를 넣을 수 없을까?
super를 해석해보면 내가 어떤 객체를 가지고 있는지는 모르겠으나 BloodOrange의 상위객체를 가지고 있다는 것이다. 즉 Orange, Object, BloodOrange 등이 들어 갈 수 있는데 이 중에서 Orange가 들어오게 된다면 이는 Object가 들어갈 수 없게 되며 super가 의미하는 객체 3개중 1개를 사용하지 못하게 된다.
하지만 List<? super Orange> li = new ArrayList<>(); 안에는 new BloodOrange() 를 넣을 수 있다.
이는 Orange로 Upcasting이 발생해도 side effect가 발생하지 않으며 super의 의미대로 3가지 모두 될 수 있기 때문에 삽입이 가능하다.

generics and lambda expression

해당 글쓴이는 아래와 같은 코드가 있는데 compile error를 내는 이유를 물어보고 있다.
Employee 클래스는 Person 클래스는 상속받고 있다.

public Predicate<? extends Person> startsA() {
        return p -> p.getName().startsWith("A");
}

startsA().test(new Employee());
startsA().test(new Person());

startsA라는 함수의 제너릭은 extends Person으로 Employee는 자식 관계에 있으므로 해당 코드는 돌아갈 것처럼 보인다.
하지만 Predicate 성격상 해당 제너릭을 소모하는 입장이어서 new Employee를 추가할 수 없다. 즉 Predicate 는 값을 입력으로 받지만 어떤 값인지는 모르는 상태이고 그 값은 Person의 하위 관계에 있다는 것만 아는 상태이다.
이는 앞의 예시와 비슷한데 만일 기존에 해당 Predicate의 제너릭이 Person 자식 관계에 있는 Student 가 들어가 있고 new Employee를 추가하게 된다면 이는 heap 오염이 발생하게 된다.
따라서 해당 코드는 오류가 발생하는 코드이며 Person 혹은 ? super Person 으로 수정하면 정상작동하게 된다.

한가지 드는 의문

Person 으로 선언을 하거나 <? super Person> 으로 선언하거나 둘다 Person, Employee 객체를 전달할 수 있다. 그러면 2가지의 차이는 무엇이 있을까?
전 시간에 PECS는 컬렉션 에서의 경계를 구분하는 방법이라고 하였다. 즉 List의 경우에는 공변성이 아닌 무공변성이며 이를 트릭처럼 공변성을 제공하는 것이 extends 와 super이다.
Predicate의 경우에는 공변성을 제공하기 때문에 Person으로도 공변성을 제공할 수 있으며 <? super Person> 도 제공한다.

제너릭 예시를 통해 이해를 마치면서

스프링 배치쪽을 구현함에 있어서 mybatis ItemReader, ItemWritre의 제너릭을 보고 기존에 알고 있던 부분과 많이 달라 깊게 공부하는 계기가 되었다.
개인적으로 대부분의 블로그가 Collections의 max만 다루고 있어 이해하기 어려웠고 필자랑 비슷한 인원이 보고 좀 더 Generic과 친해지는 계기였으면 좋겠다.
또 한 중간중간 왜 해당 제너릭을 사용하고 왜 이 경계를 사용하는 지에 대해서는 필자의 생각이 들어가 있는 부분이 많아 틀린 부분이 있으면 댓글로 알려주시면 감사합니다.

profile
best of best

0개의 댓글