ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java - 제네릭(Generic)의 제한 - extends, super, ?
    Language/Java 2022. 12. 28. 14:57

    제네릭(Generic)의 제한 - extends, super, ?

    개요

    이전 게시글에서는 제네릭의 가장 일반적인 사용법과 특성에 대해서 알아보았다. 예를 들어, 타입을 T라고 하고 외부에서 Integer로 지정하면 T는 Integer가 되고, String으로 지정하면 T는 String이 된다. 만약, Student 클래스를 만들고 T를 Student를 지정한다면 T는 Student가 된다. 즉, 제네릭은 이 처럼 참조 타입 모두가 될 수 있다.

     

    만약, 이러한 T를 특정 범위 내로 좁혀서 제한하고 싶다면 어떻게 해야 할까?

     

    이때 필요한 것이 바로 extends, super, ?이다. extends와 super이라는 키워드는 많이 보았을 것이다. ?는 와일드카드라고 해서 쉽게 말해 알 수 없는 타입이라는 의미이다.

     

    <K extends T>   // T와 T의 자손 타입만 가능(K는 들어오는 타입으로 지정됨)
    <? extends T>   // T와 T의 부모 타입만 가능(K는 들어오는 타입으로 지정됨)
    
    <K super T>   // T와 T의 자손 타입만 가능
    <? super T>   // T와 T의 부모 타입만 가능
    
    <?>   // 모든 타입 가능, <? extends Object>랑 같은 의미

    세 가지 키워드의 사용법은 위와 같다.

     

    K extends T? extends T는 비슷한 구조지만 차이점이 있다. 타입의 경계를 지정한다는 것은 같지만, 경계가 지정되고 나서 K는 특정 타입으로 지정되지만 ?는 타입이 지정되지 않는다.

     

    /*
     * Number와 이를 상속하는 Integer, Short, Double, Long 등의
     * 타입이 지정될 수 있으며, 객체 혹은 메서드를 호출 할 경우 K는
     * 지정된 타입으로 변환이 된다.
     */
    <K extends Number>
     
     
    /*
     * Number와 이를 상속하는 Integer, Short, Double, Long 등의
     * 타입이 지정될 수 있으며, 객체 혹은 메서드를 호출 할 경우 지정 되는 타입이 없어
     * 타입 참조를 할 수는 없다.
     */
    <? extends Number>

    위와 같은 차이가 있다. 그렇기 때문에 특정 타입의 데이터를 조작하고자 하는 경우에는 K같이 특정 제네릭 인수로 지정을 해주어야 한다.

     

     

    우선, extends와 super의 예를 들기 위해, 위와 같이 서로 다른 클래스들이 상속관계를 가지고 있다고 가정해 보겠다.

     

    <K extends T>, <? extends T>

    <K extends T>, <? extends T>는 T 타입을 포함한 자식 타입만 가능하다는 의미이다.

     

    <T extends B>   // B와 C 타입만 올 수 있음
    <T extends E>   // E 타입만 올 수 있음
    <T extends A>   // A, B, C, D, E 타입이 올 수 있음
     
    <? extends B>   // B와 C 타입만 올 수 있음
    <? extends E>   // E 타입만 올 수 있음
    <? extends A>   // A, B, C, D, E 타입이 올 수 있음

    주석에 작성했듯이, extends 뒤에 오는 타입이 최상위 타입으로 한계가 정해진다.

     

    예를 들어, 제네릭 클래스에서 수를 표현하는 클래스만 받고 싶은 경우가 있다. 대표적적으로 Integer, Long, Byte, Double, Float, Short 같은 래퍼 클래스들은 Number 클래스를 상속받는다.

     

    public class ClassName <K extends Number> { ... }

    특정 클래스를 Integer, Long, Byte, Double, Float, Short와 같은 수를 표현하는 래퍼 클래스만으로 제한하고 싶은 경우 위와 같이 사용할 수 있다.

     

    class ClassName <K extends Number> {
       ...
    }
    
    public class Main {
        public static void main(String[] args) {
            ClassName<Double> a01 = new ClassName<Double>();
            
            ClassName<String> a02 = new ClassName<String>();   // 컴파일 에러
        }
    }

    Double은 Number 클래스를 상속받는 클래스라 문제가 없지만, String은 Number 클래스와는 완전 별개의 클래스이기 때문에, 컴파일 에러를 발생시킨다.

     

    <K super T>, <? super T>

    <K super T>, <? super T>는 T 타입을 포함한 부모 타입만 가능하다는 의미다.

     

    <K super B>   // B와 A 타입만 올 수 있음
    <K super E>   // E, D, A 타입만 올 수 있음
    <K super A>   // A 타입만 올 수 있음
     
    <? super B>   // B와 A 타입만 올 수 있음
    <? super E>   // E, D, A 타입만 올 수 있음
    <? super A>   // A 타입만 올 수 있음

    주석에 작성했듯이, super 뒤에 오는 타입이 최하위 타입으로 한계가 정해진다.

     

    super는 대표적으로 해당 객체가 업 캐스팅이 될 필요가 있을 때 사용된다.

    예를 들어, '과일'이라는 클래스가 있고, 이 클래스를 상속받는 '사과' 클래스와 '딸기' 클래스가 있다고 해보자. 이때 각각의 '사과'와 '딸기'는 종류가 다르지만, 둘 다 '과일'로 보고 자료를 조작해야 할 수도 있다. 이런 경우 '사과'를 '과일'로 캐스팅해야 하는데, '과일'이 상위 타입이므로 업 캐스팅해야 한다. 이럴 때 쓸 수 있는 것이 super이다.

     

    조금 더 현실성 있는 에제라면 제네릭 타입에 대한 객체 비교가 있다.

     

    public class ClassName <E extends Comparable<? super E>> {
       ...
    }

    위와 같은 코드를 한 번쯤 보았을 것이다. 특히 PriorityQueue, TreeSet, TreeMap 같이 값을 정렬하는 클래스에서 사용된다. 만약, 여러분이 특정 제네릭에 대한 자기 참조 비교를 하고 싶은 경우 대부분 위와 같은 형식을 가진다.

     

    우선, <E extends Comparable<E>>부터 살펴보겠다. extends는 앞서 말했듯이 extends 뒤에 오는 타입이 최상위 타입이 되고, 해당 타입과 그에 대한 하위 타입으로 제한된다고 했다. 이 말은 즉, E 객체는 반드시 Comparable을 구현해야 한다는 의미이다.

     

    class SaltClass <E extends Comparable<E>> { ... }
    
    class Student implements Comparable<Student> {
        @Override
        public int compareTo(Student o) { ... }
    }
    
    public class Main {
        public static void main(String[] args) {
            SaltClass<Student> a = new SaltClass<Student>();
        }
    }

    <E extends Comparable<E>>는 위와 같이 사용할 수 있다. SaltClass의 E는 Comparable<E>의 하위 클래스여야 한다. 즉, E가 될 수 있는 타입은 Comparable을 구현해야 한다는 것을 의미한다.

     

    그렇다면, 왜 <E extends Comparable<E>>가 아닌 <E extends Comparable<? super E>>일까? 앞서 설명했듯이, super E는 E를 포함한 상위 클래스의 객체들이 올 수 있다고 했다.

     

    class SaltClass <E extends Comparable<E>> { ... }   // 에러 가능성 있음
    class SaltClass <E extends Comparable<? super E>> { ... }   // 안전성 높음
    
    class Person { ... }
    
    class Student extends Person implements Comparable<Person> {
        @Override
        public int compareTo(Person o) { ... }
    }
    
    public class Main {
        public static void main(String[] args) {
            SaltClass<Student> a = new SaltClass<Student>();
        }
    }

    위 예제에서 Student보다 더 큰 범주의 클래스인 Person 클래스가 있다고 가정해 보자. Student 클래스는 Person 클래스의 하위 클래스이며, Person 타입으로 업 캐스팅하고 있는 compareTo() 메서드를 가지고 있다.

     

    만약, <E extends Comparable<E>>라면, Comparable<Person>을 구현하고 있는 Student 클래스를 타입으로 지정하는 것은 알맞지 않은 방식이다. 따라서, <E extends Comparable<? super E>>를 사용함으로써, 상위 타입의 Comparable<Person>을 구현하고 있는 클래스를 지정할 수 있게 한다.

    즉, <E extends Comparable<? super E>>는 E 타입 또는 E 타입의 상위 클래스가 Comparable을 의무적으로 구현해야 한다는 뜻으로, 상위클래스로의 업 캐스팅이 발생하더라도 안전성을 보장받을 수 있는 방법이다.

     

    <?>, 와일드카드

    <?><? extends Object>와 마찬가지라고 했다. Object는 자바에서의 모든 API 및 사용자 클래스의 최상위 클래스이다.

     

    public class ClassName { ... }
    public class ClassName extends Object { ... }

    위 두 코드는 같은 의미의 코드이다. 즉, ClassName 클래스는 묵시적으로  Object 클래스를 상속받는 것이다.

     

    한마디로 <?>는 어떤 타입을 지정하든 상관이 없다는 의미다. <?>는 주로 데이터가 아닌 기능의 사용에만 관심이 있는 경우 사용할 수 있다.


    출처

    https://st-lab.tistory.com/153

     

    728x90

    댓글

Designed by Tistory.