Language/Java

9. 제네릭스(gernerics)

리져니 2021. 7. 26. 01:12

내부에서 사용할 데이터 타입을 외부에서 지정하는 기법.

컴파일 시의 타입 체크를 해주는 기능으로, 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄인다.

타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

* 타입의 안정성을 높인다: 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올때 원래 타입과 다르게 형변환되는 오류를 줄여준다.

 

Class Box{
	Object item;
    
    void setItem(Object item){ this.item = item };
    Object getItem() { return item; }
}

위의 Box 클래스를 제네릭 클래스로 변경해 보면,

 

class Box<T>{	// 지네릭 타입 T를 선언
	T item;
    
	void setItem(T item) { this.item = item; }
    T getItem() { return item; }
}

Box<T>에서 T는 타입 변수라고 한다. 타입 변수가 여러 개인 경우에는 Map<K,V>와 같이 콤마 ','로 구분하여 나열하면 된다.

 

제네릭 클래스가 된 Box 클래스의 객체를 생성해 보면,

Box<String> b = new Box<String>();	// T대신 실제 타입을 지정
b.setItem(new Object());		// error. String이외의 타입은 지정할수 없다
b.setItem("ABC");			// OK
String item = b.getItem();		// (String) b.getItem() 처럼 형변환이 필요없다

 

Box클래스 인스턴스 b를 String 타입으로 지정 했으므로, 제네릭 클래스 Box<T>는 다음과 같이 정의된 것이다.

Class Box{	// 지네링 타입을 String으로 지정
	String item;
    
    void setItem(String item)	{ this.item = item; }
    String getItem()	{ return item; }
}

 

제네릭스의 용어

Box<T>: 제네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다.

T: 타입 변수 또는 타입 매개변수.

Box: 원시 타입 (raw type)

참조변수와 생성자에 대입된 타입이 일치해야 한다

Box<String>과 Box<Integer>는 지네릭 클래스 Box<T>에 서로 다른 타입을 대입하여 호출한 것일뿐, 이 둘이 별개의 클래스를 의미하는 것은 아니다.

컴파일 후에 Box<String>과 Box<Integer>는 원시 타입인 Box로 바뀐다. (지네릭 타입이 제거됨)

 

제네릭스 클래스 객체생성 및 사용 예제

import java.io.IOException;
import java.util.ArrayList;


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

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


class Toy{
    public  String toString(){return "Toy";}
}

class Box<T>{
    ArrayList<T> list = new ArrayList<>();
    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();}
}

public class Main {
    public static void main(String[] args) throws IOException {
        Box<Fruit> fruitBox = new Box<>();
        Box<Apple> appleBox = new Box<>();
        Box<Toy> toyBox = new Box<>();

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

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

        toyBox.add(new Toy());

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

    }
}

 

실행 결과

 

 

제네릭스의 제한

제네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정할 수 있다.

(인스턴스별로 다르게 동작하도록 만든 기능이기 때문)

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

static멤버에는 타입 변수 T를 사용할수 없다. static멤버는 인스턴스 멤버를 참조할수 없는데. T는 인스턴스 변수로 간주되기 때문이다. (static멤버는 대입된 타입 종류에 관계없이 동일한 것이여야함)

또한, 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. (지네릭 배열 타입의 참조변수 선언은 가능하지만 생성은 할수 없음)

지네릭 배열을 생성해야 한다면, newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object배열을 생성해서 복사한 다음에 T[]로 형변환하는 방법 등을 사용하면된다.

 

두 타입이 상속 관계에 있을때, (Apple이 Fruit의 자손이라고 가정)

Box<Fruit> appleBox = new Box<Apple>();	// 애러
Box<Apple> appleBox = new FruitBox<Apple>()	// OK. 다형성

Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit());	// OK
fruitBox.add(new Apple());	// OK

 

특정 타입의 자손들만 대입할 수 있게 제한하기 (extends 사용)

class FruitBox<T extends Fruit>{	// Fruit의 자손만 타입으로 지정 가능
	ArrayList<T> lsit = new ArrayList<>();
    ArrayList<T> list = new ArrayList<>();
    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();}
}

FruitBox<Apple> appleBox = new FruitBox<Apple>();	// OK
FruitBox<Apple> appleBox = new FruitBox<Apple>();
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();

fruitBox.add(new Apple());
fruitBox.add(new Toy());

System.out.println(fruitBox);	// [Apple, Toy]

* 인터페이스 구현에도 extends 키워드를 사용한다 (implements 아님)

 

클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야 하는 경우 ('&' 사용)

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

 

와일드 카드

static 메서드의 매개변수로 특정한 제네릭 클래스를 사용할때, 해당 메서드를 오버라이딩 하기위해 매개변수에 다른 제네릭 클래스를 선언하게 되면 컴파일 애러가 발생한다.

제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다.

이를 위해 와일드 카드를 사용한다. 화일드 카드는 기호 '?'로 표현하며 어떠한 타입도 될수 있다.

와일드 카드 사용법

import java.io.IOException;
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 Juice{
    String name;

    Juice(String name){ this.name = name + "Juice";}
    public String toString(){return name;}
}

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


class Box<T>{
    ArrayList<T> list = new ArrayList<>();
    void add(T item){list.add(item);}
    ArrayList<T> getList() {return list;}
    public String toString(){return list.toString();}
}

class FruitBox<T extends Fruit> extends Box<T>{}


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

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

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


    }
}

실행 결과

 

제네릭 메서드

메서드의 선언부에 제네릭 타입이 선언된 메서드

 

기존에 작성했던 메서드를 제네릭 메서드로 변경해 보면,

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);
    }

 

제네릭 메서드를 호출하기 위해서는 아래와 같이 두가지 방법이 있다.

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

//변경된 지네릭 메서드를 호출하려면 아래와 같이 사용할 수있다.
System.out.println(Juicer.<Fruit>.makeJuice(fruitBox));
System.out.println(Juicer.<Apple>.makeJuice(appleBox));

//하지만 대부분 컴파일러가 타입을 추정할수 있기 때문에 생략 가능하다.
System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));

 

제네릭 타입의 형변환

제네릭 타입과 넌제네릭(non-generic)타입 간의 형변환은 항상 가능하다. (경고가 발생할 뿐)

그러나 대입된 타입이 다른 제네릭 타입 간에는 형변환이 불가능 하다

Box box = null;
Box<Object> objBox = null;

box = (Box)objBox; 		// ok
objBox = (Box<Object>)box;	// ok

 
Box<String> strBox = null;

objBox = (Box<Object>) strBox;	// error
strBox = (Box<String>) objBox;	// error

 

이전에 배웠던 와일드 카드를 사용하면 형변환이 가능하다.

FruitBox<? extends Fruit> box1 = new FruitBox<Fruit>();
FruitBox<? extends Fruit> box2 = new FruitBox<Apple>();
FruitBox<? extends Fruit> box3 = new FruitBox<Grape>();

 

728x90