[JAVA] 제네릭(Generics)과 와일드카드(Wildcard)

Sia Hwang·2022년 11월 20일
0

JAVA

목록 보기
4/6

What is the Generics?

  • 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(compile-time type check)를 해주는 기능
  • 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

타입 안정성을 높인다는 것 : 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다.

The advantages of Generics

  1. 타입 안정성을 제공한다.
  2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

Why use the Generics?

  • 다양한 종류의 데이터타입을 다루는 코드를 작성할 때, 형변환 코드를 작성할 필요가 없어진다.
  • 가독성이 좋은 코드를 작성할 수 있다.
class Box {
  private Object item;
  public void setItem(Object item) { this.item = item; }
  public Object getItem() { return item; }
}
  • 위와 같이 물건을 담을 수 있는 상자 클래스가 있다고 하자. 이 클래스에 어떤 물건이든 담고 싶어서 아이템의 타입을 모든 클래스의 최고 조상인 Object 타입으로 설정했다. 그런데 이렇게 작성하면 물건을 담을 때엔 문제가 없지만 꺼낼 때에 좀 번거롭다.
Box b = new Box();
b.setItem("new item");
String s = (String) b.getItem(); // 형변환 필요
  • 방금 String 타입의 아이템을 담고 바로 꺼내 쓰려 했는데, 방금 담은 아이템이 String 타입이라는 것은 코드를 쓴 나만 아는 것이지 컴파일러는 모른다. 그렇기 때문에 컴파일러 입장에서는 마지막 줄처럼 String 타입으로 객체에 저장된 아이템을 받을 수 있다고 보장할 수 없다. 그래서 형변환을 해 주어야만 꺼내쓸 수 있다.
  • 보통 IDE에서 형변환 코드를 자동으로 추가해주기는 하지만, 그래도 빨간줄이 뜨니까 마우스 오버해서 뭐가 원인인지 알아본 뒤 형변환하는 코드를 추가해줘야 하니까 번거롭고 또 코드가 지저분해 보이기도 한다.
  • 이 때 제네릭을 사용해서 클래스 코드를 작성하면 훨씬 깔끔한 코드를 작성할 수 있다.
class Box<T> {
  private T item;
  public void setItem(T item) { this.item = item; }
  public T getItem() { return item; }
}

public static void main(String[] args) {
  Box<String> b = new Box();
  b.setItem("new item");
  String s = b.getItem();
}
  • 처음에 만들었던 Box 클래스를 제네릭 클래스로 바꿔보았다. 객체를 생성하는 단계에서부터 클래스에 담을 자료형을 지정해줄 수 있기 때문에 담겨 있는 아이템을 꺼낼 때 굳이 형변환을 하지 않아도 된다. 그리고 코드상으로도 어떤 자료형이 담겨 있는 것인지 명확하게 알 수 있다. 때문에 Object 클래스를 이용했던 코드보다 훨씬 가독성이 좋아졌다.

Terms of Generics

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

Limit of Generics

  1. static 멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스 변수로 간주되기 때문이다.
class Box<T> {
  static T item; // Error!
  static int compare(T t1, T t2) {} // Error!
}
  • 이런 코드는 작성할 수 없다. 클래스가 로드되는 시점에 static 키워드가 붙은 멤버는 메모리에 즉시 할당이 되어야 하는데 자료형이 뭔지 알아야 그에 맞게 할당을 할 것이 아닌가.
  1. 제네릭 타입의 배열을 생성하는 것이 허용되지 않는다.
class Box<T> {
  T[] items; // OK
  T[] items = new T[3]; // Error!
}
  • 첫번째와 같이 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만 두번째와 같이 new 연산자로 배열을 생성하는 것은 할 수 없다.
  • new 연산자는 컴파일 시점에 타입 T가 뭔지 정확하게 알아야 하기 때문이다. 하지만 지금 클래스에 작성된 코드만 봤을 때엔 타입 T가 어떤 타입이 될 지 전혀 알 수 없다. instanceof 연산자도 마찬가지 이유로 T를 피연산자로 사용할 수 없다.
  • 제네릭 배열을 생성해야 한다면 Reflection APInewInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 다음에 T[]로 형변환하는 방법을 사용할 수 있다고 하는데, ArrayList와 같은 컬렉션 클래스를 사용하면 간편하고 가독성도 좋으니까 이 쪽을 사용하자.

