웹개발/JAVA

[JAVA] 제네릭스( generics )

ss__jae2 2022. 2. 7. 23:44
반응형

1. 제네릭스( generics )

  • 제네릭스는 다양한 타입의 객체들을 다루는 메소드나 컬렉션 클래스에 컴파일 시 타입체크( compile-time type check )를 해주는 기능이다.
  • 객체의 타입은 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
  • 타입 안정성을 높인다는 것은 의도하지 않는 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄어준다는 것이다.

    1
    2
    ArrayList<Integer> list = new ArrayList<Integer>();
    ArrayList<Integer> list2 = new ArrayList<>();
     

     

  • 제네릭스의 장점
    • 타입 안정성을 제공한다.
    • 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.
      ==> 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다는 것이다.

2. 제네릭 클래스의 선언

  • 제네릭 타입은 클래스와 메소드에 선언할 수 있는데, 먼저 클래스에 선언하는 제네릭 타입에 대해서 알아보자.

    1
    2
    3
    4
    5
    6
    class Box{
        Object item;
            
        void setItem(Object item){}
        Object getItem(){}
    }
    c

     

  • 위 클래스를 제네릭 클래스로 변경하고자 한다면 아래와 같이 '<T>'를 붙이고 Object는 모두 T로 변경하면 된다.

    1
    2
    3
    4
    5
    6
    class Box<T>{
        T item;
     
        void setItem(T item){}
        T getItem(){}
    }
    cs

     

  • Box<T>에서 'T'를 타입변수( type variable )라고 하며 'Type'에서 첫글자를 따온 것이다.( 타입변수는 T가 아닌 다른 것을 사용해도 된다. )
  • 타입변수가 여러개인 경우에는 Map<K,V>와 같이 콤마를 구분자로 나열하면 된다.
  • T, K, V 이들은 기호의 종류만 다를 뿐, '임의의 참조형 타입'을 의미한다는 것은 모두 같다.
  • 기존에는 다양한 종류의 타입을 다루는 메소드의 매개변수나 리턴 타입으로 Object타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만, 이젠 Object타입 대신 원하는 타입을 지정하기만 하면 되는 것이다.

    1
    2
    3
    4
    Box<String> b = new Box<String>(); // 객체생성
    b.setItem(new Object());    // 에러 O, void setItem(String item){} String item = new Object();
    b.setItem("ABC");            // 에러 X
    String item = b.getItem        // 형변환을 따로 할 필요가 없다는 장점
    cs

     

  • 제네릭이 도입되기 이전의 코드와 호환을 위해 제네릭클래스인데도 예전의 방식으로 객체를 생성하는 것이 허용되지만 제네릭타입을 지정하지 않아서 안전하지 않다는 경고가 발생한다.

    1
    2
    3
    Box b = new Box(); ==> Box<Object> b = new Box<>();
    b.setItem(new Object()); // 에러 X
    b.setItem("ABC");        // 에러 X
    cs

     

  • 제네릭스 용어

    class Box {}
    Box<T> 제네릭클래스, 'T의 Box' 또는 'T Box'라고 읽는다.
    T 타입변수 또는 타입 매개변수( T는 타입문자 )
    Box 원시타입( raw type )
    • 타입문자 T는 제네릭 클래스 Box<T>의 타입변수 또는 타입 매개변수라고 하는데 메소드의 매개변수와 유사한 점이 있기 때문이다.
    • 그래서 아래와 같이 타입 매개변수에 타입을 지정하는 것을 '제네릭 타입 호출'이라고 하고 지정된 타입을 '매개변수화된 타입( parameterized type )'이라고 한다.

      1
      2
      Box<String> b = new Box<String>();
      Box<Integer> b1 = new Box<Integer>();
       
    • 위처럼 Box<String>, Box<Integer>는 서로 다른 타입을 대입하여 호출한 것일 뿐 둘이 별개의 클래스를 의미하는 건 아니다.
  • 제네릭스의 제한
    • 제네릭 클래스 Box의 객체를 생성할 때는 객체 별로 다른 타입을 지정해주는 것이 적절하다.
      왜? 제네릭은 인스턴스 별로 다르게 동작하도록 만든 것이기 때문이다.
    • 그러나 모든 객체에 대해 동일하게 동작해야 하는 static멤버에 타입변수 T를 사용할 수 없다.
      왜? T는 인스턴스 변수로 간주되기 때문이다.

      1
      2
      3
      4
      class Box<T>{
          static T item;            // 에러
          static int compare(T t1, T t2){};        // 에러
      }
      cs

       

    • static 멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.
    • 또한 제네릭 타입의 배열을 생성하는 것도 허용되지 않는다.
    • 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만 new T[10]; 과 같은 배열을 생성하는 것은 안된다.
      ==> new 연산자로 인해 불가능한 것인데, new 연산자는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 하기 때문이다.

