제네릭: 타입 파라미터와 와일드 카드

홍혁준·2023년 4월 7일
1

generic

목록 보기
1/2

제네릭이란?

제네릭이 도입되기 전에, 여러 타입을 사용하는 클래스와 메서드에서 Object를 사용했습니다. Object는 최상위 클래스이기에 모든 데이터를 받을 수 있기 때문이었지요.

하지만 반환된 객체를 사용하려면 다시 원하는 타입으로 캐스팅해야했죠.

이 타입 캐스팅(Obejct를 캐스팅하는 경우)의 경우 컴파일타임에 잡을 수 없고, 런타임에서만 발견되기에 많은 불편함이 있었습니다.

하지만 JDK 1.5에서 Generic이 도입되면서 이러한 불편점을 해소하였습니다.

Generic의 경우 컴파일 단계에서 이미 파일이 정해지기에, 런타임에 발견될 오류를 미리 컴파일타임에 발견할 수 있었습니다.

자바에선 아래와 같은 두 가지 방법으로 제네릭 타입을 지원합니다.

타입 파라미터

가장 간단하게 타입 파라미터를 이용해서 다양한 타입을 받을 수 있습니다.

일반적으로 타입 파라미터를 generic type이라고 부릅니다.

아래와 같이 타입 파라미터를 사용할 수 있습니다.

public class SimpleArrayList<T> {
    private T[] values;
    
    void add(T value){
    // 값을 추가하는 기능
    }
}

final SimpleArrayList<Integer> integers = new SimpleArrayList<>();
integers.add(1);
integers.add("error"); -> 컴파일 오류 발생!

위처럼 List에 맞지 않는 타입이 들어오면 컴파일 단계에서 에러가 발생한다.

하지만 generic도 만능은 아니다. 모든 SimpleArrayList가 공통적으로 사용하는 메서드를 만들려고 하면 문제가 생긴다.

아래는 모든 원소들을 String 형식으로 append해 반환하는 로직이다.

void test() {
    final List<Integer> integers = List.of(1, 2, 3);
    convertString(integers); --> List<Integer>List<Object>의 하위타입이 아니기에 오류 발생
}

public String convertString(final List<Object> objects) {
    final StringBuilder stringBuilder = new StringBuilder();
    for (final Object object : objects) {
        stringBuilder.append(object.toString());
    }
    return stringBuilder.toString();
}

라이브러리 설계자가 위와 같은 메서드를 내부에서 정의한다면 만들 수 있지만, 프로그래머가 필요에 의해서 메서드를 만들 때는 문제가 된다.

위와 같은 문제를 해결하기 위해 와일드카드가 등장했다.

와일드카드

와일드 카드는 ? 키워드로 사용할 수 있다. ?는 어떤 타입이든 올 수 있는 것을 의미한다.

Object와는 다르다. 아까 위에서 사용햇던 메서드를 아래와 같이 사용할 수 있다.

void test() {
    final List<Integer> integers = List.of(1, 2, 3);
    final String convertedString = convertString(integers);
}

public String convertString(final List<?> objects) {
    final StringBuilder stringBuilder = new StringBuilder();
    for (final Object object : objects) {
        stringBuilder.append(object.toString());
    }
    return stringBuilder.toString();
}

하지만 위와 같은 방법도 문제가 있다. 메서드 내부에서 Object의 메서드인 toString을 사용할 수 있지만 Object의 메서드들이 아닌 다른 메서드를 사용한다면 문제가 된다.

예를 들어 내부 element에서 최대값을 구해 반환하는 메서드를 정의한다고 하자.

@Test
void test() {
    final List<Integer> integers = List.of(1, 2, 3);
    max(integers);
}

public double max(final List<?> values) {
    double max = Double.MIN_VALUE;
    for (final Double value : values) { --> 이 부분에서 오류발생
        max = Math.max(value, max);
    }
    return max;
}

final Double value : values 부분이 문제가 된다. ? 타입이 Double로 캐스팅 될 수 있다는 보장이 없기에 위처럼 사용할 수 없다.

런타임에서 에러가 터지게 두고 허용하게 둘 수도 있지만, 그러면 제네릭을 쓰는 이유가 없기에, 설계자들이 위와 같은 경우 컴파일을 못하게 막아두었다.

그래서 자바에서는 제한된 Generic이라는 기능을 제공해 위와 같은 케이스를 구현 가능하게 한다.

제한된 Generic

