본문 바로가기
토비의봄

3.2. Generics

by 이석준석이 2021. 2. 25.

1. WildCard vs Generics

 

  • 언제 제너릭을 사용하고 언제 wildcard를 사용할까?
  • 아래의 예시에서는 wildcard의 한계를 보여준다.
static <T> void method1(List<T> list) {

}

static void method2(List<?> list) {
    /**
     * list.add(1); list.add("string"); -> 불가
     */
    list.add(null);
    list.size();
    list.clear();
    Iterator<?> it = list.iterator();
    list.equals();
}

wildcard는 list 그 자체의 기능을 사용할 뿐, 원소를 추가할 수는 없는 모습이다.


예시) isEmpty() 함수를 만들어보자.

public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(isEmpty...(list));
}

static <T> boolean isEmptyGenerics(List<T> list) {
    return list.size() == 0;
}

static boolean isEmptyWildCard(List<?> list) {
    return list.size() == 0;
}

 

isEmpty의 경우에는 리스트가 비어있는가만 확인하면 되기 때문에, wildcard를 사용하는것이 올바르다고 볼 수 있다.

  • Generics 를 사용하는 경우는 원소의 추가 등, 타입 파라미터에 대해 관심이 있는경우 사용하도록 한다.
    • 내부 구현에서 타입 파라미터를 이용해서 사용하겠다고 명시하는 것이며, 의도를 바르게 드러내지 못한 코드이기 때문이다.

2. 상위경계와 하위경계

 

max 함수를 만들어보자.

static <T extends Comparable<T>> T max(List<T> list) {
    return list.stream().reduce((a, b) -> a.compareTo(b) > 0 ? a : b).get();
}

 

