Web/Java

[Java] 제네릭

  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • 제네릭 메소드 만들기
  • Erasure

 

 

제네릭

JDK 1.5에서 처음도입이 되었습니다.

 

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

 

장점

객체의 타입을 컴파일시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어듭니다.

(타입 안정성 : 의도하지 않은 타입의 객체가 저장되는 것을 막는다는 뜻)

 

 

 

제네릭 클래스 선언

 

클래스 Box가 아래와 같이 정의되어있으면

class Box {
    Object item;
    
    void setItem(Object item) {
        this.item = item;
    }

    Object getItem() {
        return item;
    }
}

 

Object 부분을 T로 바꿔주면 위의 코드와 같다고 보면 됩니다.

class Box<T> {
    T item;

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

    T getItem() {
        return item;
    }
}

 

 

Box <T>에서 T는 '타입 변수'라고 합니다. (T는 'type'에서 따온 것입니다.)

타입 변수는 T가 아닌 다른 문자를 사용해도 됩니다. 그래서 무조건 'T'를 사용하기 보다 상황에 맞게 의미있는 문자를 선택하면 좋습니다.

 

예시

  • ArrayList<E> : Element의 E
  • Map<K, V> : Key의 K, Value의 V

 

 

사용

 

아래와 같이 사용시에 <String>으로 넣어주면 제네릭 타입이 String으로 지정됩니다.

        Box<String> b = new Box<String>();
        b.setItem("ABC");
        String item = b.getItem();

 

인스턴스 변수 생성시에 제네릭 타입을 String으로 생성하면 

제네릭 클래스 Box<T>는 아래와 같이 정의된 것과 같습니다.

class Box {
    String item;

    void setItem(String item) {
        this.item = item;
    }

    String getItem() {
        return item;
    }
}

 

 

그래서 다른 타입으로 사용하려고 하면 컴파일 에러가 발생합니다.

 

 

 

용어 정리

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

 

 

 

주의

 

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

 

static 멤버는 타입 변수에 지정된 타입에 관계 없이 동일한 것이어야 합니다.

만약 static 멤버에 T를 사용한다면 'Box<Apple>.item' 과 Box<Grape>.item'이 같아야 하지만 실제로는 당연히 다릅니다.

 

 

2. 제네릭 타입의 배열을 생성 제한

 

배열을 생성할때 사용하는 new 연산자는 컴파일 시점에 타입이 무엇인지 정확히 알아야합니다.

그러나 컴파일 시점에 T가 어떤 타입인지 알 수 없기 때문에 제네릭 배열을 생성할 수 없습니다.

 

만약 제네릭 배열을 생성해야할 필요가 있을 경우에는 아래 두가지 방법으로 해결이 가능합니다.

  1. new 연산자 대신 Reflection API의 newInstance()같은 동적으로 객체를 생성하는 메서드로 배열을 생성하는 방법
  2. Object배열을 생성해서 복사한 다음에 'T[]'로 형변환 하는 방법

 

 

 

 

바운디드타입 (제한된 제네릭 클래스)

제네릭 클래스는 모든 종류의 타입을 지정할 수 있습니다.

 

그렇다면, 지정할 수 있는 타입의 종류를 제한할 수 있는 방법이 바운디드 타입입니다.

 

 

FruitBox가 <T extends Fruit>로 되어있으므로 Fruit의 자손타입만 담을 수 있는 클래스가 됩니다.

그래서 Fruit의 자손이 아닌 Toy를 담게되면 컴파일 에러가 발생합니다.

class Fruit{}
class Apple extends Fruit {}
class Grape extends Fruit {}
class Toy {}


class FruitBox<T extends Fruit> {
    ArrayList<T> list = new ArrayList<>();
    void add(T fruit) {
        list.add(fruit);
    }
}

public class BoundedType {

    public static void main(String[] args) {
        FruitBox<Apple> appleBox = new FruitBox<>();
        FruitBox<Toy> toyBox = new FruitBox<>();
    }
}

 

 

 

만약 인터페이스 Eatable을 구현하는 타입만 받기를 원한다면 그때도 'extends'를 사용합니다.

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

 

 

 

또한, 클래스 Fruit의 자손이면서 동시에 Eatable인터페이스도 구현해야 한다면 아래와 같이 '&' 기호로 연결하면 됩니다.

class FruitBox<T extends Fruit & Eatable> {...}

 

 

 

와일드 카드

 

제네릭은 어떤 타입이 들어올 지 모르는 여러 타입을 받을 수 있는 장점이 있습니다.

그런데 만약 제네릭이 포함된 타입을 매개변수를 사용하는 메서드가 있다면 어떻게 작성해야할까요?

 

이해하기 쉽게 코드로 확인해봅시다.

 

아래의 makeJuice는 제네릭 클래스인 FruitBox타입을 매개변수로 받습니다.

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

 

 

