Java Generic

제네릭

  • 데이터 타입을 일반화 하는 것
  • 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법
  • 컴파일 시 type check를 함으로써 클래스나 메서드 내부에서 사용되는 객체의 타입의 안정성을 높이고, 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.
  • Java 5 이전에는 여러 타입을 사용하는 대부분의 클래스나 메서드에서 인수나 반환값으로 Object 타입을 사용했었다. 이 경우 반환된 Object 객체를 다시 원하는 타입으로 cast 해야하고 이때 오류가 발생할 가능성도 생긴다.
  • Java5 부터 도입된 제네릭을 사용하면 컴파일 시에 미리 타입이 정해지므로, 타입검사나 타입변환과 같은 번거로운 작업을 생략할 수 있게 된다.

제네릭을 사용하는 이유

  • 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거
  • 실행 시 발생할 수 있는 타입 에러를 컴파일 시 미리 강하게 체크하여 사전에 에러를 방지
  • 타입 변환을 할 필요가 없어 프로그램 성능이 향상

제네릭 사용법

제네릭 선언 및 생성

class DemoGeneric<T> {
    T element;
    void setElement(T element) {
        this.element = element;
    }
    T getElement() {
        return element;
    }
}

타입 변수

  • 클래스 명 옆에 로 DemoGeneric이라는 클래스 내부에서 T를 사용할 수 있게 된다.
  • T는 클래스 내부에서 타입 매개변수를 대표하는 값으로 사용된다.
  • 타입변수는 어떠한 문자를 사용해도 되지만 네이밍 규칙을 지켜주는 것이 좋다.
  • E(element), K(key), N(number), T(type), V(value), R(return type)

class GenericTest {

    static class DemoGeneric<E, V> {
        E element;
        V value;
        void setElement(E element) {
            this.element = element;
        }
        E getElement() {
            return element;
        }
        void setValue(V value) {
            this.value = value;
        }
        V getValue() {
            return value;
        }
    }

    public static void main(String[] args) {
        DemoGeneric<String, Integer> gr = new DemoGeneric<>();
        gr.setElement("Test");
        gr.setValue(27);
        System.out.println(gr.getElement());
        System.out.print(gr.getValue());
    }
}

제네릭 메서드 생성

public class GenericTest {

    public static <T> T genericMethod(T t) {
        return t;
    }

    public static <T> void printAll(List<T> list) {
        for(T t : list) {
            System.out.print(t);
        }
    }

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        System.out.println(genericMethod("Test"));
        printAll(list);

    }
}
  • 제네릭 클래스에서는 해당 클래스 내부에서 사용할 타입 파라미터가 무엇인지 알려주기 위해 class를 선언할 때 알려주었다면 제네릭 메서드에서는 메서드를 정의할 때, 해당 메서드 내부에서 사용할 타입 파라미터가 무엇인지 알려주기 위해 메서드를 정의할 때 먼저 나열해주고 사용해야 한다. 즉, 리턴타입을 명시하기 전에 작성되어야 한다.

바운드 타입 매개변수

  • 바운드타입은 특정 타입의 서브타입으로 제한한다. 클래스나 인터페이스를 설계할 때 가장 흔하게 사용된다.
public class BoundTypeTest <T extends Number>{
    public void set(T value) {}

    public static void main(String[] args) {
        BoundTypeTest<Integer> boundTypeTest = new BoundTypeTest<>();
        BoundTypeTest.set("Hi");
    }
}
  • BoundTypeTest 클래스의 Type 파라미터를 T로 선언하고 로 선언하여 BoundTypeTest의 타입으로 Number의 서브타입만 허용한다.
  • BoundTypeTest 클래스 내부 메서드 set의 인자에 T 타입을 사용하면 Number의 서브타입만 허용하기 때문에 문자열을 전달하려고 하면 컴파일 에러가 발생한다.

WildCard

  • 제네릭으로 구현된 메서드의 경우 선언된 타입으로만 매개변수를 입력해야한다. 이를 상속받은 클래스 혹은 부모클래스를 사용하고 싶어도 불가능하고 어떤 타입이 와도 상관없는 경우 대응하기 좋지않다. 이를 위해 WildCard를 사용한다.

