자바 기본기 다지기 - 제네릭

jungwoo jo·2021년 8월 31일
0

자바 기본기

목록 보기
9/9

제네릭(Generic)

JDK1.5부터 새로 추가된 기능으로 제네릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(compile-time type check)를 해주는 기능이다.

제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때마다 형변환을 해야 했다. 그래서 누군가 실수로 엉뚱한 타입의 객체를 넣어두면 런타임에 형변환 오류가 나곤 했다. 반면, 제네릭을 사용하면 컬렉션이 담을 수 있는 타입을 컴파일러에게 알려주게되어 엉뚱한 타입의 객체를 차단해 더 안전하고 명확한 프로그램을 만들어준다.
또한 제네릭을 모른다면 자바 API 문서를 제대로 볼 수가 없다. 그렇기 때문에 제네릭은 꼭! 알고 가야한다.

또한 제네릭을 사용하지 않다면..
입력 받는 데이터 타입을 알 수 없을 때, Object 타입으로 데이터를 받고 매번 형변환을 통해 처리해줘야 하기 때문에 성능 저하가 발생하게 된다.

특징

  • 컴파일 시 강한 타입 체크가 가능하다.(타입 안전성 제공)
  • 타입 체크와 타입 변환(casting)을 제거한다.(코드가 간결해짐)

타입 안정성: 의도하지 않은 타입의 객체가 저장되는 것을 막고 저장된 객체가 원래의 타입과 다른 타입으로 형변환되는 것을 방지

제네릭 사용 방법

class Box<T> {}

위 코드는 제네릭을 사용한 클래스의 예시이다. 각 항목들은 다음을 표현한다.

  • Box<T> : 제네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다.
  • T : 타입 변수 또는 타입 매개변수. (T는 타입 문자)
  • Box : 원시 타입(raw type)

제네릭 타입에 원하는 타입을 지정하려면 다음과 같이 선언해준다.

Box<String> b = new Box<String>();
  • Box<String> : 제네릭 타입 호출
  • String : 대입된 타입(매개변수화된 타입, parameterized type)

제네릭 사용 전

class Box {
    Object item;
    
    void setItem(Object item) { this.item = item; }
    Object getItem() { return item; }
}

제네릭 사용 후

class Box<T> {  // 제네릭 타입 T를 선언
    T item;
    
    void setItem(T item) { this.item = item; }
    T getItem() { return item; }
}

타입 변수는 T가 아닌 다른 것을 사용해도 된다(사용자 임의 설정). 이런 타입들은 기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미하는 것은 모두 같다.

따라서 이후에 Box클래스의 객체를 생성할 때 참조변수(T) 대신에 생성자에 실제 사용하려는 타입을 지정해준다.

Box<String> b = new Box<String>();  // 타입 T 대신, 실제 타입을 지정
b.setItem(new Object());            // 에러. String 이외의 타입은 지정불가
b.setITem("ABC");                   // 성공. String 타입이므로 가능
String item = b.getItem();          // 형변환 필요 없음

위 예시처럼 참조변수 대신에 String 타입을 지정했을 때는 Box클래스가 다음과 같이 선언된다고 보면 된다.

class Box {
    String item;
    
    void setItem(String item) { this.item = item; }
    String getItem() { return item; }
}

제네릭 제한

제네릭의 타입 제한
한번 특정 인스턴스으로 제네릭 타입 호출에 대입된 타입 외에 다른 타입 사용은 불가능하다.

Box<String> box = new Box<Integer>(); // 에러

다만 자식과 부모 클래스 간 상속관계의 다형성은 적용이 가능하다.

제네릭 불공변

ArrayList<Object> objectList = new ArrayList<String>();  // 컴파일 에러

제네릭은 불공변하기 때문에 String 타입이 Object 타입을 확장했다 하더라도 컴파일 에러가 발생한다. 하지만 제네릭 타입으로 배열을 생성할 때는 컴파일 에러가 발생하지 않는다. 이는 배열이 공변 특성을 갖고 있기 때문이다.

의미자바에서 사용 키워드
공변성(covariant)C[T’]는 C[T]의 하위 클래스이다extends
반공변성(contravariant)C[T]는 C[T’]의 하위 클래스이다super
무공변성(invariant)C[T]와 C[T’]는 아무 관계가 없다제네릭

다음의 예시를 통해 확인할 수 있다.

class Course<T> {
    private String name;
    private T[] students;
    
    public Course(String name, int capacity) {
        this.name = name;
        // 배열은 공변이기 때문에 제네릭 타입으로 형변환한 배열을 생성할 수는 있다.
        // 하지만 이후 배열의 원소 사용이 불가능하다.
        students = (T[]) (new Object[capacity]);
    }