Limited Generics class

  • 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있다.
class FruitBox<T extends Fruit> {
  ArrayList<T> list = new ArrayList<>();
}
  • 위와 같이 작성하면 Fruit 타입의 자손만 타입으로 지정할 수 있다.
  • 인터페이스를 구현한 타입으로 지정할 때에도 똑같이 extends 키워드를 사용한다.
interface Eatable {}
class FruitBox<T extends Eatable> {}
  • 만약 클래스 Fruit의 자손이면서 Eatable 인터페이스도 구현해야 한다면 & 기호로 연결한다.
class FruitBox<T extends Fruit & Eatable> {}
  • 이렇게 작성하면 Fruit의 자손이면서 Eatable 인터페이스를 구현한 클래스만 타입 매개변수 T에 대입될 수 있다.

What is the Wildcard?

  • 가끔 자바 코드를 보다 보면 <?>을 볼 수 있다. 이것의 명칭을 와일드카드라 한다.
  • 스태틱 메서드에서 제네릭 타입 매개변수를 사용할 수 없는 점과 같은 제네릭의 한계를 보완한 것이라고 생각하면 이해가 쉬울 것 같다. 와일드카드엔 어떤 자료형이든 대입할 수 있다.
  • 코딩을 하다 보면 스태틱 메서드의 매개변수로 항상 한 가지 자료형으로만 지정할 수는 없는 경우가 생긴다. 이럴 때 예외적으로 허용할 수 있기 위해 만들어진 것이라고 보면 되는 듯 하다.
static Juice makeJuice(FruitBox<?> f) {}
  • 스태틱 메서드에서 매개변수 타입을 하나로 지정할 수 없는 경우 위와 같이 와일드카드를 사용하면 제네릭을 사용한 것과 같은 동작을 할 수 있다.

Limit of Wildcard

  • 와일드카드도 제네릭과 비슷하게 상한과 하한을 적용할 수 있다.

  • <? extends T> : 와일드카드의 상한 제한. T와 그 자손들만 가능

  • <? super T> : 와일드카드의 하한 제한. T와 그 조상들만 가능

  • <?> : 제한 없음. 모든 타입 가능. <? extendx Object>와 동일

  • 하지만 & 기호는 사용할 수 없다. <? extends T & E>는 불가능하다.

static Juice makeJuice(FruitBox<? extends Fruit> f) {}
  • 이렇게 쓰면 매개변수로 Fruit 클래스와 그 자손 클래스들만 가능해진다.

Generic method

class FruitBox<T> {
  static <T> void sort(List<T> list, Comparator<? super T> c) {}
}
  • 메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라 한다.
  • 제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 완전 별개의 것이다. 문자 T만 같은 것을 쓰는 것이지 같은 타입은 아니다.
  • 메서드에 선언된 제네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 되는데, 매개변수에 선언할 타입 T에 대한 자료형을 미리 선언해 주기 때문에 static 메서드에서 타입 매개변수 T의 사용을 가능하게 해 주는 것이다.
  • 위에서 썼던 스태틱 메서드를 제네릭 메서드로 바꾸면 다음과 같이 쓸 수 있다.
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {}
  • 매개변수로 사용할 FruitBox<T>의 타입 매개변수 T에 대한 자료형을 <T extends Fruit>을 통해 지정해 주었다. 그래서 이 메서드가 스태틱 메서드여도 타입 매개변수 T를 사용할 수 있는 것이다.

Last

  • 이제 Collectionssort() 메서드를 이해할 수 있다.
public static <T extends Comparable<? super T>> void sort(List<T> list)
  • 타입 T를 요소로 하는 매개변수를 허용하되,
  • T는 Comparable을 구현한 클래스여야 하며 T 또는 그 조상의 타입을 비교하는 Comparable이어야 한다.
  • 만약 T가 Student이고 Person의 자손이라면 <? super T>Student, Person, Object가 가능하다.

References

profile
당면한 문제는 끝까지 해결하기 위해 노력하는 주니어 개발자입니다.

0개의 댓글