[Java] Generics

Hoplin·2023년 2월 12일
0

Generics?

지네릭스는 JDK 1.5버전에서 도입된 문법으로, 컴파일 타임에서 타입 체킹을 해주는 문법이다. 지네릭스 문법의 장점은 아래 두가지가 있다

  • 타입 안정성을 보장한다
  • 타입체크와 형변환을 생략할 수 있기때문에, 코드가 간결해 진다.

Generic 클래스 선언

지네릭 클래스를 선언해 보자. 클래스 옆에 <T>를 붙여서 선언하면 된다. 아래 코드에서 지네릭이 있고 없고에서의 클래스 선언 차이를 살펴보자 여기서 T는 타입변수를 의미하며, T가 아닌 다른 문자로 사용해도 된다. 상황에 따라 의미있는 알파벳을 사용하면 좋다. 예를 들면 Map<K,V(K는 key, V는 value와 같이 말이다.)

class BoxWithoutGeneric {
    Object something;

    void setSomething(Object something) {
        this.something = something;
    }

    Object getSomething() {
        return this.something;
    }
}

class BoxWithGeneric<T> {
    T something;

    void setSomething(T something) {
        this.something = something;
    }

    T getSomething() {
        return this.something;
    }
}

지네릭스가 도입되기 이전에는 매개변수나 반환타입에 Object타입을 참조변수 타입으로 많이 사용했다. 그로 인해 일일히 형변환을 하거나, 타입을 체킹해 주어야 하는 일도 빈번하였다. 하지만 지네릭이 도입됨에 따라, 이러한 불편함이 많이 없어졌다.

지네릭 클래스가 선언된 클래스를 이용해서 객체를 생성해 보자. 참조변수 타입에 <T>대신 타입을 지정해서 넣어주면 된다. 생성자에도 원래 타입을 지정해 주지만, 컴파일러가 추정이 가능하기 때문에 타입은 생략해도 괜찮다(단, JDK 1.7부터 해당한다). 또한 제네릭 클래스를 만들때 지네릭 타입을 지정해 주지 않아도 괜찮지만, unsafe 경고가 발생하게 된다.

BoxWithGeneric is a raw type. References to generic type BoxWithGeneric\ should be parameterized

class genericInstance {
    public static void main(String[] args) {
    	// 생성자에 타입을 생략해 주었다.
        BoxWithGeneric<String> box = new BoxWithGeneric<>();
        BoxWithGeneric box2 = new BoxWithGeneric();
    }
}

지네릭스 제한

지네릭스에서 제한사항들이 있다.먼저 static멤버에 지네릭 타입 T를 사용할 수 없다는것이다. T인스턴스 변수로서 간주된다. static멤버의 경우에는 new연산자에 의해 인스턴스가 생성되기 이전에 미리 로드가 되는 멤버들이다. 해당 시점에서는 T를 알 수 없을 뿐더러 static은 모든 인스턴스가 동일한 값을 공유해야하므로, 여러가지 형태의 타입을 띄고있어서는 안된다. static 멤버에서는 지네릭 타입을 사용할 수 없는것이다.

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

다른 성질은 지네릭 타입의 배열을 참조변수로 선언하는것은 가능하지만, 생성하는것은 허용하지 않는다. 이는 new연산자 떄문인데, new연산자는 컴파일 시점에서 타입이 뭔지 정확하게 알아야 한다. 이와 비슷한 이유로 instanceof또한 지네릭 타입에 대해 사용이 불가능하다.

class Box<T> {
    private T[] items = new T[10];
}

만약 지네릭 배열을 꼭 만들어야 되는 경우, Reflection APInewInstance()와 같은 방법을 통해 동적으로 객체를 생성해야한다.

import java.lang.reflect.Array;

class Box<T> {
    private T[] items;

    public void setItems(Class<T> cls) {
        this.setItems(cls, 10);
    }

    public void setItems(Class<T> cls, int size) {
        this.items = (T[]) Array.newInstance(cls, size);
    }
}

지네릭 클래스의 객체 생성 및 사용

지네릭 클래스를 가지고 객체를 생성하기 위해서는 아래 조건들을 주의해야한다

  • 참조변수와 생성자에 대입된 타입이 무조건 일치해야한다(생성자에 타입을 생략하지 않을시). 일치하지 않으면 에러가 발생한다.
  • 지네릭에 쓰이는 타입이 서로 상속관계에 있어서도 안된다.
  • 지네릭 클래스간의 상속관계는 허용한다.
class Fruit {
}

class Apple extends Fruit {
}

class Grape extends Fruit {
}

class FruitBox<T> extends Box<T> {

}

class Box<T> {
    private T[] items;

    // static T item; // Error
    // static int compare(T t1, T t2); // Error

    public void setItems(Class<T> cls) {
        this.setItems(cls, 10);
    }