하지만 더 정확하게 만든다면 아래와 같다.

  • 파라미터로 넘어오는 list에서 비교하는 타입이 슈퍼타입과 서브타입의 비교가 있을 수 있기 때문 (<? extends T>
    • 상위제한
    • a.compareTo(b) 에서 a는 Number, b는 Integer 인 경우
  • 슈퍼타입과 서브타입이 넘어온 경우에, 둘 다에서 통용되는 Comparable은 슈퍼타입으로 한정해야 하기 때문에 (Comparable<? super T>)
    • 하위제한
    • a.compareTo(b) 에서 a는 Number, b는 Integer 인 경우, 둘 모두를 비교하기 위해서는 하위한정으로 Number까지 Comparable이 구현되어있어야 되기 때문
static <T extends Comparable<? super T>> T max(List<? extends T> list) {
    return list.stream().reduce((a, b) -> a.compareTo(b) > 0 ? a : b).get();
}

3. Wildcard capture

 

capture

  • wildcard 를 사용했는데, 상황에 따라 타입을 추론해야 할 필요가 있어 컴파일에러가 나는 경우
  • 아래의 코드는 컴파일에러가나며 capture of ? 라는 에러 발생
static void reverse(List<?> list) {
    List<?> temp = new ArrayList<>(list);
    for(int i = 0 ; i < list.size() ; i++) {
        list.set(i, temp.get(list.size() - i - 1));
    }
}

 

한번 감싸서 반환해준다.

  • reverse(List<?> list) 를 reverseHelper 로 컴파일러가 캡쳐해주고, 반환하여 사용자로 하여금 api 에 대한 오해를 불러일으키지 않도록 한다.
static void reverse(List<?> list) {
    reverseHelper(list);
}

static <T> void reverseHelper(List<T> list) {
    List<T> temp = new ArrayList<>(list);
    for(int i = 0 ; i < list.size() ; i++) {
        list.set(i, temp.get(list.size() - i - 1));
    }
}

4. Intersection type

 

람다식은 아래와같이 캐스팅이 가능하다.

public class IntersectionType {
    public static void main(String[] args) {
        hello((Function)s -> s);
    }

    private static void hello(Function f) {

    }
}

 

Intersection Type은 &를 이용해서 아래와 같이 사용하는 방식

  • Serializable은 내부에 함수가없는 마커 인터페이스 결국은 1(1+0)개의 메소드이므로 Intersection Type으로 만들 수 있다.
public class IntersectionType {
    public static void main(String[] args) {
        hello((Function & Serializable) s -> s);
    }

    private static void hello(Function f) {

    }
}

람다식은 내부적으로 해당 인터페이스를 구현한 클래스를 만든다.

  • 위의 (Function & Serializable) 은 
  • class ?? implements Function, Serializable 이 된다.
  • 이를 이용해서 인터페이스를 조합하면서 새로운 클래스를 정의하지 않고도 여러 기능이 있는 코드를 생성할 수 있다.
    • 일종의 익명클래스의 개념이다.
public class IntersectionType {
    interface Hello {
        default void hello() {
            System.out.println("Hello");
        }
    }

    interface Hi {
        default void hi() {
            System.out.println("hi");
        }
    }

    public static void main(String[] args) {
        hello((Function & Hello & Hi) s -> s);
    }

    private static <T extends Function & Hello & Hi> void hello(T t) {
        t.hello();
        t.hi();
    }
}

 

콜백방식으로도 사용 가능

  • 첫번째 파라미터에서 타입추론을 가능하게 하고, 두번째 파라미터인 consumer 가 실행하도록 함.
public class IntersectionType {
    public static void main(String[] args) {
        run((Function & Hello & Hi) s -> s, o -> {
            // 타입추론돼서 hello(), hi() 사용 가능
            o.hello();
            o.hi();
        });
    }

    private static <T extends Function> void run(T t, Consumer<T> consumer) {
        consumer.accept(t);
    }
}

Hello와 Hi에 Function을 extends 하여도 컴파일에러가 발생하지 않는다.

  • induce 되어 결국에는 다 Function 하위의 apply() 메소드만 갖는 것이기 때문에 아래와같이도 사용가능
public class IntersectionType {
    interface Hello extends Function {
        default void hello() {
            System.out.println("Hello");
        }
    }

    interface Hi extends Function {
        default void hi() {
            System.out.println("hi");
        }
    }

    interface Printer {
        default void print(String str) {
            System.out.println(str);
        }
    }

    public static void main(String[] args) {
        run((Function & Hello & Hi & Printer) s -> s, o -> {
            // 타입추론돼서 hello(), hi() 사용 가능
            o.hello();
            o.hi();
            o.print("lambda");
        });
    }

    private static <T extends Function> void run(T t, Consumer<T> consumer) {
        consumer.accept(t);
    }
}

사용

  • delegate() 메소드의 구현은
    • () -> "sjLee" 
  • 이를 통해 위의 s -> s  처럼 무의미한 것이 아니게하여 사용할 수 있음
public class IntersectionType {

    interface DelegateTo<T> {
        T delegate();
    }

    interface Hello extends DelegateTo<String> {
        default void hello() {
            System.out.println("Hello " + delegate());
        }
    }

    interface UpperCase extends DelegateTo<String> {
        default void upperCase() {
            System.out.println(delegate().toUpperCase());
        }
    }

    public static void main(String[] args) {
        run((DelegateTo<String> & Hello & UpperCase) () -> "sjLee", o -> {
            o.hello();
            o.upperCase();
        });
    }

    private static <T extends DelegateTo<S>, S> void run(T t, Consumer<T> consumer) {
        consumer.accept(t);
    }
}

응용

  • default 메소드로 모두다 오버라이딩 한 뒤에 Forwarding interface 를 만들어서 Functional Interface 처럼 사용
  • 이에 대해서 intersection type 으로 사용
public class IntersectionType {

    interface Pair<T> {
        T getFirst();
        T getSecond();
        void setFirst(T first);
        void setSecond(T second);
    }

    static class Name implements Pair<String> {
        private String firstName;
        private String lastName;

        public Name(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public String getFirst() {
            return this.firstName;
        }

        public String getSecond() {
            return this.lastName;
        }

        public void setFirst(String first) {
            this.firstName = first;
        }

        public void setSecond(String second) {
            this.lastName = second;
        }
    }

    // forwarding interface
    interface ForwardingPair<T> extends DelegateTo<Pair<T>>, Pair<T> {
        default T getFirst() {
            return delegate().getFirst();
        }

        default T getSecond() {
            return delegate().getSecond();
        }

        default void setFirst(T first) {
            delegate().setFirst(first);
        }

        default void setSecond(T second) {
            delegate().setSecond(second);
        }
    }

    interface DelegateTo<T> {
        T delegate();
    }

    private static <T extends DelegateTo<S>, S> void run(T t, Consumer<T> consumer) {
        consumer.accept(t);
    }

    interface Convertable<T> extends DelegateTo<Pair<T>> {
        default void convert(Function<T, T> mapper) {
            Pair<T> pair = delegate();

            pair.setFirst(mapper.apply(pair.getFirst()));
            pair.setSecond(mapper.apply(pair.getSecond()));
        }
    }

    interface Printable<T> extends DelegateTo<Pair<T>> {
        default void print() {
            System.out.println(delegate().getFirst() + " " + delegate().getSecond());
        }
    }

    public static void main(String[] args) {
        Pair<String> name = new Name("Toby", "Lee");
        run((ForwardingPair<String> & Convertable<String> & Printable<String>)()->name, o->{
            o.print();
            o.convert(s -> s.toUpperCase());
            o.print();
            o.convert(s -> s.substring(0, 2));
            o.print();
        });
    }
}

'토비의봄' 카테고리의 다른 글

4.2. Reactive Streams  (0) 2021.02.28
4.1. Reactive Streams  (0) 2021.02.28
3.1. Generics  (0) 2021.02.24
2. 슈퍼 타입 토큰  (0) 2021.02.20
1. 재사용성과 다이나믹 디스패치, 더블 디스패치  (0) 2021.02.20