JAVA/Generic

6. Generic 메소드

cla9 2020. 3. 10. 00:01

Integer 값이 들어있는 List 원소중 가장 큰 값을 리턴하는 함수를 만든다고 가정해봅시다.

 

public static int max(Collection<Integer> cols){
    return cols.stream().max((o1, o2) -> o1.compareTo(o2)).get();
}

 

대략 위와같이 구현할 수 있습니다. 그럼 Integer 타입이아닌 비교가 가능한 모든 클래스 타입이 사용될 수 있도록 하려면 어떻게 해야할까요?

 

그동안 학습한 내용을 적용하면서 이번 포스팅 주제를 다루어보겠습니다.

 

Step 1. Generic 사용

public static <T> T max(Collection<T> cols){
    return cols.stream().max((o1, o2) -> o1.compareTo(o2)).get(); //Compile 에러 발생
}

 

Get and Put 원리 고민없이 작성한다면 위와같이 작성할 수 있습니다. 여기서 첫번째 등장하는 <T>는 T 타입의 경계를 지정합니다. 이는 첫번째 포스팅에서 클래스 멤버 변수 타입의 경계 설정과 동일한 개념입니다.

 

하지만 이를 실행하면 두 원소의 대소 비교부분인 compareTo 메소드에서 컴파일 에러가 발생하게됩니다.

과연 무엇이 문제일까요?

 

첫번째 등장하는 <T> 구문은 T 타입의 경계를 지정한다고 했습니다.

위 코드는 Generic 타입이 존재한다는 것만 지정할 뿐, 해당 타입에 대한 경계를 지정하지 않았습니다.

이는 어떠한 타입이 지정되어도 상관없다는 의미입니다. 문제는 여기서 발생합니다.

 

compareTo 메소드를 사용하기 위해서는 해당 클래스에 Comparable 인터페이스가 구현되어있어야 합니다.

하지만 T 타입에는 어떠한 타입도 허용가능하므로 문제가 발생할 수 있습니다. 따라서 허용되지 않습니다.

 

이와 같은 문제를 해결하기 위해서는 Comparable 인터페이스가 구현된 타입만 지정되도록 경계를 정의해야합니다. 

 

Step2. 타입 경계 적용

 public static <T extends Comparable<T>> T max(Collection<T> cols){
    return cols.stream().max((o1, o2) -> o1.compareTo(o2)).get();
 }

 

메소드 레벨에서 타입 경계를 Comparable로 지정하면, Comparable Subtype 만 지정될 수 있습니다. 따라서 메소드 내부에서 compareTo 사용이 가능합니다.

 

Step3. Get and Put 원리 적용

public static <T extends Comparable<? super T>> T max(Collection<? extends T> cols){
    return cols.stream().max((o1, o2) -> o1.compareTo(o2)).get();
}

 

Get and Put 원리를 적용하면 위와같이 변경 가능합니다. 그 이유는 Collection 내부에서 바깥으로 T의 Subtype 값을 꺼내며, Comparable 인터페이스 compareTo 메소드안으로 값을 넣기 때문입니다.

 

Step4. Method Reference 적용

public static <T extends Comparable<? super T>> T max(Collection<? extends T> cols){
    return cols.stream().max(T::compareTo).get();
}

 

추가로 Generic 타입에 메소드 레퍼런스를 적용하면 위와같이 작성할 수 있습니다.


Bridge 메소드

 

public interface Comparable{
	public int compareTo(Object o);
}    

 

이번에는 Bridge 메소드에 대해서 알아보겠습니다. Java 1.4 버전 이하에서는 Generic 지원이 없었기 때문에 Comparable 인터페이스 구조는 위와 같습니다. 이때 파라미터는 모든 타입을 허용해야하므로 Object 타입인 것을 확인할 수 있습니다.

 

class Integer implements Comparable{
    private final int value;
    public Integer(int value) { this.value = value; }
    public int compareTo(Integer o){
        return (this.value < o.value) ? -1 : (value == o.value ? 0 : 1 );
    }
    @Override
    public int compareTo(Object o) {
        return compareTo((Integer)o);
    }
}

 

이를 구현해야하는 Integer, Double 등의 클래스에서는 올바른 비교 처리를 위해 Object를 자기 자신의 클래스 타입으로 치환하여 처리하도록 부가적인 메소드 구현이 필요했습니다.

 

public interface Comparable<T>{
	public int compareTo(T o);
}    
class Integer implements Comparable<Integer>{
    private final int value;
    public Integer(int value) { this.value = value; }
    @Override
    public int compareTo(Integer o){
        return (this.value < o.value) ? -1 : (value == o.value ? 0 : 1 );
    }
}

 

Generic 등장 이후 compareTo 메소드에 Object가 아닌 명확한 타입 지정이 가능해졌습니다.

따라서 이를 구현하는 클래스에서도 Object 타입이 아닌 자신이 지정한 타입으로의 메소드 구현이 가능해졌습니다.

그 결과 Bridge 메소드 없이 1개의 메소드만 구현하면 됩니다.

 

하지만 여기서 한가지 의문이 발생합니다.

 

'Java는 하위 호환성을 중요시 여기는 언어인데, 위와 예시와 같이 Generic 도입에 따라서 메소드 수가 줄어들게 되면, 이는 Integer 클래스 구조가 변경되므로 하위 호환성을 유지할 수 있을까?'

 

 

실제 구조가 변경되었는지 확인을 위해 Reflection을 활용해서 런타임시 compareTo 메소드 정보를 출력해보겠습니다.

 

public class Main {
    public static void main(String[] args) {
        Stream.of(Integer.class.getDeclaredMethods())
                .filter(m -> "compareTo".equals(m.getName()))
                .map(Method::toGenericString)
                .forEach(System.out::println);
    }
}
public int java.lang.Integer.compareTo(java.lang.Object)
public int java.lang.Integer.compareTo(java.lang.Integer)

 

출력 결과 2개의 메소드가 존재함을 확인할 수 있습니다.

이를 통해 알 수 있는 사실은 Generic 타입 파라미터가 포함된 메소드를 오버로딩 할경우 Bridge 메소드를 자동으로 추가하여 레거시 코드와 호환성을 유지한다는 점입니다.