지네릭스

지네릭스란 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다.

예를 들어 ArrayList에서 만약 형을 정해주지 않으면 뜻하지 않은 형이 나타날 수도 있다.

나는 Integer만 저장하고 싶은데 String이 저장될 수 있고 이러한 것을 방지하고 또한, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄이기 위해 사용한다.

즉, 다시 말하자면 지네릭스의 장점은 이거라 할 수 있다.

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

지네릭 클래스의 선언

지네릭 타입은 클래스와 메서드에 선언할 수 있다.

class Box<T>{
    T item;

    void setItem(T item) {this.item = item;}
    T getItem() {return item;}
}

이와 같이 본다면 C++에서 Template과 비슷한 점을 볼 수 있다.

또한, 이렇게 선언한 T를 타입변수라고 한다. 꼭 T만 사용할 수 있는것이 아니라 필요에 따라서 다른 문자를 사용해도된다.

이렇게 선언을 했다면 객체를 생성할 때 <T>를 붙여줘야한다.

그렇다고 안붙여주면 컴파일이 안되는 것은 아니고 경고가 발생한다.

Box b = new Box(); // 타입변수를 사용하지 않아 Object로 자동조정
b.setItem("Hello"); // 경고 발생 unchecked or unsafe operation

Box<String> b = new Box<T>();
b.setItem("Item"); // 경고발생 x 

또한, 다른타입으로 생성이 불가능하다.

Box<Apple> appleBox = new Box<Grape>(); // 선언하는 타입과 생성자에 대입된 타입이 일치 x

그러면 상속관계에서는 괜찮지 않을까 하는데 경기도 오산이다.

FruitApple이 상속했다고해도 생성이 불가능하다.

하지만 대입된 타입이 같은 경우는 가능하다.

Box<Fruit> box = new Box<Apple>(); //불가능

//대입된 타입이 같은경우는 가능
Box<Apple> box = new FruitBox<Apple>();

또한, 당연히 대입된 타입과 다른 타입의 객체는 추가할 수 없다.

하지만, 상속관계에서는 가능하다. 객체 생성이 불가능 한거지 매개변수는 가능하다.

//대입된 타입과 다른 타입의 객체 추가 x
Box<Apple> box = new Box<Apple>();
box.add(new Apple()); // 가능
box.add(new Grape()); // 불가능

// 상속관계에서 가능 예시
Box<Fruit> box = new Box<Fruit>();
box.add(new Fruit());
box.add(new Apple());

지네릭스의 용어정리

예를들어 Box<T>라는 지네릭 클래스가 존자한다고 한다.

Box<T>지네릭 클래스라고 불린다 또한, T타입 변수 or 타입 매개변수라고 불린다.

Box원시 타입으로 불린다.

지네릭스의 제한

지네릭스는 제한되는 점이 있다.

  1. static멤버에 타입 변수를 사용할 수 없다.

만약 static멤버에 타입 변수를 사용한다면 인스턴스변수로 간주된기 떄문이다.

static멤버는 인스턴스 변수를 참조할 수 없기 때문이다.

  1. 지네릭타입의 배열을 선언할 수 없다.

왜냐하면 new연산자 떄문이다. new는 컴파일 하는 시점에는 T가 어떤타입인지 알아야하지만 컴파일 하는 시점에서는 T가 어떤타입인지 알 수 없기 때문이다.

이러한 지네릭 클래스는 타입변수 T가 제한이 되있지 않아서 아무 타입종류나 다 들어갈 수 있다.

이러한 타입변수를 제한할 수 있는 방법은 없을까?

해결방법은 extends를 사용하면된다. 이를 사용하면 특정 타입의 자식들만 대입할 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit>{ // Fruit의 자식만 타입으로 지정할 수 있다.
    /...
}

또한, 클래스가아닌 인터페이스도 포함한다는 제약이 필요하다면 implements가 아닌 그냥 extends를 사용해서 해결하면 된다. <T extends Eatable> 이런식으로 사용하면 된다.

만약, 클래스의 자손이면서 인터페이스도 구현하려면 &를 붙여주면 해결된다. <T extends Eatable & Fruit> 이렇게 하면 Fruit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수에 대입될 수 있다.

와일드 카드

static메서드에서는 타입 매개변수를 매개변수로 사용할 수 없기 때문에 도입하였다.

class Juicer {
  static Juice makeJuice(FruitBox<Fruit> box) {
    String tmp = "";
    for(Fruit f: box.getList()) tmp += f + " ";
    return new Juice(tmp);
  }
}

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

Juicer.makeJuice(fruitBox); // 가능하다.
Juicer.makeJuice(appleBox); // 불가능하다. 형변환을 할 수없기 때문이다.

이를 해결하면 그냥 makeJuice라는 메소드를 오버로딩하면 되지 않겠냐 생각할 수도 있지만 이것도 경기도 오산이다.

왜냐하면 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다.

그래서 Apple타입을 오버로딩을 함부로 할 수 없다.

이를 해결하기 위해서 나온것이 바로 와일드 카드이다.

사용하는법은 다음과 같다.

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

또한, 특징이 있는데 와일드 카드에서는 &를 사용할 수 없다는 것이 특징이다.

지네릭 메서드

지네릭 메서드는 메서드의 선언부에 지네릭 타입이 선언된 메서드를 말한다.

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

지네릭 메서드는 굳이 지네릭 클래스가 아닌 클래스에서도 정의될 수가 있다.

그리고 static멤버와 차이점이 있는데 staic멤버는 타입 매개변수를 사용할 수 없지만 지네릭 타입을 메서드에 선언하고 사용하면 가능하다.

또한, 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 static이건 아니건 상관이 없다.

예시로 makeJuice()를 지네릭 메서드로 바꾸면 다음과 같이 변경된다.

static Juice makeJuice (FruitBox<? extends Fruit> box){
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

// 지네릭 메서드로 변환
static <T extends Fruit> Juice makeJuice (FruitBox<T> box){
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

이러한 메서드를 호출할 떄는 반드시 타입변수에 타입을 대입하고 호출해야한다.

지네릭 타입의 형변환

지네릭타입은 non지네릭 타입간의 형변환은 항상 가능하다. 하지만 경고가 발생하긴한다.

Box<Object> objBox = null;
Box box = null;

box = (Box)objBox; // 가능 -> 경고발생
objBox = (Box<String>)box; //가능 -> 경고발생

하지만 대입된 타입이 다른 지네릭타입간의 형변환은 불가능하다.

Box<Object> objBox = null;
Box<String> strBox = null;

objBox = (Box<Object>)strBox; //에러발생 
strBox = (Box<String>)objBox; // 에러발생

만약 와일드카드 지네릭 타입에서 지네릭타입으로의 형변환을 어떨까?

결론적으로말하면 서로 형변환이 가능하다.

또한, 와일드카드 지네릭 타입에서 와일드카드 지네릭 타입으로의 형변환도 가능하다.

Reference

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 쓰레드 (1)  (0) 2021.09.24
[Java/Study] java.time 패키지  (0) 2021.09.18
[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11

+ Recent posts