자바 (ref. 자바의정석)

제네릭스(Generics) - JAVA

쿠쿠s 2022. 3. 10. 16:31

 

 

제네릭스

제네릭스란 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능입니다. 타입체크를 함으로써 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어들게 됩니다. 타입 안정성이 높다는 뜻은 의도하지 않는 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여 줍니다. 그래서 코드를 안전하게 작성이 가능하고, 형변환의 번거로움이 줄어 코드가 간결해집니다.

 

 

제네릭 클래스를 한번 만들어 보겠습니다.

import java.util.ArrayList;

class GenericExam {

    public static void main(String[] args) {
        MyGeneric<Integer> myGeneric = new MyGeneric(); //실제타입 Integer 지정

//        myGeneric.setItem("abc");  //타입이 다르다! Integer가 아님.
        myGeneric.setItem(1502);
        Integer item = myGeneric.getItem(); //형변환이 필요없다.

        System.out.println(item + 2);
    }
}

class MyGeneric<T> {
    T item;

    public T getItem() {
        return item;
    }

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

 

<T> 는 '타입 변수(type variable)' 라고 불립니다. 이것은 어떤 글자를 사용해도 되지만  보통 아래와 같이 많이 쓰입니다.

 - ArrayList<E> 의 경우 'Element(요소)' 의 첫 글자

 - Map<K, V> 는 'Key(키)' 와 'Value(값)' 을 의미합니다.

타입 설명
<T> Type
<E> Element
<K> Key
<V> Value
<N> Number

이들은 기호의 종류만 다를뿐 '임의의 참조형 타입' 을 의미하는 것은 모두 같습니다. 마치 'f(x, y) = x + y' 와 'f(k , v ) = k + v' 다르지 않은 것 처럼 입니다.

 

제네릭 클래스를 제가 임의로 만들었는데 Class MyGeneric<T> {  } 에서 MyGeneric<T> 를 제네릭 클래스라 부르며 T는 타입 변수, MyGeneric은 원시 타입(raw type) 이라고 불립니다.

 

 


 

제네릭 제한 

 제네릭 클래스는 객체를 생성할때 다른 타입을 지정하여 만들 수 있다. 하지만 만약 모든 객체에 대해 동일하게 동작하는 static멤버에 타입 변수 T(타입변수) 는 사용할 수 없다. T(타입변수)는 인스턴스변수로 간주되기 때문에 인스턴스 변수를 참조할 수 없는 static은 사용이 불가하다. 

 

class MyGeneric<T> {
    static T item; //에러

    public T getItem() {
        return item;
    }

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

 

그리고 제네릭 배열을 생성할 수 없습니다. 왜냐하면 new 연산자 때문인데 이 연산자는 컴파일 시점에 타입변수가 뭔지 알아야 합니다. 컴파일 시점에는 이 타입변수가 무엇인지 알 수 없어 제네릭 배열이 사용 불가합니다.

 

class MyGeneric<T> {
    T[] item; //T 타입의 배열을 위한 참조변수는 가능

    T[] toArray(){
        T[] list = new T[item.length]; // 에러! 이런 제네릭 배열은 생성 불가
        return list;
    }
}

 


 

 

제네릭 다형성

 

제네릭에서 적용되는 타입  변수의 다형성을 활용 할 수도 있습니다. 클래스, 인터페이스 등 다양하게 활용이 가능합니다.

 

 

import java.util.ArrayList;

class Car { public String toString(){ return "Car";} }
class Tesla extends Car { public String toString(){ return "Tesla";}}
class K3 extends Car { public String toString(){ return "K3";}}
class Boeing707 { public String toString(){return "Boeing707";}}

class GenericExam {

    public static void main(String[] args) {
        Factory<Car> car = new Factory<>();
        Factory<Tesla> teslaCar = new Factory<>();
        Factory<K3> k3Car = new Factory<>();
        Factory<Boeing707> boeing707 = new Factory<>();


        car.add(new Car());
        car.add(new Tesla()); //부모는 자식을 품을 수 있다.
        car.add(new K3());
//        car.add(new Boeing707()); //에러 상속관계가 아님. 해당 타입을 포함 할 수없다.

        teslaCar.add(new Tesla());
//        teslaCar.add(new K3()); //에러 타입 불일치.

        k3Car.add(new K3());
        k3Car.add(new K3());
        
        boeing707.add(new Boeing707());
//        boeing707.add(new K3()); //에러.

        System.out.println(car);
        System.out.println(k3Car);
        System.out.println(boeing707);

    }
}

class Factory<T> {
    ArrayList<T> list = new ArrayList<>();

    void add(T car) {
        list.add(car);
    }

    public ArrayList<T> getList() {
        return list;
    }