제한된 Generic이란 Generic에 제한을 둬서, 모든 타입이 아닌 특정 범위의 변수만 받을 수 있도록 한 것이다.

와일드카드

  • 상한 경계

extends 키워드를 사용하여, 자식 타입만 올 수 있도록 제한을 둘 수 있다.

<? extneds Number> 이처럼 사용하면, Number와 Number의 자식들만 올 수 있다

void test() {
    final List<Integer> integers = List.of(1, 2, 3);
    final List<Double> doubles = List.of(Double.MIN_VALUE);
    final List<String> strings = List.of("1", "2");
    max(integers);
    max(doubles);
    max(strings); ---> 이 부분에서 예외 발생
}

public double max(final List<? extends Number> values) {
	//이 부분에서 로직이 수행된다.
}

max의 파라미터로 List<? extends Number>로 제한을 두었다.

Number의 하위 타입인 List, List을 받는 경우 예외가 생기지 않는다.

하지만, Number의 하위 타입이 아닌 String의 경우에는 컴파일 단계에서 예외가 발생한다.

  • 하한 경계

하한 경계는 상한과 반대다. super 키워드를 이용하여, 자기 자신과 부모타입들만 들어올 수 있도록 제한을 둘 수 있다.

void test() {
    final List<Number> numbers = List.of(1, 2, 3);
    final List<Object> objects = List.of();
    final List<Integer> integers = List.of(1, 2);
    max(nubers);
    max(objects);
    max(integers); ---> 이 부분에서 예외 발생
}

public double max(final List<? super Number> values) {
	//이 부분에서 로직이 수행된다.
}

Number와 Object는 문제가 없지만, integers는 문제가 된다.

타입 파라미터

  • 상한 경계

타입 파라미터 또한 와일드카드와 동일한 문법으로 사용할 수 있다.
<T extneds Number> 이처럼 사용하면, 와일드카드와 동일한 방식으로 사용이 가능하다.

public class SimpleNumberList<T extends Number>{
	... 클래스 내부 구현
}

final SimpleNumberList<Integer> integers = new SimpleArrayList<>();
final SimpleNumberList<Double> doubles = new SimpleArrayList<>();
final SimpleNumberList<String> strings = new SimpleArrayList<>(); --> 여기서 예외 발생

제한을 두는 이유

그러면 제한을 왜 두는 걸까? 앞에서 와일드 카드에서 다뤘던 코드를 다시 보자.

@Test
void test() {
    final List<Integer> integers = List.of(1, 2, 3);
    max(integers);
}

public double max(final List<?> values) {
    double max = Double.MIN_VALUE;
    for (final Double value : values) { --> 이 부분에서 오류발생
        max = Math.max(value, max);
    }
    return max;
}

위 같은 경우 문제가 되는 것이 ?로 온 타입이 Double 형태로 변할지 확인이 안되기 때문에 그런 것이다. 와일드 카드에 제한을 두면 이를 해결 할 수 있다

상한 경계

@Test
void test() {
    final List<Integer> integers = List.of(1, 2, 3);
    max(integers);
}

public double max(final List<? extends Number> values) {
    double max = Double.MIN_VALUE;
    for (final Number number : values) {
        Double value = number.doubleValue();
        max = Math.max(value, max);
    }
    return max;
}

위와 같은 코드는 정상 작동한다. 파라미터에서 <? extends Number>로 제한을 두었기 때문에, values의 element들은 모두 Number거나 Number의 하위 타입이다.

그래서 for (final Number number : values) 와 같은 구문이 가능하다.

하한 경계

상한 경계는 위처럼 사용할 수 있고, 하한 경계는 다음과 같은 경우에 사용될 수 있다.

예를들어 파라미터로 받은 Collection에 int값인 0를 추가하여 반환하는 케이스가 있을 것이다.

void addInt0(Collection<? super Integer> c) {
    c.add(Integer.valueOf(0));
}

만약 단순 ? 형태였다면, int값인 0이 들어갈 수 있는지 판단이 안 되서 오류가 나겠지만, super로 하한 제한을 두었기에, 위와 같이 사용할 수 있다.

정리

generic을 이용해서 유연한 코드를 작성할 수 있으며, 코드의 중복을 줄일 수 있다.

위와 같은 내용을 공부하며 생긴 의문점이 있는데 그 의문점들은 다음 글에 정리해보도록 하겠다.

profile
끊임없이 의심하고 반증하기

0개의 댓글