Generic 이란 무엇일까 ?
혹시 아래 코드들이 어떤식으로 다른지 정확하게 이해하고 있나요 ?
아니라면 이번시간에 차근차근 알아보도록 합시다
public class Generics<T> { // T == type parameter
private T t;
T method(T t) { return null; } // T 를 반환하는 메서드 레벨의 type parameter
static <P> void method1(P t) { } // Generics<T>와 상관없는 메서드 레벨의 type parameter (Parameter 를 받을 때 P 타입을 유추하기 때문에 사용가능)
static <E, P> E method2(P p) { return null; } // E 타입 P 타입을 받아 E 로 반환. 메서드 레벨의 type parameter
static <P extends Objects & Comparable & Comparator> void method1(P t) {} // 이런식으로도 사용할 수 있음 (Multiple Bounded)
void method3(List<T> list) { } // 명시적인 T 타입 제공
static void method4(List<?> list) { } // 와일드 카드
static void method5(List<? extends Comparable> list) { } // 공변 (상위 제한)
static void method6(List<? super Comparable> list) { } // 반공변 (하위 제한)
static void method7(List<T> list) { } // 컴파일 오류 (static 이면 컴파일 시점에 Generics<T> 타입이 정해져야하는데 T가 정해지지 않음)
}
어떤 상황에서 제너릭이 도움이 될까?
아래와 같이 Raw type인 list 를 사용하여, 코딩을 하게되면 실수가 일어날 수 있기 때문에, 우리는 제너릭 정보를 넘겨 컴파일 시점에서 일관성있게 코드를 작성할 수 있다. 보통 우리는 제너릭을 치환에만 많이 사용하고 있다.
* 여기서 Raw type 이란 type parameter 를 넘길수 있도록 클래스가 고려되었지만, type parameter 을 넘기지않는 경우를 의미한다.
before)
public static void main(String[] args) {
List list = new ArrayList(); // raw type List
list.add(1);
list.add("HI");
}
after)
public static void main(String[] args) {
List<Integer> list = new ArrayList(); // raw type List
list.add(1);
list.add("HI"); // 컴파일 에러
}
또한 잘못된 타입으로 캐스팅해서 나는 오류도 런타임이 아닌, 컴파일 시점에서 잡을 수 있다
public static void main(String[] args) {
List list = new ArrayList(); // raw type List
list.add(1);
String str = (String) list.get(0); // Runtime error
}
extends 예약어가 붙어있는 generic 은 어디에 사용하는 걸까?
static void method5(List<? extends Comparable> list) {}
예를 들기위해 Integer 배열을 받아 특정 숫자보다 큰 숫자가 몇개가 되는지 계산하는 함수를 작성해보자.
static long greaterThan(Integer[] integers, Integer element) {
return Arrays.stream(integers).filter(it -> it > element).count();
}
public static void main(String[] args) {
Integer[] integers = new Integer[]{1, 2, 3, 4, 5, 6, 7, 8,};
System.out.println(greaterThan(integers, 3));
}
작성하고 나니, String 클래스에도 적용하고 싶고, 나중에 다양하게 이용이 될 수 도 있을 것 같아서 generic 하게 프로그래밍을 작성한다고 가정해보자. 작성하고 나니 it > element 에서 Operator 메소드가 정의되지 않았다는 에러가 나온다
static <T> long greaterThan(T[] integers, T element) {
return Arrays.stream(integers).filter(it -> it > element).count(); // it > element 컴파일 오류 발생 (T 클래스에 어떤 정보가 들어 있는지 모르기 때문에)
}
public static void main(String[] args) {
String[] strings = new String[]{"a", "b", "c", "d", "e", "f"};
System.out.println(greaterThan(strings, 3));
}
이럴때 사용하는 것이 extends 를 사용하여 T 타입에 대해 제한을 걸어주는 것이다. T 에는 최소한 Comparable 인터페이스를 구현한 놈이 와야한다! 정의해준다. 이러면 말끔하게 해결된다. 이제 다양한 타입에 대해 처리할 수 있도록 되었다.
static <T extends Comparable> long greaterThan(T[] integers, T element) {
return Arrays.stream(integers).filter(it -> it.compareTo(element) > 0).count();
}
public static void main(String[] args) {
String[] strings = new String[]{"a", "b", "c", "d", "e", "f"};
System.out.println(greaterThan(strings, "b"));
}
그리고 실수를 많이 하는 부분이 List<Integer> 의 슈퍼타입이 List<Object> 라고 생각하는 것이다. 하지만 그렇지 않고 List<Integer> 의 슈퍼타입은 Iterable<Integer> 혹은 List<? extend object> 로 표현할 수 있다.
Iterable -> Collection -> List -> ArrayList 오른쪽으로 이동할수록 하위 타입이다
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4);
List<Number> numbers = integers; // 할당 불가능 (두 리스트의 super 타입이 List<Object> 가 아니기 때문)
Collection<Integer> integerCollection = integers; // 할당 가능
Iterable<Integer> integerIterable = integers; // 할당 가능
ArrayList<Integer> integerArrayList = new ArrayList<>();
List<Integer> listInteger = integerArrayList; // 할당 가능
}
위에서 List<? extend object> 로 표현할 수도 있다고 했는데, 실제로 잘 할당된다.
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4);
List<? extends Object> temps = integers; // 정상 작동
}
아래의 두 메소드는 무엇인 다를까 ?
void methodExplicit(List<T> list) { }
void methodWildCard(List<?> list) { } // 코틀린에서는 *
먼저 후자의 ? type parameter 의 의미는 현재 받는 리스트는 안에 어떤 타입의 원소가 들어오든 상관없이 받겠다. 왜냐면 원소와 관련있는 로직은 전혀 작성하거나 사용하지 않을꺼니까! 라는 의미입니다. 따라서 add, addAll 메서드를 사용할 수 없게 됩니다.
add 메서드에 null 값 밖에 들어가지 않고 정수를 할당하려고 하면 "capture of ?" 타입이 들어와야하는데 정수가 들어와서 할당할 수 없다고 에러를 던집니다.
public static void main(String[] args) {
List<?> integers = Arrays.asList(1, 2, 3, 4);
integers.add(null);
}
후자의 T type parameter 정보를 전달한다는 것은 List 안에 있는 원소(Element) 에 관심이 있다는 의미 입니다.
그렇기에 T로 명시적으로 주게되면 add, addAll 같은 메서드를 사용할 수 있습니다.
그래서 리스트를 조회하거나, 삭제등 List 인터페이스에서 제공하는 T를 사용하지 않는 메서드만 사용할 것이라면, List<?> 를 사용하고, 아니라면 List<T> 를 사용하게 됩니다.
예를 들어 List 의 모든 아이템을 조회하는 함수 두개가 있다면 어떤 것을 사용해야할까요?
당연히 타입에 대한 어떠한 행위도 하고 있지 않기 때문에 아래 함수를 사용하는 것이 좋습니다.
static void printAllElement(List<Object> objects) {
objects.forEach( o -> System.out.println(o.toString()));
}
static void printAllElement2(List<?> objects) {
objects.forEach( o -> System.out.println(o.toString()));
}
또 사용해야하는 이유가 하나 더 있습니다.
두번째 함수를 사용하면 정상 실행되고, 첫번째 함수를 실행하면 컴파일 오류가 납니다. Collections.singletonList(integers) 같은 것을 사용할 필요가 없게됩니다.
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4);
printAllElement(integers); // 컴파일 오류 List<Integer> 의 슈퍼타입이 List<Object> 가 아니니
printAllElement2(integers); // 정상 실행
}
이제는 응용해서 좀더 알아보도록 하자.
아래의 두 함수는 기능적으로 완전히 같지만 어떤 것을 사용해야할까 ?
개인의 선택이지만 2번을 사용하는 것이 좋다.
강력한 그 이유 중 하나는 Collection 프레임워크의 내부 frequency 함수가 2번으로 작성이 되어 있기 때문에, 자바에 철학에서 봤을 때는 2번이 맞는 표현이라고 한다.
static <T> long frequencyGeneric(List<T> objects, T object) {
return objects.stream().filter(o -> o.equals(object)).count();
}
static long frequencyWildCard(List<?> objects, Object object) {
return objects.stream().filter(o -> o.equals(object)).count();
}
다른 사례를 보자
리스트를 받아 그 중 가장 큰 숫자를 찾는 함수를 작성해보자
static <T extends Comparable<T>> T max(List<T> list) {
return list.stream().reduce( (ac, e) -> ac.compareTo(e) > 0 ? ac : e).orElseThrow();
}
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(5, 1, 2, 15, 3, 4, 12);
System.out.println(max(integers));
}
잘 작성한 것 같지만, 아직 보완해야할 부분이 남아있다.
max 메서드의 매개변수 타입에 List<T> (무공변) 에는 T의 하위타입이 들어와도 compareTo가 잘 동작하기 때문에 문제가 없다.
그렇기 때문에 max(List<T> list) -> max(List<? extend T> list) 로 공변하게 변경되어야 한다.
그리고 <T extends Comparable<T>> 는 T를 포함한 상위 타입만 받기 위해서 <T extends Comparable<? super T> 로 변경되어야한다.
static <T extends Comparable<? super T>> T max(List<? extends T> list) {
return list.stream().reduce( (ac, e) -> ac.compareTo(e) > 0 ? ac : e).orElseThrow();
}
이제는 Collection 프레임워크에 정의되어 있는 함수와 같은 방식으로 구현이 되었다.
처음에는 Comparable<? super T> 이 부분이 이해가 잘 안되었는데,
일단은 Comparable<Integer> 같은 것을 Comparable<Object>로 사용할 수 있다는 표현이니 실제로 코딩을 해보자.
(a, b) 의 타입은 원래는 Comparator<Integer> 이지만, Comparator<Object> 로 케스팅 해서도 사용가능하다.
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(5, 1, 2, 15, 3, 4, 12);
System.out.println(Collections.max(integers, (a,b) -> a.compareTo(b)));
System.out.println(Collections.max(integers, (Comparator<Integer>) (a,b) -> a.compareTo(b)));
System.out.println(Collections.max(integers, (Comparator<Object>) (a,b) -> a.toString().compareTo(b.toString())));
}
어떤 경우에는 super 를 사용하고, 어떤 경우에 extends 를 사용하는지는 Collections 의 copy 메소드를 보면 자세하게 알 수 있다.
내부에서 소모되는 값에는 extends (아래에서는 src) 외부에서 사용될 값에는 super(아래에서는 dest) 를 붙이면 된다.
Generic 프로그래밍을 하며 자주 만나는 상황?
capture 에러
정수 리스트를 받아 역순으로 출력해주는 함수를 작성해보자
기능에는 문제가 없어보이지만, reverse 내부에서 T 에 대한 정보를 사용하는 부분이 없음을 알 수 있다.
리스트의 크기(size)와 어떤 아이템인지는 모르지만, 원하는 position 에 값을 할당하고(set) , 가져오는 용도(get) 로 만 사용하고 있다.
static <T> void reverse(List<T> list) {
List<T> temp = new ArrayList<>(list);
for (int i = 0; i < list.size(); i++) {
temp.set(i, list.get(list.size() - i - 1));
}
}
그럼 와일드 카드를 사용하도록 변경해보자.
알수 없는 에러를 던지고 좀더 자세하게 에러를 보면
capture#1 타입으로 변경할 수 없다는 오류가 난다. 이럴때 해결책으로는 크게 3가지가 있다.
첫번째로는 다시 T를 명시하는 제너릭 타입으로 돌아가는 것이다. 하지만 이는 기능은 작동하지만 자바의 철학과는 거리가 있다.
두번째로는 와일드 카드를 사용하지만, 중간에 헬퍼 클래스를 두어 값을 캡쳐 할 수 있도록 구현하는 것이다.
static void reverse(List<?> list) {
reverseHelper(list);
}
private static <T> void reverseHelper(List<T> list) { // T 의 값을 캡처링 하게됨
List<T> temp = new ArrayList<>(list);
for (int i = 0; i < list.size(); i++) {
temp.set(i, list.get(list.size() - i - 1));
}
System.out.println(temp.toString());
}
세번째 방법으로는 raw type 을 이용하는 방법이다.
static void reverse(List<?> list) {
List temp = new ArrayList<>(list);
for (int i = 0; i < list.size(); i++) {
temp.set(i, list.get(list.size() - i - 1));
}
System.out.println(temp.toString());
}
'Kotlin & Java' 카테고리의 다른 글
동시성 작업 (0) | 2022.03.27 |
---|---|
람다식, 익명 함수, 일급 함수, 클로저 간단하게 알아보자 (0) | 2020.03.03 |
인터페이스의 정적(static) 메서드와 기본(default) 메서드 (0) | 2020.03.02 |
타입 변환과 instanceof 연산자 Tips (0) | 2020.03.02 |
중첩 클래스 와 내부 클래스 (부제 : ViewHolder 를 이너 클래스로 만들때는 왜 static class 로 만들까?) (0) | 2020.03.01 |