    public void setItems(Class<T> cls, int size) {
        this.items = (T[]) Array.newInstance(cls, size);
    }
}

class GenericUsageTest {
    public static void main(String[] args) {
        Box<Apple> applebox = new Box<Apple>();
        Box<Fruit> fruitbox = new Box<Grape>(); // mismatch
        Box<Apple> applebox2 = new FruitBox<Apple>(); // OK
    }
}

제한된 지네릭 클래스

지네릭 클래스에 대해 인스턴스 생성시, 하나의 타입을 지정하여 사용한다. 다만, 이 지네릭 타입에는 모든 래퍼타입 뿐만 아니라 모든 사용자 정의 타입까지 들어갈 수 있다는 한계점이 있다. 그리고 예를 들어 제네릭 타입에 Object타입이 들어가게 된다면, 사실상 모든 타입이 들어갈 수 있게되는것이다(모든 객체는 Object를 기본 상속받으므로) 필요에 따라 지네릭에 지정되는 타입의 범위를 지정해야하는 경우도 있다. 이런 경우, 지네릭에 extends 키워드를 사용하여 특정 타입의 자손들만 대입하게끔 할 수 있다.

class Toy {

}

class robot extends Toy {

}

class toycar extends Toy {

}

// Box는 위와 동일
class ToyBox<T extends Toy> extends Box<T> {

}

public class genericTypeLimit {
    public static void main(String[] args) {
        ToyBox<robot> robots = new ToyBox<>();
        ToyBox<toycar> cars = new ToyBox<>();
    }
}

이렇게 정의하게 되면, ToyBox의 제네릭 타입으로 올 수 있는 타입은 Toy타입을 상속받은 타입만 대입할 수 있게 되는것이다. 클래스 뿐만 아니라 인터페이스를 구현해야하는 경우, 동일하게 extends를 통해 제약을 걸 수 있다. 그리고 만약 특정 클래스 상속, 인터페이스 구현이 제약 조건이라면 &를 통해 연결해 줄 수 있다. 단 클래스 상속이 있는경우, 클래스 상속이 먼저 오고 그 다음 인터페이스들을 적어주어야 한다.

interface movable {
}

class Toy {

}

class robot extends Toy {

}

class toycar extends Toy {

}

// 클래스 상속을 먼저 적어준다.
class MovableToyBox<T extends Toy & movable> extends Box<T> {

}

class MovableBox<T extends movable> extends Box<T> {
}

class ToyBox<T extends Toy> extends Box<T> {
}

와일드 카드

예를 들어 static 메소드의 인자로 지네릭 클래스를 지정했다고 가정하자.

class Juicer {
    public static void juiceMaker(FruitBox<Fruit> box) {
        System.out.print("Making Juice");
    }
}

이제 이를 사용해 본다고 가정하자 아래와 같이 코드를 작성해 본다. 하나는 Fruit 타입이 지정된 FruitBox를 하나는 Apple타입이 지정된 FruitBox를 만들고, 이를 juiceMaker에 인자로 넘긴다. 그렇게 되면 Apple제네릭 타입의 FruitBox에서 에러가 나는것을 알 수 있다. 앞에서 봤듯이, 제네릭은 완전히 동일한 타입이어야 하기 때문이다.

class wildCardTest {
    public static void main(String[] args) {
        FruitBox<Fruit> fb = new FruitBox<>();
        FruitBox<Apple> ab = new FruitBox<>();
        Juicer.juiceMaker(fb);
        Juicer.juiceMaker(ab); // 에러
    }
}

지네릭 객체생성에서 봤듯이, 지네릭은 참조변수와 생성자에 있는 타입이 무조건 동일해야한다. juiceMaker()에는 Fruit 타입이 참조변수로 들어가있고, main함수에서는 지네릭 타입이 Apple인 지네릭클래스 인스턴스가 들어가므로, 에러가 나는것이다. 그렇다면 이에 대응하는 방법이 무엇이 있을까? 우리는 Overloading이라는 것을 배웠다. 동일한 매소드 이름이지만, 매개변수의 개수, 유형이 서로 다른것을 의미한다. 그렇다면 이번에는 FruitBox<Apple>타입을 받는 juiceMaker를 작성해본다.

class Juicer {
    public static void juiceMaker(FruitBox<Fruit> box) {
        System.out.print("Making Juice");
    }

    public static void juiceMaker(FruitBox<Apple> box) {
        System.out.print("Making Juice");
    }
}

이 코드를 컴파일 하면, 컴파일 오류가 발생한다. 그 이유는 지네릭은 컴파일러가 컴파일 할때만 사용하고, 제거한다. 그렇기에 런타임 시점에서 두 메소드는 중복정의로 되기 때문에, 오버로딩이 성립되지않는것이다. 이를 보완하고자 와일드 카드가 나온것이다. 와일드카드는 ?로 표기하며, 어떠한 타입도 될 수 있다.(Object 타입과 동일하다)

