14회차. 제네릭

KIMA·2023년 2월 19일
0
post-thumbnail

목표

자바의 제네릭에 대해 학습한다.

학습할 것

제네릭이란?

자바5 버전 이전에는 클래스나 메소드에서 매개변수나 반환값으로 다양한 타입을 사용하는 경우 Object 타입을 사용했다.
하지만 이 경우에는 반환된 Object 객체를 다시 원하는 타입으로 형변환해야하고, 이때 오류가 발생할 가능성도 생긴다.
따라서 자바 5버전부터 제네릭이 도입되어, 다양한 타입의 객체들을 다루는 메소드나 컬렉션 클래스에 컴파일 시의 타입체크를 해준다.
컴파일 시의 타입체크란 다양한 타입이 실제 대입된 타입으로 컴파일시간에 치환된다는 뜻이다.

예를 들어, ArrayList와 같은 컬렉션 클래스는 다양한 타입의 객체를 담을 수 있긴 하지만, 꺼낼 때 마다 타입체크를 하고 형번환을 하는 것은 불편하다. 또한, 원하지 않은 타입의 객체가 포함되는 것을 막을 방법이 없다. 이러한 문제를 제네릭이 해결해준다.

  • 컴파일 시에 타입 체크할 경우의 장점
    • 첫째, 다룰 객체의 타입을 미리 명시해줌으로써, 의도하지 않은 타입의 객체가 저장되는 것을 막아 타입 안정성을 높일 수 있다.
    • 둘째, 다룰 객체의 타입을 미리 명시해줌으로써, 타입 체크 및 반환값의 형변환에 들어가는 노력을 줄일 수 있다.

제네릭 클래스 선언, 생성 및 사용

선언

class GenericSample {
  Object element;
  
  void setElement(Object element) {
    this.element = element;
  }
  
  Object getElement() {
    return element;
  }
}

위의 코드는 제네릭을 사용하면 아래와 같다.

class GenericSample<T> {
  T element;
  
  void setElement(T element) {
    this.element = element;
  }
  
  T getElement() {
    return element;
  }
}
  • 타입 변수
    위의 코드에서 T를 타입 변수, GenericSample<T>를 제네릭 클래스, GenericSample를 원시 타입이라고 한다.
    타입 변수란 임의의 참조형 타입을 의미한다.
    • 타입 변수는 클래스와 인스턴스 멤버에만 선언할 수 있다.
      • static 멤버는 타입 변수에 지정된 타입의 종류에 상관없이 항상 동일한 것이기 때문이다.
    • 타입 변수명은 어떻게 지어도 상관없지만 아래의 네이밍 규칙을 지키는 것이 좋다.
      • T : Type
      • E : Element
        • 자바 컬렉션에서 주요 사용된다.
      • K : Key
      • V : value
      • N : Number
    • 타입 변수가 여러 개인 경우에는 Map<K, V>와 같이 ,를 구분자로 나열하면 된다.
    • 타입 변수를 지정해주지않으면, 안전하지 않다는 경고가 발생한다.

생성

타입 변수대신 실제 타입을 지정한다.

GenericSample<String> genericSample = new GenericSample<String>();
  • GenericSample<String>은 컴파일 후에 이들의 원시 타입인 GenericSample로 바뀌고 타입 변수는 모두 Object으로 치환된다. 그리고 String 타입으로 형변환되는 코드가 추가된다.
  • 참조 변수와 생성자에 대입된 타입은 완벽히 일치해야한다.
    • 상속 관계여도 안된다.
  • JDK 7버전 이후로는 참조변수의 타입을 이용한 타입 추정을 사용하여 생성자에 대입된 타입을 생략할 수 있게 되었다.
    GenericSample<String> genericSample = new GenericSample<>();

사용

지정해준 타입만 사용할 수 있다.

genericSample.setElement(new Object()); // String이외의 타입 사용불가
genericSample.setElement("HELLO") // String 타입이므로 가능
String s = genericSample.getElement(); // 형변환이 필요없음

바운디드 타입

제네릭 타입 변수에 extends를 사용하면, 특정 타입의 자손들만 대입될 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit> {
  ArrayList<T> fruitList;
  
  void addFriut(T fruit) {
    fruitList.add(fruit);
  }
}
FruitBox<Apple> appleBox = new FruitBox<>();
FruitBox<Toy> toyBox = new FruitBox<>(); // ❌
  • 인터페이스를 구현해야 한다는 제약이 필요하면, 이때도 extends를 사용한다.
  • 제약사항이 여러개인 경우, &로 연결한다.

와일드 카드

? 기호로 표현하며, 어떠한 타입도 될 수 있다는 의미이다.

  • <? extends T> : T와 그 자손들만 가능

  • <? super T> : T와 그 조상들만 가능

  • <?> : 제한 없음, <? extends Object>와 동일

  • 와일드 카드에서는 여러개의 제약사항을 &로 연결할 수 없다.

제네릭 메소드

메소드의 선언부의 반환 타입 앞에 제네릭 타입이 선언된 메소드를 제네릭 메소드라고 한다.

class FruitBox<T> {
	static <T> void sort(List<T> list, Comparator<? super T> c)
}

앞에서 static 멤버에는 타입 변수가 올 수 없다고 했다.
따라서 위의 코드에서 제네릭 클래스 FriutBox에 선언된 타입 변수 T와 제네릭 메소드 sort()에 선언된 타입 변수 T는 문자만 같을 뿐 서로 다른 것이다.
그렇다면 제네릭 메소드에 선언된 타입 변수는 기존의 타입 변수와는 어떤 의미가 있는것일까?

메소드에 선언된 제네릭 타입은 지역 변수와 같이 매서드 내에서만 지역적으로 사용된다.

  • 제네릭 메소드 사용하기 전

    static Juice makeJuice(FruitBox<? extends Fruit> box) {
      String tmp = ""
      for(Fruit f : box.getList()) tmp += f + " ";
      return new Juice(tmp);
    }
  • 제네릭 메소드 사용한 후

    class Juicer {
    	static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
        String tmp = ""
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
      }
    }
    FruitBox<Apple> appleBox = new FruitBox<Apple>();
    Juicer.<Apple>makeJuice(appleBox);
    • appleBox의 선언부를 보고 컴파일러가 타입을 추정할 수 있기 때문에 <>를 생략 가능하다.
      Juicer.makeJuice(appleBox);
  • static 메소드도 제네릭 메소드로 만들 수 있다.

Reference

  • 자바의 정석 3rd Edition, 남궁성 지음
profile
안녕하세요.

0개의 댓글