Unbounded WildCard

  • List<?> 와 같은 형태로 물음표만 가지고 정의된다. 내부적으로 Object로 정의되어서 사용되고 모든 타입의 인자를 받을 수 있다.
  • 내부적으로 Object로 정의되어서 사용되고 모든 타입의 인자를 받을 수 있다. 타입 파라미터에 의존하지 않는 메서드만을 사용하거나 Object 메서드에서 제공하는 기능으로 충분한 경우에 사용한다.
  • Object 클래스에서 제공되는 기능을 사용하여 구현할 수 있는 메서드를 작성하는 경우나 타입 파라미터에 의존하지 않는 일반 클래스의 메서드를 사용하는 경우에 사용됨

Upper Bounded WildCard

  • <? extends Foo> 와 같은 형태로 사용하고, 특정 클래스의 자식 클래스만을 인자로 받는다. 임의의 Foo 클래스를 상속받는 어떤 클래스가 와도 되지만 Foo 클래스에 정의된 기능만 사용가능하다.

Lower Bounded WildCard

  • <? super Foo> 와 같은 형태로 사용하고, Upper Bounded WildCard와 다르게 특정 클래스의 부모 클래스만을 인자로 받는다.

Erasure

  • 컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고 필요한 곳에 형변환을 넣어준다.
  • 즉, 컴파일된 파일에는 제네릭 타입에 대한 정보가 없다. 이렇게 하는 주 목적은 하위 호환성에 있다.
  • 제네릭은 타입 파라미터에 primitive 타입을 사용하지 못하는데 그 이유는 타입 소거(type Erasure) 때문이다.
public class GenericTest() {
    List<Integer> list = new ArrayList<>();
}
public class GenericTest {

  // compiled from: GenericTest.java

  // access flags 0x0
  // signature Ljava/util/List<Ljava/lang/Integer;>;
  // declaration: list extends java.util.List<java.lang.Integer>
  Ljava/util/List; list

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 4 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 5 L1
    ALOAD 0
    NEW java/util/ArrayList
    DUP
    INVOKESPECIAL java/util/ArrayList.<init> ()V
    PUTFIELD GenericTest.list : Ljava/util/List;
    RETURN
   L2
    LOCALVARIABLE this LGenericTest; L0 L2 0
    MAXSTACK = 3
    MAXLOCALS = 1
}

  • 바이트 코드를 살펴보면 ArrayList가 생성될 때 타입정보가 나오지 않는다. 제네릭을 사용하지 않고 raw type으로 ArrayList를 생성해도 동일한 바이트 코드를 볼 수 있다.
  • 내부에서 타입 파라미터를 사용할 경우 Object 타입으로 취급하여 처리된다.
  • 제네릭 타입이 특정 타입으로 제한되어 있을 경우 해당 타입에 맞춰 컴파일시 타입 변경이 발생하고 타입 제한이 없을 경우 Object 타입으로 변경된다. 이를 타입소거(type Erasure)라 한다.
  • 이는 제네릭을 사용하더라도 하위 버전에서 동일하게 동작해야하기 때문에 하위 호환성을 지키기 위해 만들어졌다.
  • 원시 타입을 사용하지 못하는 것도 바로 이 기본 타입은 Object 클래스를 상속받고 있지 않기 때문이다. 그래서 기본 타입 자료형을 사용하기 위해서는 Wrapper 클래스를 사용해야 한다.

Unbounded WildCard 타입소거

public class Node<T> {
    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() {
        return data;
    }
}



// 위 코드는 아래와 같다고 볼 수 있다.
public class Node {
    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() {
        return data;
    }
}

Bounded WildCard 타입소거

public class Node<T extends Comparable<T>> {
    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() {
        return data;
    }
}



// 위 코드는 아래와 같다고 볼 수 있다.
public class Node {
    private Object data;
    private Comparable next;

    public Node(Object data, Comparable next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() {
        return data;
    }
}

메서드 타입소거

public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray) {
        if (e.equals(elem)) {
            cnt++;
        }
    }
    return cnt;
}



// 위 코드는 아래와 같다고 볼 수 있다.
public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray) {
        if (e.equals(elem)) {
            cnt++;
        }
    }
    return cnt;
}

Categories:

Updated:

Comments