3 minute read

목표

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

학습할 것 (필수)

제네릭 사용법

제네릭(Generic): JDK 1.5에 도입된 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다.

제네릭에 대해 알아야 할 3가지

  1. 제네릭을 쓰는 클래스(ArrayList 등)의 인스턴스를 만드는 방법 ArrayList를 만들 때는 일반 배열을 만들 때와 마찬가지로 목록에 들어갈 수 있는 객체의 유형을 명시해야 한다.

     new ArrayList<Song>()
    
  2. 제네릭 유형의 변수를 선언하고 변수에 값을 대입하는 방법

     List<Song> songList = new ArrayList<Song>();
    
  3. 제네릭 유형을 인자로 받아들이는 메소드 선언(호출) 방법

     void foo(List<Song> list)
    

제네릭 클래스 사용법

public class ArrayList<E> extends AbstractList<E> implements List<E> ... {
    // 'E' 는 컬레션에 저장하고 컬렉션에서 리턴할 원소의 유형(Element(원소))
    public boolean add(E o) // ArrayList에 집어넣을 수 있는 객체의 유형
    ...
}
// 다음과 같은 코드를 쓰면:
ArrayList<String> strList = new ArrayList<String>

// 다음과 같은 ArrayList가: 
public class ArrayList<E> extends AbstractList<E> ... {
    public boolean add(E o)
    ...
}

// 컴파일러에 의해 다음과 같은 식으로 해석된다: 
public class ArrayList<String> extends AbstractList<String> ... {
    public boolean add(String o)
    ...
}

즉 ‘E’가 ArrayList를 생성할 때 사용한 실제 유형(type parameter)으로 치환 된다. 그래서 ‘E’와 호환되는 유형의 레퍼런스를 제외한 다른 레퍼런스는 ArrayList의 add() 메소드에 전달할 수 없다. ArrayList을 만들었다면 그 add() 메소드는 add(String o)로 변한다.

자바에서 식별자는 보통 한글자만 사용하는 것이 관례로 되어 있고, 컬렉션 클래스를 작성하는 경우(컬렉션에 저장할 원소(Element)의 유형임을 나타내기 좋도록 ‘E’ 사용)를 제외하면 일반적으로 ‘T’를 사용한다.

제네릭 메소드 사용법
제네릭 클래스(generic class)란 클래스 선언에 유형 매개변수가 들어있는 클래스를 뜻하고, 제네릭 메소드(generic method)는 메소드 선언 서명에 유형 매개변수가 포함되어 있는 메소드를 뜻한다.

  1. 클래스 선언부에서 정의된 유형 매개변수를 사용하는 방법
     public class ArrayList<E> extends AbstractList<E> ... {
         public boolean add(E o) // 클래스 정의 시 이미 'E'를 썼기 때문에 'E' 사용 가능
     }
    
  2. 클래스 선언부에서 쓰이지 않은 유형 매개변수를 사용하는 방법
     public <T extends Animal> void takeThing(ArrayList<T> list)
     /*
     클래스 자체에서는 유형 매개변수를 사용하지 않더라도 특별한 위치(리턴)에서 선언해주기만 하면 메소드 내에서 유형 매개변수를 별도로 지정해서 쓸 수 있다. 위와 같이 선언되어 있을 경우, Animal 유형(하위클래스 포 유형 포함)이면 뭐든지 T자리에 들어갈 수 있다.
     */
    

제네릭 주요 개념 (바운디드 타입, 와일드 카드)

  • 바운디드 타입(한정적 타입 매개변수(Bounded Type Parameter)): 제네릭으로 사용될 타입 파라미터의 범위를 제한 할 수 있는 방법

      public class GenericArrayList<T extends Number> // Number의 서브클래스만 타입으로 가지도록 하고 싶은 경우(String 불가)
    
      public class GenericArrayList<T super Number> // Number의 상위클래스만 타입으로 가지도록 하고 싶은 경우
    

    바운디드 타입 파라미터가 사용되는 가장 흔한 예시는 Comparable을 적용하는 경우이다. T extends Comparable 와 같이 정의하면 Comparable 인터페이스의 서브클래스들만 타입으로 사용하겠다는 것이다. Comparable 인터페이스를 구현하기 위해서는 compareTo() 메서드를 반드시 정의해야하기 때문에 Comparable 인터페이스를 구현한 클래스들은 비교가 가능한 타입이 된다.

    비교하는 로직이 들어간 클래스에는 비교가 가능한 타입들을 다루는 것이 맞을 것이다. 이를 강제하도록 할 수 있는게 바운디드 타입 파라미터이다.

  • 와일드 카드

      public void takeAnimals(ArrayList<? extends Animal> animals){
          for(Animal a : animals){
              a.eat();
          }
      }
    

제네릭 메소드 만들기

제네릭 메소드: 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메서드

제네릭 메서드를 선언하는 방법은 리턴 타입 앞에 <> 기호를 추가하고 타입 파라미터를 기술한 다음, 리턴 타입과 매개 타입으로 타입 파라미터를 사용하면 된다.

public <타입 파라미터, ...> 리턴타입 메소드명(매개변수, ...) { ... }

제네릭 메서드는 두가지 방식으로 호출 가능하다. 코드에서 타입 파라미터의 구체적인 타입을 명시적으로 지정해도 되고, 컴파일러가 매개값의 타입을 보고 구체적인 타입을 추정하도록 할 수도 있다.

리턴타입 변수 = <구체적인 타입> 메서드명(매개변수); // 명시적으로 구체적 타입을 지정
리턴타입 변수 = 메서드명(매개변수); // 매개값을 보고 구체적 타입을 추정

Erasure

제네릭 타입소거(Generic Type Erasure): 원소 타입을 컴파일 타임에만 검사하고 런타임에는 해당 정보를 알 수 없는 것. 컴파일 타임에만 타입에 대한 제약 조건을 적용하고, 런타임에는 타입에 대한 정보를 소거하는 프로세스

List<Object> objList = new ArrayList<Long>(); // 컴파일 에러

컴파일 시 타입 오류를 바로 알 수 있다.(리스트도 제네릭 타입으로 구현되어 있음)

Java 컴파일러는 타입소거를 아래와 같이 적용한다.

  • 제네릭 타입(Exmaple) 에서는 해당하는 타입 파라미터(T) 나 Object로 변경해준다. Object로 변경하는 경우는 unbounded 된 경우를 뜻하며, 이는 <E extends Comparable>와 같이 bound를 해주지 않은 경우를 의미한다. 따라서 이 소거 규칙에 대한 바이트코드는 제네릭을 적용할 수 있는 일반클래스, 인터페이스, 메서드에만 해당된다.
  • 타입 안정성 보존을 위해 필요하다면 type casting을 넣어준다.
  • 확장된 제네릭 타입에서 다형성을 보존하기 위해 bridge method를 생성한다.

참고 서적

Head First, Java


live-study with whiteship