그런데 사용자는 FruitBox에 Apple도 넣을 수 있고, Grape도 넣을 수 있고 여러 다른 타입들을 넣어 생성할 수 있을 것입니다.

 

그렇다면 저 makeJuice는 어떻게 작성해야할까요?

 

1. 매개변수 받는 부분에 FruitBox<Fruit> 대신 FruitBox<T>를 넣는다. (불가능)

 

Juicer 클래스는 제네릭 클래스가 아닙니다.

게다가 만약 Juicer가 제네릭 클래스라고 해도, static에는 타입 매개변수 T를 사용할 수 없습니다.

 

 

 

 

2. 타입 매개변수를 오버로딩 한다. (불가능)

 

단순히 타입 매개변수 제네릭타입이 다른 것만으로는 오버로딩 성립이 되지 않습니다.

위의 두 메서드는 그냥 '메서드 중복 정의'가 된 것입니다.

 

 

 

 

와일드카드

위의 문제를 해결하기 위해 고안된 것이 와일드카드 입니다.

 

  • 와일드 카드는 기호 '?'로 표현합니다.
  • 와일드 카드는 어떠한 타입도 될 수 있습니다.

 

와일드 카드로 해결 코드

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



하지만 보통 와일드 카드를 사용할 땐 상한, 하한 제한을 두고 사용합니다.

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

 

 

그래서 현재는 FruitBox<?> 이기 때문에 모든 타입으로 만들어진 FruitBox가 들어올 수 있습니다.

 

이것을 Fruit와 그 자손들만 makeJuice를 사용할 수 있도록 하려면 상한을 Fruit로 해주면 되겠죠.

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

 

 

 

 

 

 

 

와일드카드의 상한, 하한 활용하기 (헷갈릴 수 있으니 집중)

와일드 카드의 상한, 하한을 개발에 활용해보겠습니다.

 

시작 전 주의

먼저 헷갈리지 않기 위해 업캐스팅을 다시 보고가야 합니다.

 

업캐스팅 : 하위클래스 타입상위클래스 타입에 넣을때 자동으로 형변환이 되는 것.

 

상위클래스 타입은 하위클래스 타입에 넣을 수 없습니다.

 

[Web/Java] - [Java] 상속 <- 헷갈리면 확인

 

 

 

 

상한제한활용

 

아래와 같이 제네릭 타입이 Toy인 box에서 Toy하나를 꺼내는 메소드 outBox가 있다고 생각해보겠습니다.

    public static void outBox(Box<Toy> box) {
        Toy t = box.get();
        System.out.println(t);
    }

 

그런데 여기서는 프로그래머가 착각해 Toy를 꺼내는 outBox에서 Toy를 넣는(in) 코드를 작성해도 컴파일 오류가 나지 않겠죠.

    public static void outBox(Box<Toy> box) {
        Toy t = box.get();
        box.set(new Toy()); // 프로그래머가 실수로 작성한 코드
        System.out.println(t);
    }

 

 

outBox 메서드를 정의할 때는 매개변수 box를 대상으로 get은 가능하지만 set은 불가능하도록 제한을 거는것이 좋습니다.

 

이 때 활용할 수 있는 것이 상한제한 와일드 카드입니다.

 

 

아래와 같이 Box<Toy> 대신 와일드 카드를 이용해 Box<? extends Toy>를 넣었습니다.

그러자 set에서 컴파일 오류를 발견할 수 있습니다.

 

왜?

Box<? extends Toy> 의 의미는 Toy와 Toy를 상속받은 타입들(=Toy의 하위클래스)만 허용한다는 뜻입니다.

Toy를 상속받는 Robot가 있다고 생각을 해보면, Box<Robot> box에 Toy를 넣을 수 없습니다.

위에서 언급햇듯이 상위클래스(Toy)는 하위클래스(Robot)에 넣을 수 없기 때문입니다.

 

정리

out 메소드에 "와일드카드의 상한제한"을 걸면 Box는 Toy의 하위클래스가 올 수 있기 때문에 Toy를 넣는 in기능을 제한할 수 있습니다.

 

 

 

 

 

하한제한활용

그렇다면, 반대로 in 함수에서는 out기능을 제한할 수 있습니다.

 

아래와같이 in메서드가 있습니다.

    public static void inBox(Box<Toy> box, Toy n) {
        box.set(n);
    }

 

여기서 마찬가지로 프로그래머가 실수로 out기능을 넣었습니다.

그런데 아무 컴파일 오류도 잡아내지 않습니다.

 

 

여기서 Box<Toy> box 대신 Box<? super Toy> box로 바꾸어주겠습니다.

 

 

그러자 myToy는 box에서 get()한 것이 자신의 하위클래스인 것이 보장되지 않기 때문에 컴파일 오류를 발생시킵니다.

(box.get()이 Toy의 하위클래스인 것이 보장이 된다면 상위클래스인 Toy에 항상 넣을 수 있겠죠)

 

 

 