와일드 카드는 super, extends 키워드를 사용하여 상한, 하한을 제한할 수 있다.

  • <? extends T> : 상한제한. T와, T의 자식 타입까지 가능
  • <? super T> : 하한제한. T와, T의 조상까지 가능
  • <?> : 모든 타입 가능

위의 Juicer같은 경우, Fruit타입이, Apple타입의 조상타입이 되므로 아래와 같이 작성해 줄 수 있다. 이렇게 작성하면 Apple지네릭타입 뿐만 아니라 추후 Grape지네릭 타입도 사용이 가능해진다.

class Juicer {
    public static void juiceMaker(FruitBox<? extends Fruit> box) {
        System.out.print("Making Juice");
    }
}

위의 예시는 지네릭 상한 제한이다. 그렇다면 반대로 하한제한에는 어떤 경우가 있을까? 가장 대표적으로는 Comparator를 사용할때를 예시로 들 수 있다.

class wildCardTest {
    public static void main(String[] args) {
        ArrayList<Apple> al = new ArrayList<>();
        FruitSorter.sort(al, new FruitCompare());
    }
}

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

class FruitCompare implements Comparator<Fruit> {
    @Override
    public int compare(Fruit o1, Fruit o2) {
        return o1.price - o2.price;
    }
}

지네릭 메소드

지네릭 타입이 선언된 메소드를 지네릭 메소드라고 한다. 지네릭 메소드에서 지네릭 선언 위치는 반환 타입 앞이다. 주의할것은, 지네릭 클래스에 정의된 타입과, 지네릭 메소드에 정의된 타입은 동일한 문자라도, 서로 다른것이라는점에 주의해야한다.

class GenericMethod<T> {
    // someMethod()의 T와 GenericMethod의 T는 서로 완전히 다른 T이다.
    static <T> void someMethod() {
    }
}

위 예시에서 한가지 이상한 점이 있다. 분명, 위에서 static 멤버변수는 지네릭 클래스에 넘겨지는 타입을 사용할 수 없다 하였었다. static 메소드는 위 예시와 같이 메소드에 제네릭 타입을 선언하고 사용하는것은 가능하다. 메소드에 선언된 지네릭 타입은 지역 변수를 선언한것이라 생각하면 된다.

위의 예시 중 Juicer클래스의 juiceMaker 메소드를 지네릭 메소드를 사용하여 아래와 같이 변경할 수 있다.

class Juicer {
    public static <T> void juiceMaker(FruitBox<T> box) {
        System.out.print("Making Juice");
    }
}

지네릭 메소드 호출시, 타입을 생략해줄 수 도 있으며, 생략을 안할 경우, 지네릭 인스턴스 선언과 동일하게 앞에 <type>를 명시해주면 된다.

class wildCardTest {
    public static void main(String[] args) {
        FruitBox<Fruit> fb = new FruitBox<>();
        FruitBox<Apple> ab = new FruitBox<>();
        Juicer.juiceMaker(fb); // 메소드의 지네릭 타입을 생략하는경우
        Juicer.<Apple>juiceMaker(ab); // 메소드의 지네릭 타입을 생략하지 않는 경우
    }
}

지네릭 형변환

지네릭 타입의 형변환을 살펴보자. 지네릭타입은 기본적으로 Non-GenericsGenerics간의 형변환이 허용 된다. 반대로 지네릭의 성질이라 당연한것이지만 대입된 타입이 다른 지네릭간에는 형변환이 이루어 지지 않는다. 만약 대입된 지네릭 타입이 서로 같다면 형변환은 허용된다.

class genericTypeChange {
    public static void main(String[] args) {
        // Generic <-> Non-Generic
        Box box = new Box();
        Box<String> strbox = new Box<>();

        box = (Box) strbox;
        strbox = (Box<String>) box;

        // Generics <-> Generics
        Box<Object> objBox = new Box<>();
        Box<Integer> intObj = new Box<>();
        Box<Integer> intObj2 = new Box<>();
        intObj = intObj2; // 동일한 타입이 대입된 Generics간에는 OK
        intObj = objBox; // Error
    }
}

지네릭간의 형변환을 하는 방법중 하나는 와일드카드를 사용하는 것이다. 단 와일드 카드 타입의 객체를 와일드 카드 범위에 소속된 타입으로 형변환을 해주지는 못한다.(범위 내에 해당하는 여러 타입중 서로 호환되지 않을 가능성이 있는 타입이 존재할 수 있으므로)

class genericTypeChange {
    public static void main(String[] args) {
        Box<? extends Fruit> fbox = new Box<>();
        Box<Apple> abox = new Box<>();
        Box<Grape> gbox = new Box<>();
        fbox = abox;
        abox = fbox; // Error
    }
}
profile
더 나은 내일을 위해 오늘의 중괄호를 엽니다

0개의 댓글