Java

제네릭 클래스Generics Class

voider 2020. 9. 9. 10:54

자바의 정석을 참고했습니다.

Generics

 

 

컬렉션 클래스 컴파일 시 타입 체크를 해주는 기능.

장점

  • 타입 안정성을 높여서, 의도하지 않은 타입의 객체가 저장되는 것을 막는다.

지네릭 클래스

class Box<T> {    

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

    T getItem() {
        return item
    }
}

Box<T>에서 T를 타입 변수라고 한다. 타입변수는 꼭 T가 아니라도 상황에 맞는 다른 알파벳을 지정할 수 있다.

위에서 정의한 제네릭 클래스 Box의 객체를 생성한다고 했을 때, <T>가 무슨 타입인지 지정해야 한다.

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

new Box<String&gt(); 이 부분은 당연히 String이 될 것이므로 new Box<>()로 적어도 된다.

용어

class Box<T> { }
  • Box<T>
    제네릭 클래스 T의 Box, T Box
  • <T>
    타입 변수, 타입 매개변수
  • Box
    원시타입raw type

지정한 타입을 매개변수화된 타입parameterized type이라고 한다.

제한

  • static멤버에 타입 변수를 사용할 수 없다.

    static T item;    // 에러

    타입변수는 인스턴스 멤버로 간주하기 때문이다.
    static변수는 매개변수화된 타입에 상관없이 동일한 것이어야 한다.

  • 지네릭 타입 배열 생성 불가

    T[] itemArr;    //T타입 배열을 위한 참조변수
    ...
    T[] toArray() {
      T[] tmpArr = new T[itemArr.length];    //Error!
      return tmpArr
    }

    선언T[] itemArr할 수 있지만 생성new할 수는 없다.
    바로 'new'연산자 때문이다. new는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 한다. 그런데 지네릭 클래스는 컴파일 시점에 T가 어떤 타입이 될 것인지 알 수 없다.

지네릭 클래스 사용

지네릭 클래스 Box<T>를 사용하는 예제 코드다.
자세한 것은 주석에.

package com.javaex.generics;

import java.util.ArrayList;
import java.util.List;

public class FruitsBoxEx1 {
    public static void main(String[] args) {
        //매개변수화된 타입으로, 어떤 타입이든 올 수 있다.
        Box<Fruit> fruitBox = new Box<>();  // == new Box<Fruit>();
        Box<Apple> appleBox = new Box<>();
        Box<Toy> toyBox = new Box<>();
        /*
        *Box<Fruit> box = new Box<Apple>(); 은 매개변수화된 타입이 다르므로 에러가 발생한다.
        *반드시 참조 변수와 생성자의 매개변수화된 타입이 일치해야만 한다. 
        *다형성은
        * Box<Apple> = new FruitBox<Apple>();
        * 이렇게 사용할 수 있다.
        * */

        //void T add(T item) -> void add(Fruit item)
        fruitBox.add(new Fruit());
        fruitBox.add(new Grape());
        fruitBox.add(new Apple());

        appleBox.add(new Apple());
//        appleBox.add(new Graple());   //사과 박스엔 사과만 담을 수 있다.

        toyBox.add(new Toy());

        System.out.println("fruitBox : " + fruitBox);
        System.out.println("appleBox : " + appleBox);
        System.out.println("toyBox : " + toyBox);
    }
}

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

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

    T getItem(int i) {
        return list.get(i);
    }

    int size() {
        return list.size();
    }

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


class Fruit {
    @Override
    public String toString(){
        return "Fruit";
    }
}

class Apple extends Fruit {
    @Override
    public String toString(){
        return "Apple";
    }
}

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

제한된 지네릭 클래스

지네릭은 타입 안정성을 보장한다고 했다. <T>타입으로 무슨 타입이든 받는 것은 과연 안전할까? 좀더 안전하게 <T>에 지정할 수 있는 타입을 제한할 수 있는 방법이 있다.

class Box<T extends Fruit>

간단하다. 지네릭 타입에 'extends'를 사용하면 된다. 구태여 설명할 것도 없겠지만, 매개변수화된 타입으로 Fruuit을 상속하는 클래스만 받겠다는 뜻이다. 참고로 인터페이스를 구현한 클래스를 매개변수화된 타입으로 받겠다고 선언할 때도 'extends'를 사용한다.

package com.javaex.generics;

import java.util.ArrayList;
import java.util.List;

public class FruitsBoxEx2 {
    public static void main(String[] args) {
        FruitBox<Fruit> fruitBox = new FruitBox<>();
        FruitBox<Apple> appleBox = new FruitBox<>();
        FruitBox<Grape> grapeBox = new FruitBox<>();
        ToyBox<Toy> toyBox = new ToyBox<>();

        fruitBox.add(new Fruit());
        fruitBox.add(new Apple());
        fruitBox.add(new Grape());

        appleBox.add(new Apple());

        grapeBox.add(new Grape());

        System.out.println("fruitBox - " + fruitBox);
        System.out.println("appleBox - " + appleBox);
        System.out.println("grapeBox - " + grapeBox);
    }

}
//Eatable을 구현하면서, Fruit를 상속하는 타입만 <T>로 받는다.
class FruitBox<T extends Fruit & Eatable> extends Box<T> {}
//Funable을 구현한 타입만 <T>로 받는다.
class ToyBox<T extends Funable> {}

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

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

    T getItem(int i) {
        return list.get(i);
    }

    int size() {
        return list.size();
    }

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

interface Eatable {}
interface Funable {}

class Fruit implements Eatable {
    @Override
    public String toString(){
        return "Fruit";
    }
}

class Apple extends Fruit {
    @Override
    public String toString(){
        return "Apple";
    }
}

class Grape extends Fruit {
    @Override
    public String toString(){
        return "Grape";
    }
}
class Toy implements Funable{
    @Override
    public String toString(){
        return "Toy";
    }
}

간단하게 조금 설명하자면.

class Box<T>

이 클래스는 세상 모든 박스의 원형이다. 말하자면 나로써는 감히 상상만 해볼 수밖에 없는 Box의 이데아! 우리는 이 박스를 이용해서 세상에 존재하는 모든 물건을 담을 수도 있을 것이다. 그러면 아마 잡동사니 박스가 되겠지. 그것을 도저히 견딜 수 없는 인간. 그러니까 너무나 게을러서 이 박스에 뭐가 들었는지 하나하나 찾아보기가 너무나 귀찮은 인간은 분류라는 것을 하기로 했다.

박스를 여러 개로 나눈 다음, 1번 박스에는 먹을 수 있는 과일만 담기로 하고, 2번 박스에는 장난감만 담기로 한다.
코드로 이렇겠지.

class FruitBox<T extends Fruit & Eatable> extends Box<T> {}
class ToyBox<T extends Funable> {}

이렇게 하면 언제든지 1번 박스에는 과일만, 2번 박스에는 장난감만 있을 것이다. 어쩌다 실수로 과일박스에 장난감을 넣을 수도, 장난감박스에 과일을 넣을 수도 없다.

그러니까 제한된 제네릭 클래스 얘기다.

'Java' 카테고리의 다른 글

제네릭 메서드Generic Method  (0) 2020.09.09
와일드카드  (0) 2020.09.09
TreeMap  (0) 2020.09.09
HashMap  (0) 2020.09.09
TreeSet  (0) 2020.09.09