정리

Box<? extends Toy> box out(get) 작업만 허용
Box<? super Toy> box in(set) 작업만 허용

 

 

출처 : http://byungwook.me/java/2021/02/22/제네릭-와일드카드-상한-하한-제한.html

 

 

 

제네릭 메서드

메서드 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 합니다.

 

제네릭 타입의 선언 위치는 반환타입 바로 앞입니다.

 

 

앞에서 "static 멤버에는 타입 매개변수를 사용할 수 없습니다."고 했지만 메서드의 선언부에 제네릭 타입이 선언되면 사용이 가능합니다.

 

주의해야할 점은 제네릭 클래스에 제네릭 메서드가 정의 되어있다면, 제네릭 클래스의 타입 매개변수와 제네릭 메서드의 타입 매개변수는 같은 것이 아니라는 점입니다.

빨간색 <T>와 파란색 <T>는 같지 않음.

 

제네릭 메서드에 정의되어있는 는 해당 메서드 내에서만 지역적으로 사용되므로 메서드가 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);
    }

 

 

그리고 선언할때는 아래와 같이 타입 변수에 타입을 대입해야합니다.

        System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
        System.out.println(Juicer.<Apple>makeJuice(appleBox));

 

하지만 fruitBox와 appleBox를 선언할때 타입변수에 타입을 대입했기 때문에 컴파일러가 어떤 타입인지 유추가 가능합니다.

그래서 타입(<Fruit>, <Apple>)을 생략할 수 있습니다.

    public static void main(String[] args) {
        FruitBox<Fruit> fruitBox = new FruitBox<>();
        FruitBox<Apple> appleBox = new FruitBox<>();

        System.out.println(Juicer.makeJuice(fruitBox));
        System.out.println(Juicer.makeJuice(appleBox));

    }

 

 

 

 

 

Erasure

제네릭은 컴파일 타임에 동작하는 문법입니다.

컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어주며 제네릭 타입을 제거합니다.

 

즉, 컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없는 것입니다.

 

컴파일시 제네릭 타입이 제거되는 과정을 간단하게 살펴보겠습니다.

 

 

 

1. 제네릭 타입의 경계 (bound)를 제거한다.

 

<T extends Fruit> : 사용되는 T는 Fruit로 치환, 그리고 클래스 옆의 선언은 제거

class Box<T extends Fruit> {
    void add(T t) {
        ...
    }
}
class Box {
    void add(Fruit t) {
        ...
    }
}

 

 

 

<T> : 사용되는 T는 Object로 치환, 그리고 클래스 옆의 선언은 제거

class Box<T> {
    void add(T t) {
        ...
    }
}
class Box {
    void add(Object t) {
        ...
    }
}

 

 

 

 

 

2. 제네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.

 

아래코드는 제네릭 타입이 제거되며 T가 Fruit로 바뀐 상황

list.get(i)의 반환값이 object이고, return 타입이 Fruit이므로 형변환을 추가합니다.

Fruit get(int i) {
    return list.get(i);
}
Fruit get(int i) {
    return (Fruit)list.get(i);
}

 

 

 

와일드 카드가 포함되어 있는 경우

 

적절한 타입으로 형변환이 추가됩니다.

 

 

 

 

 

 

제네릭 타입 제거의 이유

이렇게 하는 주된 이유는,

제네릭이 도입이 되었지만 아직까지 원시타입(primitive type)을 사용해서 코드를 작성하는 것을 허용합니다.

 

제네릭 타입이 제거될때 타입 파라미터는 기본적으로 Object 클래스로 치환됩니다.

그러나 원시타입(primitive type)은 Object를 상속받지 않습니다.

그래서 제네릭에서 원시타입(primitive type)은 타입으로 사용하지 못하는 것입니다.

 

 

따라서 제네릭이 도입되기 이전의 소스코드와의 호환성을 유지하기 위해 제네릭 타입 제거과정이 존재합니다.

 

앞으로 가능하면 원시타입을 사용지 않도록 해야할 것입니다.

(언젠가는 분명히 새로운 기능을 위해 하위 호환성을 포기하게 될 때가 올 것이기 때문입니다.)

 

 

 

 

 

 

 

 

 

 

제네릭에 대한 더 자세한 설명과 실제 사용할때 주의점은 아래 링크에서 더 볼 수 있습니다.

많이 참고한 정리의 출처 : https://www.notion.so/4735e9a564e64bceb26a1e5d1c261a3d

 

제네릭

WhiteShip Java Study 시즌 1

www.notion.so

 

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

[우테코 프리코스] 1주차: 숫자 야구 게임  (2) 2021.11.25
[Java] 람다식  (2) 2021.09.01
[Java] I/O  (2) 2021.08.24
[Java] Enum  (2) 2021.08.11
[Java] 애노테이션  (1) 2021.08.11