    // 배열 초기화 시 클래스 타입을 추가해줘야 한다.
    public Course(String name, int capacity, Class<T> tClass) {
        this.name = name;
        /* 
        제네릭은 타입을 알수 없기 때문에 어쩔수 없이 배열 정보를 얻어오려면 
        만드는 객체자체를 원하는 타입으로 받아와야 함..
        그래서 생성자에 클래스 타입을 받는 방법(Reflection API)으로 해결은 가능하다
        */
        students = (T[]) Array.newInstance(tClass, capacity);
    }

    public String getName() {
        return name;
    }

    public T[] getStudents() {
        return students;
    }

    public void add (T t) {
        for(int i=0;i<students.length;i++) {
            if (students[i] == null) {
                students[i] = t;
                break;
            }
        }
    }
}

멀티 타입 파라미터

제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있다.

public class Product<T, M> {
    private T kind;
    private M model;
}

제네릭 주요 개념

바운디드 타입(bounded type)

public <T extends 상위타입> 리턴타입 메서드(매개변수, ...) { ... }

타입 파라미터에 지정되는 구체적인 타입을 제한할 수 있다. 위 예시의 T 타입변수는 extends한 상위타입 이하의 타입만 지정할 수 있게 제한 한다.

와일드카드 타입(wildcard type)

Unbounded Wildcards : <?>

  • 제한 없음. 모든 타입이 가능. <? extends Object>와 동일
  • 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.

Upper Bounded Wildcards : <? extends T>

  • 와일드 카드의 상한 제한. T와 그 자식들만 가능
  • 상위 클래스 제한

Lower Bounded Wildcards : <? super T>

  • 와일드 카드의 하한 제한. T와 그 부모들만 가능
  • 하위 클래스 제한

위 클래스간의 상속 관계를 통해 와일드 카드 방법을 자세하게 설명한다.

만약 수업을 듣는 수강생들의 타입을 제네릭으로 지정한다면 다음과 같이 지정할 수 있다.

Course에 ?로 사용하려는 타입 즉 수강생들을 나타내는 클래스(Person, Worker, Student, HighStudent)가 들어온다.

  • Course<?>
    수강생은 모든 타입(Person, Worker, Student, HighStudent)이 될 수 있다.
  • Course<? extends Student>
    수강생은 Student와 HighStudent만 될 수 있다.
  • Course<? super Worker>
    수강생은 Worker와 Person만 될 수 있다.

제네릭 메서드

메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라 한다. 제네릭 메서드는 메서드 리턴 타입 앞에 <> 기호를 추가하고 타입 파라미터를 기술하고 리턴 타입과 매개 타입으로 타입 파라미터를 사용한다. (Collections.sort()가 대표적 제네릭 메서드이다.)

static <T> void sort(List<T> list, Comparator<? super T> c)

제네릭 메서드는 제네릭 클래스가 아닌 클래스에도 정의가 가능하다.
위에서 언급한 바운디드 타입, 와일드 카드 타입 모두 적용이 가능하다.

static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
    ...
}

제네릭 타입의 제거(Erasure)

컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다.
그리고 제네릭 타입을 제거한다. 즉 컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없다.

이렇게 하는 주된 이유는 제네릭이 도입되기 이전의 소스 코드와 호환성을 유지하기 위해서이다. JDK1.5부터 제네릭이 도입되었지만 아직도 원시 타입을 사용해서 코드를 작성하는 것을 허용한다.(되도록 원시 타입을 사용하지 말자)

실제 제네릭이 제거되는 과정

1.제네릭 타입의 경계(bound)를 제거한다.
<T extends Fruit> 일때 T는 Fruit로 치환된다. <T>인 경우에는 Object로 치환된다.

class Box<T extends Fruit> { -> class Box
    void add(T t) { -> void add(Fruit T) {
        ...
    }
}

2.제네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
와일드 카드가 포함된 경우 역시 적절한 타입으로 형변환이 추가된다.

T get(int i) { -> Fruit get(int i) {
    return list.get(i); -> return (Fruit)list.get(i);
}

위 예시는 List의 get()이 Object타입을 반환하기 때문에 형변환이 필요하다.

제네릭은 단순히 프로그래머가 좀더 편하게 개발할 수 있도록 지원해주는 기능으로 결국 컴파일 과정에서 컴파일러에 의해 모든 형변환이 이뤄진다.


이 글은 자바 언어에 대한 기본기를 다지기 위해 작성하는 글입니다.
글에서 잘못되거나 추가되어야 하는 내용 관련 사항은 jungwoo5759@gmail.com 로 공유해주시면 감사하겠습니다.
해당 글을 참고하시거나 퍼가실 때는 출처 링크 부탁드립니다 :)

참고
1. 백기선-자바스터디
2. 이것이 자바다 - 신용권의 Java 프로그래밍 정복
3. 자바의 정석 책 3rd Edition
4. stackoverflow-Generics, arrays, and the ClassCastException
5. stackoverflow-How to create a generic array? [duplicate]
6. Java 제네릭과 가변성 1편
7. JAVA 제네릭 배열을 생성하지 못하는 이유

profile
개발이 즐거운 사람

0개의 댓글