    @Override
    public String toString() {
        return list.toString();
    }
}

출력 결과

 

 

 

 

제한된 제네릭클래스와 와일드 카드

 

타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한이 가능하다는 것은 알았다. 특정 범위 내로 좁혀서 제한을 하고 싶으면 어떻게 해야할까. 이럴때는 extends 를 활용할 수 있다. 한 종류의 타입만 담을 수 있지만 특정 클래스의 자손들만 담을 수 있는 제한이 추가된 것이다.

* 참고로 인터페이스를 구현해야하는 제약이 필요하면 이때도 'extends'를 사용한다. 'implements'를 사용하지 않는다.

 

import java.util.ArrayList;

class Car { public String toString(){ return "Car";} }
class Tesla extends Car { public String toString(){ return "Tesla";}}
class K3 extends Car { public String toString(){ return "K3";}}
class Musk extends Tesla { public String toString(){ return "Msuk";}}
class Boeing707 {public String toString(){ return "Boeing707";}}

class GenericExam {

    public static void main(String[] args) {
        Factory<Car> car = new Factory<>();
        Factory<Tesla> teslaCar = new Factory<>();
        Factory<K3> k3Car = new Factory<>();
//        Factory<Boeing707> boeing707 = new Factory<>(); //부모자식 관계가 아님.
        Factory<Musk> musk = new Factory<>();
        
        car.add(new Car());
        car.add(new Tesla());
        car.add(new K3());

        teslaCar.add(new Tesla());

        k3Car.add(new K3());
        
        System.out.println(car);
        System.out.println(k3Car);

    }
}

class Factory<T extends Car> {
    ArrayList<T> list = new ArrayList<>();

    void add(T car) {
        list.add(car);
    }

    public ArrayList<T> getList() {
        return list;
    }

    @Override
    public String toString() {
        return list.toString();
    }
}

 

와일드 카드 타입

 

제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 구체적인 타입 대신 와일드 카드를 다음과 세가지 형태로 사용할  수 있습니다.

<? extends T>	// 와일드 카드의 상한 제한, T와 그 자손들만 가능
<? super T>	// 와일드 카드의 하한 제한, T와 그 부모들만 가능
<?>		// 모든 타입 가능. <? extends Object>랑 같은 의미

 

class Register{

    void RegisterCar(Factory<Tesla> factory) {
        System.out.println(factory.getList());
    }

    void RegisterCar(Factory<K3> factory) {
        System.out.println(factory.getList());
    }

}

 

 

제네릭 타입이 다른것만으로는 오버로딩이 성립하지는 않습니다. 제네릭 타입은 컴파일러가 컴파일 할 때만 사용하고 제거하기 때문입니다. 그래서 오버로딩이 안되고 메서드 중복이 일어납니다. 이런 문제를 해결하기 위해 와일드 카드를 사용할 수 있습니다.

 

 

import java.util.ArrayList;

class Car { public String toString(){ return "Car";} }
class ElectricCar {public String toString(){ return "ElectricCar";}}
class Tesla extends ElectricCar { public String toString(){ return "Tesla";}}
class Ev6 extends ElectricCar {public String toString(){ return "Ev6";}}
class K3 extends Car { public String toString(){ return "K3";}}
class Genesis extends Car {public String toString(){ return "Genesis";}}

class GenericExam {

    public static void main(String[] args) {
        Factory<Car> car = new Factory<>();
        Factory<ElectricCar> electricCar = new Factory<>();
        Factory<K3> k3Car = new Factory<>();
        Factory<Tesla> teslaCar = new Factory<>();

        car.add(new Car());
        car.add(new Genesis());

        electricCar.add(new ElectricCar());
        electricCar.add(new Tesla());
        electricCar.add(new Ev6());
        
        System.out.println(Register.RegisterCar(electricCar));
//        System.out.println(Register.RegisterCar(car));  // 에러! 타입이 Ev6의 부모가 아니다.
//        System.out.println(Register.RegisterCar(k3Car)); // 에러! 타입이 Ev6의 부모가 아니다.
//        System.out.println(Register.RegisterCar(teslaCar)); // 에러! 타입이 Ev6의 부모가 아니다.
    }
}

class Register{

    static Factory RegisterCar(Factory<? super Ev6> factory) {
        return factory;
    }

}

class Factory<T> {
    ArrayList<T> list = new ArrayList<>();

    void add(T car) {
        list.add(car);
    }

    public ArrayList<T> getList() {
        return list;
    }

    @Override
    public String toString() {
        return list.toString();
    }
}

출력 결과

 

 

나머지 예로 하위타입이나 타입의 제한을 걸지 않고싶으면 위 코드를 직접 변경하시면서 확인하면 될 것 같습니다.