자바 지네릭스

계리·2023년 4월 19일
0
post-thumbnail

지네릭스(Generics)란?

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(complie-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

타입 안정성을 높인다는 것은 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

지네릭스 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.


지네릭스 클래스의 선언

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

Box<'T'>에서 T를 '타입 변수(type variable)'라고 하며 'Type'의 첫 글자에서 따온 것이다. 타입 변수는 T가 아닌 다른 것을 사용해도 된다. ArrayList<'E'>의 경우 'Element(요소)'의 첫 글자를 따서 사용했다. 타입 변수가 여러 개인 경우에는 Map<K, V> 처럼 콤마를 사용하면 된다. 무조건 T를 사용하기보다 상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋다.


지네릭스 용어

Box<T> : 지네릭 클래스 'T의 Box' 또는 'T Box'라고 읽는다.
T : 타입 변수 또는 타입 매개변수.<T는 타입 문자)
Box : 원사 타입(ray type)

지네릭스의 제한

지네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다. 지네릭스는 이처럼 인스턴스별로 다르게 동작하도록 하려고 만든 기능이다.

	Box<Apple> appleBox = new Box<Apple>();	//	OK. Apple객체만 저장가능
    Box<Grape> grapeBox = new Box<Grape>();	//	OK. Grape객체만 저장가능

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

class Box<T> {
	static T item;	//	에러
    static int compare(T t1, T t2) { ... }	//	에러
		...

static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다. 즉 'Box<'Apple'>'.item과 Box<'Grape'>.item이 다른 것이어서는 안된다는 것이다. 그리고 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 'new T[10]'과 같이 배열을 생성하는 것은 안된다.

class Box<T> {
		T[] itemArr;	//	OK. T타입의 배열을 위한 참조변수
        ...
        T[] toArray() {
        	T[] tmpArr = new T[itemArr.length];	//	에러. 지네릭 배열 생성불가.
            ...
            return tmpArr;
        }
        	...
 }

new연산자 때문에 지네릭 배열을 생성할 수 없게 된다. 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 그런데 위 코드처럼 Box<'T'>클래스를 컴파일하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없다. instanceof연산자도 new연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.

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<Toy>();
//        Box<Grape> grapeBox = new Box<Apple>();   //  에러. 타입 불일치

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

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

        toyBox.add(new Toy());
//        toyBox.add(new Apple());  // 에러. Box<Toy>에는 Apple을 담을 수 없음

        System.out.println(fruitBox);
        System.out.println(appleBox);
        System.out.println(toyBox);
    }   //  main의 끝
}

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

결과
[Fruit, Apple]
[Apple, Apple]
[Toy]

와일드카드

와일드카드는 지네릭스를 사용할 때 지네릭스로 지정한 타입이 달라도 지네릭스를 사용할 수 있는 방법이다.

와일드카드 기호는 '?'로 표현하는데 이 와일드카드로 어떠한 타입도 될 수 있다.

"?"가 Object타입과 다를 게 없기 때문에 다음과 같이 'extends'와 'supber'로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.

  • " ? extends T " 와일드카드의 상한 제한. T와 그 자손들만 가능
  • " ? super T " 와일드카드의 하한 제한. T와 그 조상들만 가능
  • " ? " 제한 없음. 모든 타입이 가능. " ? extends Object "와 동일

아래 예제를 통해 확인해보자

? extends T 예제

<12-3>
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(Object f : box.getList())
            tmp += f + " ";
        return new Juice(tmp);
    }
}

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

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

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

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

결과
Apple Grape Juice
Apple Apple Juice

class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box){
String tmp = "";

    for(Object f : box.getList())
        tmp += f + " ";
    return new Juice(tmp);
}

}

이 부분에서 "FruitBox<? extends Fruit> box" 부분을 보면 Fruit로 부터 상속 받은 클래스 타입들에 대해서만 상관 없이 매개변수로 받겠다는 의미이다.

이렇게 되면 Fruit로 상속받은 Apple타입 클래스와 Grape클래스 어느 클래스 타입이 들어와도 해당 메서드를 실행시킬 수 있다.


? super T 예제

[12-4]
import java.util.*;

class Fruit{
    String name;
    int weight;

    Fruit(String name, int weight){
        this.name = name;
        this.weight = weight;
    }

    public String toString() { return name + " ("+weight+") "; }
}

class Apple extends Fruit{
    Apple(String name, int weight){
        super(name, weight);
    }
}

