Java - Generics (제네릭)

JS · 18 Oct 2020

Java의 Generics는 여러 타입을 지원하는 클래스를 정의하는 방법입니다.

컴파일될 때 Type checking을 하기 때문에 형변환을 하지 않아도 되고, 버그를 줄일 수 있습니다.

이 글에서는 Generics에 대해서 간단히 알아보고, Generics를 사용하는 방법에 대해서 알아보겠습니다.

Generics를 사용하는 이유

다음과 같이 Java의 List는 Object 형태로 데이터를 저장하기 때문에 어떤 타입이든 저장할 수 있습니다.

List list = new ArrayList();
list.add("Hello world");

List는 Object 형태로 저장하기 때문에 어떤 타입의 데이터가 저장되어있는지 알 수 없습니다. 그렇기 때문에 get()을 호출하면 Object가 리턴됩니다.

따라서, List에서 데이터를 가져올 때는 다음과 같이 형변환을 해줘야 합니다. 형변환을 하지 않으면 컴파일 에러가 발생합니다.

List list = new ArrayList();
list.add("Hello world");
String hello = (String) list.get(0);

이제 List의 Generics로 위의 코드를 구현해보겠습니다. String 타입의 데이터를 저장하는 리스트는 List<String>으로 선언할 수 있습니다. 이 List는 String을 저장하기 때문에 데이터를 가져올 때 String으로 리턴됩니다. 그렇기 때문에 형변환을 하지 않아도 됩니다.

List<String> list = new ArrayList<String>();
list.add("Hello world");
String hello = list.get(0);

위 두개의 예제를 보고, Generics를 사용하면 안정적이라는 것을 느낄 수 있습니다.

Generics의 장점은 다음과 같습니다.

  • 여러 타입을 지원하는 클래스를 추상화할 수 있다.
  • 형변환을 하지 않아도 된다.
  • 컴파일 타임에 Type checking을 할 수 있다.

Generics 구현

Generics는 method를 정의하거나 class를 정의할 때 적용할 수 있습니다.

Method에 Generics 적용

Method에서 Generics의 타입을 표시할 때는 <T>처럼 리턴 타입 앞에 정의합니다. 타입 T는 컴파일 타임에 실제 타입으로 결정됩니다.

public <T> void printListItem(List<T> list) {
    for (T item : list) {
        System.out.println(item);
    }
}

위 메소드는 다음과 같이 쓰일 수 있습니다.

List<String> list = new ArrayList<String>();
list.add("Hello~");
list.add("world!");
printListItem(list);

위의 코드에서 T는 String이기 때문에, printListItem() 함수는 다음과 같이 컴파일이 됩니다.

public String void printListItem(List<String> list) {
    for (String item : list) {
        System.out.println(item);
    }
}

실행 결과는 다음과 같습니다.

Hello~
world!

이렇게 컴파일 타임에 실제 타입으로 코드가 변경되기 때문에 형변환을 하지 않아도 되고, Type checking을 하기 때문에 실수로 잘못된 타입이 객체에 대입되는 일은 발생하지 않게 됩니다.

Class에 Generics 적용

클래스에 Generics를 적용할 수 있습니다.

타입은 다음과 같이 클래스 이름 뒤에 <T>처럼 정의합니다. 타입 T는 컴파일 타임에 실제 타입으로 변경됩니다.

public class Hello<T> {
    private T data;

    Hello(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

위의 클래스는 다음과 같이 사용될 수 있습니다.

Hello<String> helloStr = new Hello<String>("Hello~");
System.out.println(helloStr.getData());

위의 코드가 컴파일 될 때 Hello<String>는 다음과 같이 컴파일 됩니다.

public class Hello {
    private String data;

    Hello(String data) {
        this.data = data;
    }

    public String getData() {
        return data;
    }
}

실행 결과는 다음과 같습니다.

Hello~

2개 이상의 타입 적용

두개 이상의 다른 타입으로 Generics를 적용할 수 있습니다.

다음은 타입 T, S를 적용한 클래스 예제입니다. <T, S>처럼 선언합니다.

public class World<T, S> {
    private T data;
    private S data2;

    World(T data, S data2) {
        this.data = data;
        this.data2 = data2;
    }

    public T getData() {
        return data;
    }

    public S getData2() {
        return data2;
    }
}

위 클래스는 다음과 같이 사용될 수 있습니다.

World<String, Integer> world = new World<>("Hello~", 1234);
System.out.println(world.getData());
System.out.println(world.getData2());

위 코드에서 World 클래스는 다음과 같이 컴파일됩니다.

public class World {
    private String data;
    private Integer data2;

    World(String data, Integer data2) {
        this.data = data;
        this.data2 = data2;
    }

    public String getData() {
        return data;
    }

    public Integer getData2() {
        return data2;
    }
}

Bounded Generics

Bounded Generics는 사용될 수 있는 타입을 제한하는 방법입니다.

다음과 같이 <T extends Number>처럼 사용하면 "T는 Number 클래스를 상속받는 클래스만 될 수 있다"라는 의미입니다.

public <T extends Number> void printListItem(List<T> list) {
    for (T item : list) {
        System.out.println(item);
    }
}

실제로 다음과 같은 코드는 컴파일 에러가 발생합니다. String은 Number를 상속하지 않기 때문입니다.

List<String> list = new ArrayList<String>();
list.add("Hello~");
list.add("world!");
printListItem(list);

다음과 같은 코드는 컴파일됩니다. Double은 Number를 상속하는 클래스이기 때문입니다.

List<Double> list = new ArrayList<Double>();
list.add(new Double(1.234));
list.add(new Double(5.678));
printListItem(list);

Output:

1.234
5.678

Multiple Bounded Generics

위의 Bounded Generics에서는 클래스 하나에 대해서 제한을 하였습니다.

1개의 클래스를 상속받고 1개 이상의 인터페이스를 구현하는 클래스로 제한하고 싶을 때 다음과 같이 & 뒤에 인터페이스를 추가할 수 있습니다.

public <T extends Number & MyInterface> void printListItem(List<T> list) {
    for (T item : list) {
        System.out.println(item);
    }
}

Wildcard Generics

Wildcard는 물음표(?)를 의미합니다. Wildcard Generics는 List<? extends Number>와 같은 표현식을 의미합니다.

아래 코드를 한번 보시죠. 자바의 Double은 Number를 상속합니다. Number가 supertype이고, Double은 subtype입니다.

그렇기 때문에 아래 코드는 문제가 없고, 자연스럽게 보입니다.

Double numDouble = 1.1;
Number number = numDouble;

하지만 아래 코드는 컴파일 에러가 발생합니다. Double이 Number를 상속하지만 Generics로 구현된 List<Double>List<Number>를 상속하지 않기 때문입니다.

List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
List<Number> numbers = doubles; // compile error

List<Double>List<Number>를 상속하는 구조라고 정의하고 싶을 때 Wildcard Generics를 사용합니다.

아래 코드에서 List<? extends Number>List<Double>List<Number>를 상속하는 구조라고 정의하는 것입니다. 그렇기 때문에 아래 코드는 컴파일 에러가 발생하지 않습니다.

List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
List<? extends Number> numbers = doubles;

Wildcard Generics는 이런 경우에 쓰이는데, 이해하기가 쉽지는 않습니다.

이것과 관련된 내용으로 Java의 Generics에서 Covariance, Contravariance 개념 이해하기라는 글이 있는데, 한번 읽어보시면 좋을 것 같습니다.

댓글을 보거나 쓰려면 이 버튼을 눌러주세요.
codechachaCopyright ©2019 codechacha