[STUDY HALLE] 14주차 과제: 제네릭

3 minute read

학습 목표

자바의 제네릭에 대해 학습하세요.

  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • 제네릭 메소드 만들기
  • Erasure

Deadline: ~2021/02/27 13:00

제네릭

제네릭은 클래스, 인터페이스를 정의할 때 타입을 인자로 전달하여 사용할 수 있도록 하는 방법으로, 컴파일 타임에 강한 타입 체크를 하여 에러를 사전에 방지하고, 불필요한 타입 변환을 생략할 수 있도록 해준다. 제네릭을 사용하는 대표적인 예시는 ArrayList인데, 여기서 설명을 위해 어떤 타입의 원소든 저장하는 ArrayList를 정의했다.

public class ArrayList {
    private int size;
    private Object[] arr = new Object[5];

    public void add(Object value) {
        arr[size++] = value;
    }

    public Object get(int idx) {
        return arr[idx];
    }
}

// Main
public class Main {
    public static void main(String[] args){
        ArrayList list = new ArrayList();
        list.add(50);
        list.add("100");

        Integer value1 = (Integer) list.get(0);
        Integer value2 = (Integer) list.get(1);

        System.out.println(value1 + value2);
    }
}
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
	at com.company.Main.main(Main.java:12)

ArrayList의 배열을 Object로 선언했기 때문에, 어떤 타입의 자료든 들어올 수 있다. 따라서 위의 코드가 빌드되면서는 에러가 발생하지 않지만, 실제로 실행했을때 String타입의 “100”이라는 자료를 Integer로 변환하고자 했기 때문에 에러가 발생하게 된다. 이 때 제네릭을 사용하면 컴파일러가 형변환 코드를 적절하게 추가해주고, 지정한 타입과 다른 타입의 참조 변수를 사용하면 오류를 발생시켜준다.

public class example <T>{
    T data;

    T getData(){
        return data;
    }
    ...
} 

클래스 또는 인터페이스 이름 뒤에 ‘<>’부호를 붙여 표기한다. 이 때, example부분을 ‘클래스 타입’(혹은 원시 타입)이라고 하며, <T>부분을 ‘자료형’이라고 한다. 문자를 반드시 T로 해야하는 것은 아니지만, 제네릭 타입이 무엇을 나타내는지에 따라 이름을 정하는 어느 정도의 규칙이 있으므로 따르는 것이 좋다.

  • E : 요소
  • K : 키
  • N : 숫자
  • T : 타입
  • V : 값
  • S, U, V : 2,3,4번째 선언된 타입

바운디드 타입

바운디드 타입은 제네릭 타입을 특정 타입의 서브타입으로 제한하는 방법이다. 위에서 선언한 ArrayList 클래스에 Double형 자료들만 넣을 수 있도록 제네릭을 이용해서 코드를 고쳐보았다.

public class ArrayList <T extends Number> {
    private int size;
    private Object[] arr = new Object[5];

    public void add(T value) {
        arr[size++] = value;
    }

    public T get(int idx) {
        return (T)arr[idx];
    }
}

// main
public class Main {
    public static void main(String[] args){
        ArrayList<Double> list = new ArrayList();
        list.add(2.1);
        list.add(1.1);

        Double value1 = list.get(0);
        Double value2 = list.get(1);

        System.out.println(value1 + value2);
    }
}

extends 예약어를 통해 Number의 하위클래스(Double)의 자료형만 갖는 ArrayList를 선언하였다. 인터페이스로 제한하더라도 extends 예약어를 사용함에 유의하자. 코드를 보면 Object형의 배열을 선언하여 사용하고 있는데, 제네릭 타입은 new를 사용할 수 없기 때문이다. 왜냐하면 new 연산자는 힙 영역에 메모리 공간을 확보해야하는데, 컴파일 시점에서는 T의 자료형이 무엇인지 알 수 없기 때문에

private T[] arr = new T[5];

이런 코드는 작성할 수 없다. 또한, static변수에도 사용할 수 없다. 생각해보면 static변수는 모든 인스턴스가 공유하는 변수인데, 인스턴스에 따라 타입이 바뀌면 안 되기 때문이다.

와일드 카드

제네릭으로 타입을 선언하면, 메소드는 해당 타입만 매개변수로 받게 된다. 이러한 제한을 해결하기 위해 와일드 카드를 이용하여 부모/자식 클래스를 사용한다.

// Unbounded
List<?> unboundList = new ArrayList<>();
// Upper bounded
List<? extends A> upperList = new ArrayList<>();
// Lower bounded
List<? super B> lowerList = new ArrayList<>();
  • Unbounded : Object로 정의되어 사용되고, 어떤 객체든 받을 수 있다.
  • Upper bounded : 특정 클래스의 자식 클래스만 인자로 받는다.
  • Lower bounded : 특정 클래스의 부모 클래스만 인자로 받는다.

제네릭 메소드

public T get(int idx) {
    return (T)arr[idx];
}

예제를 위해 억지로 만들었지만, 제네릭 메소드이다. 메소드의 선언부에 제네릭을 사용하였다. 반환값이 없는 void 메소드도 반환 타입 앞에 제네릭을 붙여주면 된다.

Erasure

제네릭의 타입 소거는 원소 타입을 컴파일 시기에만 검사하고 런타임에는 해당 타입의 정보를 알 수 없다는 개념이다. 이러한 개념이 나온 이유는, 제네릭 개념을 도입한 이후로 이전 버전의 자바와의 하위 호환성때문이다.

Non-Reifiable Type 런타임에 타입 정보의 유무에 따라 타입을 나누는 개념이다.

  • Refiable type : 런타임에 타입에 대한 정보를 가지고 있다. primitive, non - generic, unbounded wildcards (<?>) 등 이 있다.
  • Non - Reifiable type : 런타임에 타입에 대한 정보가 없다. type erasure 가 진행되는 대부분의 generic parameterized type 이 있다.

참고문헌

https://b-programmer.tistory.com/275

Leave a comment