3. 제한된 제네릭 클래스

  • 타입문자로 사용할 타입을 명시하면 한 종류의 타입만 지정할 수 있도록 제한할 수 있지만, 그래도 여전히 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없다.
  • 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법이 있을까?

    1
    2
    Box<Toy> toyBox = new Box<>();
    toyBox.add(new Toy());        // Ok, 과일상자에 장난감을 담을 수 있다.
    cs

     

  • 아래와 같이 제네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

    1
    class FruitBox<extends Fruit>{/* 내용 생략 */}
    cs

     

  • 여전히 한 종류의 타입만 담을 수 있지만, Fruit 클래스의 자손들만 담을 수 있다는 제한이 더 추가된 것이다.

    1
    2
    3
    4
    FruitBox<Toy> fruitBox = new FruitBox<>();    // 에러
    FruitBox<Fruit> fruitBox = new FruitBox<>();    
    fruitBox.add(new Apple());
    fruitBox.add(new Grape());
    cs

     

  • 다형성에서 조상타입의 참조변수로 자손타입의 객체를 가리킬 수 있는 것처럼, 매개변수화된 타입의 자손타입도 가능한 것이다.
  • 만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면 이 때도 implements가 아닌 extends를 사용한다.

    1
    2
    3
    4
    interface Eatable{}
    class FruitBox<implements Eatable> // 에러
    class FruitBox<extends Eatable>
    class FruitBox<extends Fruit & Eatable> // 둘다 상속하고 있는 타입만 가능
     

4. 와일드 카드( wild card )

  • 매개변수에 과일박스를 대입하면 주스를 만들어 반환하는 Jucier라는 클래스가 있고, 이 클래스에는 과일을 주스로 만들어서 반환하는 static 메소드 makeJuice()가 있다고 가정하자.

    1
    2
    3
    class Juicer{
        static Juice makeJuice(FruitBox<T> box){}
    }
    cs

     

  • Juicer 클래스는 제네릭 클래스가 아닌데다가 제네릭 클래스라고 해도 static 메소드에는 타입 매개변수 T가 사용할 수 없으므로 아예 제네릭스를 적용하지 않던가 아래와 같이 매개변수 대신 특정 타입을 정해줘야 한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Juicer{
        static Juice makeJuice(FruitBox<Fruit> box){}
        static Juice makeJuice(FruitBox<Apple> box){}    ==> 에러
    }
     
    FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();        
    FruitBox<Apple> appleBox = new FruitBox<Apple>();    
        
    Juicer.makeJuice(fruitBox);        // 에러 X  
    Juicer.makeJuice(appleBox);        // 에러 O 
    cs

     

  • 이렇게 제네릭 타입을 'FruitBox<Fruit>'로 고정해 놓으면 위의 코드에서도 알 수 있듯이 'FruitBox<Apple>' 타입의 객체는 메소드의 매개변수가 될 수 없으므로 해당 메소드를 사용하기 위해선 오버로딩해야한다.
    ==> "제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않는다."라는 규칙때문에 우리가 생각한 것처럼 오버로딩을 하면 에러가 발생한다.
  • 이럴 때 사용하는 것이 와일드카드이다. 와일드카드는 기호 ?를 사용한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Juicer{
        static Juice makeJuice(FruitBox<extends Fruit> box){}    // Fruit 상속 받은 애 다 할 수 있음
    }
     
    FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();        
    FruitBox<Apple> appleBox = new FruitBox<Apple>(); 
       
    Juicer.makeJuice(fruitBox);        // 에러 X 
    Juicer.makeJuice(appleBox);        // 에러 X
    cs

     

  • 와일드카드는 기호 ?로 표현하는데 와일드카드는 어떠한 타입도 될 수 있다.
  • ? 만으로는 Object 타입과 다를게 없으므로 아래와 같이 제한을 할 수 있다.

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

5. 제네릭 메소드

  • 메소드 선언부에 제네릭 타입이 선언된 메소드를 제네릭 메소드라 한다.
  • 제네릭 타입의 선언 위치는 반환 타입 바로 앞이다.

    1
    static<T> void sort(List<T> list, Comparator<super T> c);
    cs

     

  • 제네릭 클래스에 정의된 타입 매개변수와 제네릭 메소드에 정의된 타입 매개변수는 전혀 별개인 것이다.
  • 같은 타입문자 T를 사용해도 같은 것이 아니라는 것에 주의해야 한다.
  • 메소드에 선언된 제네릭 타입은 지역변수를 선언한 것과 같다고 생각하면 편한데, 이 타입 매개변수는 메소드 내에서만 지역적으로 사용될 것이므로 메소드가 static이건 아니건 상관없다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    static Juice makeJuice(FruitBox<extends Fruit> box){}
     
    static<extends Fruit> Juice makeJuice(FruitBox<T> box){}
     
    FruitBox<Fruit> fruitBox = new FruitBox<>();
    FruitBox<Apple> appleBox = new FruitBox<>();
     
    Juicer.<Fruit>makeJuice(fruitBox);
    Juicer.<Apple>makeJuice(appleBox);
    cs

    * 두 개는 같은 것이다.

    static Juice makeJuice(FruitBox<extends Fruit> box){}
    static<extends Fruit> Juice makeJuice(FruitBox<T> box){}


  • 그러나 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략해도 된다.

    1
    2
    Juicer.makeJuice(fruitBox);
    Juicer.makeJuice(appleBox);
    c
반응형