class Grape extends Fruit{
    Grape(String name, int weight){
        super(name, weight);
    }
}

class AppleComp implements Comparator<Apple>{
    public int compare(Apple t1, Apple t2){
        return t2.weight - t1.weight;
    }
}

class GrapeComp implements Comparator<Grape>{
    public int compare(Grape t1, Grape t2){
        return t2.weight - t1.weight;
    }
}

class FruitComp implements Comparator<Fruit>{
    public int compare(Fruit t1, Fruit t2){
        return t1.weight - t2.weight;
    }
}

public class FruitBoxEx4 {
    public static void main(String[] args) {
        FruitBox<Apple> appleBox = new FruitBox<Apple>();
        FruitBox<Grape> grapeBox = new FruitBox<Grape>();

        appleBox.add(new Apple("GreenApple", 300));
        appleBox.add(new Apple("GreenApple", 100));
        appleBox.add(new Apple("GreenApple", 200));

        grapeBox.add(new Grape("GreenGrape", 400));
        grapeBox.add(new Grape("GreenGrape", 300));
        grapeBox.add(new Grape("GreenGrape", 200));

        Collections.sort(appleBox.getList(), new AppleComp());
        Collections.sort(grapeBox.getList(), new GrapeComp());
        System.out.println(appleBox);
        System.out.println(grapeBox);
        System.out.println();
        Collections.sort(appleBox.getList(), new FruitComp());
        Collections.sort(grapeBox.getList(), new FruitComp());
        System.out.println(appleBox);
        System.out.println(grapeBox);
    }   // main
}

class FruitBox<T extends Fruit> extends Box<T> { }
class Box<T>{
    ArrayList<T> list = new ArrayList<T>();

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

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

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

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

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

결과
[GreenApple (300) , GreenApple (200) , GreenApple (100) ]
[GreenGrape (400) , GreenGrape (300) , GreenGrape (200) ]

[GreenApple (100) , GreenApple (200) , GreenApple (300) ]
[GreenGrape (200) , GreenGrape (300) , GreenGrape (400) ]

예제에서 보면 Collections.sort() 메서드를 확인해보면 다음과 같다.

static <T> void sort(List<T> list, Comparator<? super T> c)

sort메서드가 위와 같이 정의 되어있는데 와일드카드를 사용하지 않고 사용했다고 가정해보고 타입 매개변수 T에 Apple이 대입되면, 아래와 같이 대입 될 것이다.

static void sort(List<Apple> list, Comparator<Apple> c)

현재까지는 문제가 없어보이지만 Apple 대신 Grape가 대입된다고 하면 List를 정렬하기 위해 Comparator가 필요하다. <>안에 타입이 맞지 않기 때문에 Comparator로는 List를 정렬할 수 없다.

그러면 Comparator클래스를 implements하여 구현한 구현체들을 타입에 맞게 생성을 해서 타입에 맞게 대입을 해야한다.

그러면 코드의 중복 문제도 있지만 새로운 타입이 생길 떄 마다 같은 코드르 반복해서 만들어야 한다는 것이 더 문제가 된다.

앞서 Collections.sort 메서드를 살펴본 것 처럼 원래 정의되어 있는 것을 보면

static void sort(List list, Comparator<? super T> c)

위 코드에서 T에 Apple이 대입되면 아래와 같이 된다.

static <T> void sort(List<Apple> list, Comparator<? super Apple> c)

매개변수의 타입이 Comparator<? super Apple>이라는 의미는 Comparator의 타입 매개변수로 Apple과 그 조상이 가능하다는 뜻이다. 즉, Comparator, Comparator, Comparator 중의 하나가 두 번째 매개변수로 올 수 있다는 뜻이다.

Comparator<? super Apple>

  • Comparator<Apple>
  • Comparator<Fruit>
  • Comparator<Object>

Comparator<? super Grape>

  • Comparator<Grape>
  • Comparator<Fruit>
  • Comparator<Object>

그래서 위 예제 <12-4>에서 만든 구현체인 FruitComp를 만들어 사용하면 List과 List를 모두 정렬할 수 있다.

Apple과 Grape는 Fruit의 조상으로 정의되어 있기 때문에 가능한 것이다.


※ 참고 문헌
남궁성, 『Java의 정석 3nd Edition』, 도우출판(2016) 책으로 공부하고 정리한 내용 입니다.

profile
gyery

0개의 댓글