[기본 개념] 7 | (1.1) 지네릭스 클래스

728x90

[기본 개념] 7 | (1.1) 지네릭스 클래스

1> 지네릭스란?

2> 지네릭 클래스의 선언

3> 지네릭 클래스의 객체 생성과 사용

4> 제한된 지네릭 클래스

5 와일드 카드

6 지네릭 메서드

7 지네릭 타입의 제거

1. 지네릭스란?

 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다. 지네릭스를 사용하면 객체의 타입을 컴파일 시에 체크하여 객체의 타입 안정성을 높이고, 형변환의 번거로움이 줄어든다.

 

 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다는 것이다.

2. 지네릭 클래스의 선언

 지네릭 타입은 클래스와 메서드에 선언할 수 있는데 우선 클래스에 선언하는 지네릭 타입을 먼저 보자.

 

class Box {

    Object item ;

 

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

    Object getItem( ) { return item ; }

}

 

 이 클래스를 지네릭 클래스로 변경하려면 클래스 옆에 '<T>'를 붙이면 된다. 그리고 'Object'를 'T'로 모두 바꾼다.

 

class Box<T> {

    T item ;

 

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

    T getItem( ) { return item ; }

}

 

 Box<T>에서 T를 '타입 변수'라고 하며 T가 아닌 다른 것을 사용해도 된다. 보통 상황에 맞게 의미 있는 문자를 선택하는 것이 좋으며 K는 Key를 의미하고, ArrayList<E>에서는 Element, V는 Value를 의미한다. 

 

 기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 같다.

 

 지네릭 클래스가 된 Box클래스의 객체를 생성할 때는 참조변수와 생성자에 타입 T 대신에 실제 사용될 타입을 지정해 주어야 한다.

 

Box<String> b = new Box<String>( ) ;         // 타입 T 대신, 실제 타입을 지정

b.setItem(new Object( )) ;                               // error. String외의 타입은 지정불가

b.setItem("ABC") ;                                             // OK. String타입이므로 가능

String item = (String) b.getItem( ) ;             // 형변환 필요없음

 

 위 코드에서 타입 T대신 String타입을 지정했으므로 지네릭 클래스 Box<T>는 다음과 같이 정의된 것과 같다.

 

class Box {

    String item ;

 

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

    String getItem( ) { return item ; }

}

 

 지네릭 도입 전의 코드와 호환을 위해 지네릭 타입이 아니더라도 예전의 방식으로 객체를 생성하는 것이 허용되지만 경고가 발생한다.

 

지네릭스의 제한

 모든 객체에 대해 동일하게 동작해야 하는 static멤버에 타입변수 T를 사용할 수 없다. T는 인스턴스변수로 간주되기 때문에 static멤버는 인스턴스변수를 참조할 수 없기 때문이다.

 

 또한 지네릭 타입의 배열 타입의 참조변수를 선언하는 것은 가능하지만, 배열을 생성하는 것은 안된다. 왜냐하면 컴파일 시점에 T가 어떤 타입이 될지 알 수 없기 때문이다.

3. 지네릭 클래스의 객체 생성과 사용

1 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다.

Box<Apple> appleBox = new Box<Grape>( ) ;     // error.

2 두 타입이 상속관계여도 일치해야 한다.

Box<Fruit> appleBox = new Box<Apple>( ) ;     // error. 대입된 타입이 다르다.

3 지네릭 클래스의 타입이 상속관계이고, 대입된 타입이 같으면 괜찮다.

Box<Apple> appleBox = new FruitBox<Apple>( ) ;     // ok.

4 추정이 가능한 경우 타입을 생략할 수 있다.

Box<Apple> appleBox = new Box<Apple>( ) ;     

Box<Apple> appleBox = new Box<>( ) ;            // ok. 위 코드와 같은 문장

5 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다. 하지만, 자손이라면 추가할 수 있다. 

Box<Apple> appleBox = new Box<Apple>( ) ;

appleBox.add(new Apple( )) ;          // ok.

appleBox.add(new Grape( )) ;          // error. Box<Apple>에는 Apple객체만 추가가능

 

Box<Fruit> fruitBox = new Box<Fruit>( ) ;           // Apple이 Fruit의 자손이라고 가정

fruitBox.add(new Fruit( )) ;             // ok.

fruitBox.add(new Apple( )) ;          // ok.

 

예제/FruitBoxEx1.java

import java.util.ArrayList;

class Fruit     { public String toString() { return "Fruit"; } }
class Apple extends Fruit { public String toString() { return "Apple"; } }
class Grape extends Fruit { public String toString() { return "Grape"; } }
class Toy       { public String toString() { return "Toy"; } }

public class FruitBoxEx1 {
    public static void main(String[] args) {
        Box<Fruit> fruitBox = new Box<Fruit>();
        Box<Apple> appleBox = new Box<Apple>();
        Box<Toy> toyBox = new Box<>();
//      Box<Grape> grapeBox = new Box<Apple>();     // error. 타입 불일치

        fruitBox.add(new Fruit());
        fruitBox.add(new Apple());      // OK.

        appleBox.add(new Apple());
        appleBox.add(new Apple());
//      appleBox.add(new Toy());        // error. Box<Apple>에는 Apple만 담을 수 있음

        toyBox.add(new Toy());
//      toyBox.add(new Apple());        // error. Box<Toy>에는 Toy만 담을 수 있음

        System.out.println(fruitBox);
        System.out.println(appleBox);
        System.out.println(toyBox);
    }
}

class Box<T> {
    ArrayList<T> list = new ArrayList<T>();
    void add(T item) { list.add(item); }
    T get(int i)     { return list.get(i); }
    int size()       { return list.size(); }
    public String toString()    { return list.toString(); }
}
실행결과

[Fruit, Apple]
[Apple, Apple]
[Toy]

4. 제한된 지네릭 클래스

 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한하려면 지네릭 타입에 'extends'를 사용하여 특정 타입의 자손들만 대입할 수 있다.

 

class FruitBox<T extends Fruit> {    // Fruit의 자손만 타입으로 지정가능

    ArrayList<T> list = new ArrayList<T>( ) ;

    . . .

}

 

 게다가 add( )의 매개변수의 타입 T도 Fruit와 그 자손 타입이 될 수 있으므로 여러 과일을 담을 수 있는 상자가 가능하게 된다.

 

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

fruitBox.add(new Apple( )) ;         // Apple이 Fruit의 자손

fruitBox.add(new Grape( )) ;         // Grape이 Fruit의 자손

 

 만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면 'implements'가 아니라 'extends'를 사용한다. 그리고 자손이면서 인터페이스도 구현한다면 '&'기호로 연결한다.

 

 

 

 

 

 

출처 | Java의 정석 (남궁 성)

728x90