TIL

  1. Generics
  2. 타입변수
  3. 타입변수에 대입하기
  4. 제네릭 용어
  5. 제네릭 타입과 다형성
  6. 제한된 제네릭 클래스
  7. 제네릭의 제약
  8. 와일드카드

 


 

Generics란?

// 이 ArrayList 클래스는 제네릭 아닌 일반 클래스
ArrayList list = new ArrayList();

list.add(new Tv());
Tv t = (Tv) tvList.get(0); // 💡형변환 필요(get 메소드의 반환타입이 Object 클래스)
// 이 ArrayList 클래스는 제네릭 클래스
ArrayList<TV> tvList = new ArrayList<Tv>();

tvList.add(new Tv());    // ok
tvList.add(new Audio()); // 컴파일 에러(Tv 타입 외엔 add 불가)

Tv t = tvList.get(0);    // 💡형변환 불필요(get 메소드의 반환타입이 Tv임)
  • 컴파일 시, 타입을 체크해주는 기능
  • 타입을 알기 때문에 형변환을 생략할 수 있다.
  • 클래스 안에 Object 타입이 있는 것들은 일반 클래스에서 제네릭 클래스로 바뀌었다.
  • 제네릭은 런타임에서 발생할 수 있는 에러들을 컴파일 때 체크할 수 있게 하기위해 탄생한 개념이다.

 

 

 

타입변수

  • 제네릭 클래스를 작성할 때, Object 타입 대신 타입변수(ex. E)를 선언해서 사용한다.
  • 타입변수 이름은 뭘 써도 상관없다. 그러나 어느정도 정해진 규칙이 있긴 하다. (T: Type, E: element)

 

 

 

타입변수에 대입하기

// 타입변수 E 대신 실제 타입 Tv를 대입
ArrayList<Tv> tvList = new ArrayList<Tv>();
  • 객체 생성 시, 타입 변수(E) 대신 실제 타입(Tv)을 지정한다.(대입)
  • 참조변수에 지정 된 타입과 생성자에 지정 된 타입이 같아야 한다.

 

 

 

제네릭 용어

Box<T>   // 제네릭 클래스. T의 BOX 또는 T BOX라고 읽는다.
T        // 타입변수, 타입 매개변수
Box      // 원시타입 (일반클래스를 의미함)

 

 

 

제네릭 타입과 다형성

class Product {}
class Tv extends Product {}    // Product를 상속 받음
class Audio extends Product {} // Product를 상속 받음

ArrayList<Tv> list = new ArrayList<Tv>();        // ok
ArrayList<Product> list = new ArrayList<Tv>(); // error

List<Tv> list = new ArrayList<>(Tv); // ok(다형성)
  • 앞에서 설명했 듯, 참조변수에 지정 된 타입과 생성자에 지정 된 타입이 같아야 한다.
  • 다형성으로 Tv의 조상인 Product로 받는 것도 안 된다.
  • 제네릭 클래스 간의 다형성은 성립한다. (ArrayList를 List로 받기)

 

 

ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv());    // ok
list.add(new Audio()); // ok

Product p = list.get(0); // 💡형변환 필요없음
Tv t = (Tv) list.get(1); // 💡형변환 필요함

매개변수의 다형성은 성립된다. ArrayList 제네릭 클래스의 add() 메소드를 살펴보면 boolean add(E e)로 선언되어 있는데 타입변수 E에 Product가 대입되면 boolean add(Product e)로 바뀐다. 이제 add() 메소드에 Product의 자손 클래스들도 매개변수로 받을 수가 있다.

 

get() 메소드 또한 리턴타입이 Product로 변경되는데 list의 0번째 요소에 있는 객체는 Product 타입이기 때문에 형변환이 필요없고 1번째 요소는 Tv 타입이기 때문에 Tv 타입의 참조변수로 받으려면 형변환이 필요하다.

 

결론적으로 add(), get() 메소드의 E 타입변수가 Product 타입으로 변경되어 매개변수로 Product의 자손들을 받을 수 있기 때문에 매개변수에 다형성이 성립된다.

 

 

 

제한된 제네릭 클래스

/**
* Fruit 클래스의 자손 클래스만 타입변수로 지정 가능
*/
class FruitBox<T extends Fruit> {
    ArrayList<T> list = new ArrayList<T>();
}
class Apple extends Fruit {}
class Toy {}

FruitBox<Apple> applBox = new FruitBox<Apple>(); // ok
FruitBox<Toy> toyBox = new FruitBox<Toy>();         // error(Toy는 Fruit의 자손이 아님)

extends 키워드를 사용하여 대입할 수 있는 타입을 제한할 수 있다.

 

 

interface Eatable {}
class FruitBox<T extends Eatable> {}

인터페이스로 타입변수를 제한하는 경우에도 extends 키워드를 사용한다.
위 코드의 경우, Eatable 인터페이스를 구현한 클래스만 FruitBox 제네릭 클래스의 타입변수를 사용할 수 있다.

 

 

 

제네릭의 제약

class Box<T> {
    static T item; // error
    static int compare(T t1, T t2) { ... } // error
}

static 멤버에 타입변수를 사용하는 것은 불가하다. 왜냐하면 static 멤버는 클래스가 동일하게 공유하는 변수로 제네릭 객체가 생성되기도 전에 이미 자료 타입이 정해져 있어야하기 때문이다. 즉, 논리적인 오류인 것이다.

 

 

class Box<T> {
    T[] items;    // ok
    T[] toArr() {
        T[] tmpArr = new T[items.length]; // error
    }

}

객체 생성이나 배열을 생성할 때, 타입변수의 사용이 불가능하다.(new 다음에 T를 못 씀) 타입변수로 배열 선언(첫 줄)은 가능하다.

 

 

 

와일드카드

ArrayList<? extends Product> list = new ArrayList<Tv>(); // ok
ArrayList<? extends Product> list = new ArrayList<Audio>(); // ok
ArrayList<Product> list = new ArrayList<Tv>(); //  error. 대입된 타입 불일치

하나의 참조변수로 서로 다른 제네릭 타입을 다룰 수 있게 된다.

 

 

 

와일드카드 종류

  • <? extends T>: 와일드카드의 상한제한. T와 그 자손들만 가능
  • <? super T>: 와일드카드의 하한제한. T와 그 조상들만 가능
  • <?>: 제한없음. 모든 타입이 가능함. <? extends Object>와 동일

 

 

static Juice makeJuice(FruitBox<? extends Fruit> box) {
    // ...
}
  • 메서드의 매개변수에도 와일드카드 사용가능하다.
  • makeJuice() 메소드를 호출할 때 매개변수 boxFruitBox<Fruit>, FruitBox<Apple>, FruitBox<Grape> 타입 모두 가능하다. 와일드카드를 사용하지 않으면 Fruit만 가능하다. Apple, Grape는 불가하다.

 

 

 

📚 참고
ch12-1 지네릭스란?
ch12-2,3 타입 변수
ch12-4~6 지네릭스용어, 지네릭 타입과 다형성
ch12-9~11 제한된 지네릭 클래스, 지네릭스의 제약