본문 바로가기
JAVA/Java

[Java] 제네릭(Generic)

by 민트맛녹차 2022. 9. 8.

제네릭(Generic)

class Box<T> {
    private T ob;
    
    public void set(T o) { ob = o; }
    public T get() { return ob; }
}

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

제너릭이란 한 가지 데이터 타입에 의존적이지 않도록, 타입을 내부적으로 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미한다.

위 코드는 제너릭을 사용해 정의된 클래스이다. Box<T> 클래스에서 사용된 T를 타입 매개변수(Type Parameter), Box<Apple> 에서 타입 매개변수에 전달된 Apple 클래스를 타입 인자(Type Argument), Box<Apple>이라는 새로운 타입을 매개변수화 타입(Parameterized Type) 또는 제너릭 타입(Generic Type) 이라고 한다.

 

제네릭 사용 이유

class Box {
    private Object ob;
    
    public void set(Object o) { ob = o; }
    public Object get() { return ob; }
}

Apple ap = (Apple)aBox.get();

제너릭 사용 이전에는 Object 클래스를 타입으로 사용해 형 변환(Type Casting)을 했다. 하지만 필요시 형 변환을 해야하거나, 자료형과 관련된 프로그래머의 실수가 컴파일 과정에서 드러나지 않는 문제점이 있었다. 특히 형 변환의 경우, List 의 데이터를 꺼낼 때 여러 번의 형 변환이 필요하게 되어 성능의 저하를 일으킨다. 제너릭의 등장 이후로 이러한 문제점들이 해결되었다.

제너릭을 사용하면 잘못된 타입의 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있고, 따로 타입을 체크하고 변환해 줄 필요가 없으며, 코드 재사용성이 높아지는 장점이 있다.

 

제네릭의 기본 문법

타입 매개변수는 일반적으로 다음 두 가지 규칙을 지켜서 이름을 짓는다.

1. 한 문자로 이름을 짓는다.

2. 대문자로 이름을 짓는다.

보통 아래 표의 타입 매개변수를 주로 사용한다.

타입 매개변수 의미
E Element
K Key
N Number
T Type
V Value

 

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

컴파일러는 제너릭 관련 문장에서 자료형의 이름을 추론하는 능력을 갖고 있으므로, 생성자의 타입 인자를 생략해도 된다. 컴파일러는 참조 변수를 통해 <>(다이아몬드 기호) 안에 Apple이 생략되었다고 판단한다.

 

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

위 코드처럼 제너릭 타입이 타입 인자로 사용될 수 있다.

 

class Box<K, V> { ... }

두 개 이상의 타입 매개변수를 사용할 수 있다.

 

public static <T> Box<T> makeBox(T o) { ... }

클래스 뿐만 아니라 일부 메서드에 대해서만 제네릭으로 정의하는 것이 가능하다. 이러한 메서드를 제네릭 메서드라고 한다. 클래스와 독립적인 제네릭을  메서드에 사용할 수 있다. 

Box<String> sBox = BoxFactory.<String>makeBox("Sweet");
Box<String> sBox = BoxFactory.makeBox("Sweet");

메서드 사용 시 제네릭 메서드의 타입 인자를 생략해도 된다. 컴파일러는 makeBox에 전달된 인자를 보고 T를 유추할 수 있기 때문이다.

 

제한된 제네릭

class Box<T extends Number> { ... }
public static <T extends Number> Box<T> makeBox(T o) { ... }

예제에서 사용한 Box<T>의 T에는 모든 참조 타입이 담길 수 있다. extend를 사용하면 타입 인자를 제한할 수 있다.

위의 코드는 타입 인자에 Number 또는 Number를 상속하는 클래스만 허용된다. 제네릭 클래스 뿐만 아니라 제네릭 메서드도 타입 인자를 제한할 수 있다.

class Box<T extends Number> {
    private T ob;
    ...
    public int toIntValue() {
    	return ob.intValue();	//Number의 intValue()
    }
}

타입 인자를 제한함으로써, 제한하는 클래스/인터페이스에 선언되어 있는 메서드들을 호출할 수 있다.

 

제네릭과 상속

class SteelBox<T> extends Box<T> {
    public SteelBox(T o) {
    	ob = o;
    }
}

제네릭 클래스도 상속이 가능하다. 

Box<Integer> sBox = new SteelBox<Integer>(13);

따라서, 위와 같이 Box<T>의 참조변수로 SteelBox<T> 인스턴스를 참조하는 문장을 만들 수 있다. "SteelBox<Integer> 제네릭 타입은 Box<Integer> 제네릭 타입을 상속한다"고 표현한다.

Box<Number> box = new Box<Integer>(); // 컴파일 불가

이때 조심해야 할 것은 Number를 Integer가 상속한다고 Box<Number>와 Box<Integer>도 상속관계를 형성하지는 않는다는 것이다. 

 

와일드 카드(WildCard)

public static <T> void peekBox(Box<T> box) { ... }
public static void peekBox(Box<?> box) { ... }

제네릭 메서드를 와일드 카드라고 불리는 <?>를 사용하여 위와 같이 선언할 수 있다. 위와 같이 ?를 단독으로 사용하는 경우 모든 참조타입이 들어갈 수 있다. 

 

와일드 카드의 상한과 하한의 제한(Upper Bounded WildCard and Lower Bounded WildCard)

와일드 카드는 extends와 super를 통해 타입의 경계를 정할 수 있다.

public static void peekBox(Box<? extends T> box) { ... }

상한 제한된 와일드 카드는 extends를 사용하여 T 또는 T의 하위 클래스/인스턴스만 전달되도록 제한한다. 

public static void peekBox(Box<? super T> box) { ... }

하한 제한된 와일드 카드는 super를 사용하여 T 또는 T의 상위 클래스/인스턴스만 전달되도록 제한한다.

 

와일드 카드에 제한을 거는 이유가 무엇일까?

class Toy { ... }

class BoxHandler {
    public static void outBox(Box<Toy> box) {
    	Toy t = box.get();
        System.out.println(t);
    }
    public static void inBox(Box<Toy> box, Toy n) {
        box.set(n);
    }
}

잘 만들어진 코드는 "필요한 만큼만 기능을 허용하여, 코드의 오류가 컴파일 과정에서 최대한 발견되도록 한다" 라는 조건을 만족해야 한다. BoxHandler의 메서드들을 보면 outBox는 box에서 인스턴스를 꺼내는 역할을, inBox는 box에 인스턴스를 저장하는 역할을 한다.

 

public static void outBox(Box<Toy> box) {
    box.get();	        // 가능
    box.set(new Toy());	// 가능
}

public static void outBox(Box<? extends Toy> box) {
    box.get();	        // 가능
    box.set(new Toy());	// Error
}

outBox 메서드에 실수로 set을 호출했다고 가정하자. 상한 제한이 되지 않는 코드는 set이 들어가 오류를 범해도 컴파일 과정에서 발견되지 않는다. 이때 extends를 사용하여 상한 제한을 하면 get은 가능하지만 set이 불가능하게 된다. 

set 호출이 불가능한 이유는 Box<? extends Toy> 이 Toy 인스턴스를 저장할 수 있는 클래스임을 보장할 수 없기 때문이다.

class Car extends Toy { ... }
class Robot extends Toy { ... }

Toy 클래스는 위의 클래스들처럼 상속이 될 수 있다. 그렇다면 outBox 메서드에 Box<Car> 이나 Box<Toy> 인스턴스가 인자로 전달될 수 있으므로 get은 가능하지만 Toy 인스턴스를 담는 set은 불가능하게 된다. 

따라서 extends를 사용하면 상한 제한을 통해 메서드에서 get의 호출만을 가능하게 해준다.

 

public static void inBox(Box<Toy> box, Toy n) {
    box.set(n);		        // 가능
    Toy myToy = box.get();	// 가능
}

public static void inBox(Box<? super Toy> box, Toy n) {
    box.set(n);		        // 가능
    Toy myToy = box.get();	// Error
}

inBox 메서드에 실수로 get을 호출했다고 가정하자. 하한 제한이 되지 않은 코드는 get이 들어가 오류를 범해도 컴파일 과정에서 발견되지 않는다. 이때 super를 사용하여 하한 제한을 하면 set은 가능하지만 get 호출 후 참조변수의 타입으로 Toy를 사용하는 것이 불가능하게 된다.

class Plastic { ... }
class Toy extends Plastic { ... }

Box<Toy> tBox = new Box<Toy>();
Toy myToy = box.get();	// get이 반환하는 것이 Toy 인스턴스

Box<Plastic> pBox = new Box<Plastic>();
Toy myToy = box.get();	// get이 반환하는 것이 Plastic 인스턴스 -> Error

Toy 클래스가 Plastic 클래스를 상속한다고 하자. inBox 메서드의 인자로 tBox가 들어온다면 문제가 없지만 , 인자로 pBox가 들어온다면 Plastic 인스턴스는 Toy 인스턴스로 다운캐스팅이 불가능하기 때문에 문제가 생긴다.

따라서 super를 사용하면 하한 제한을 통해 메서드에서 set의 호출만을 가능하게 해준다.

 

메서드 오버로딩과 <? extends T>, <? super T>

class BoxHandler {
    
    // 오버로딩 불가
    public static void outBox(Box<? extends Toy> box) { ... }
    public static void outBox(Box<? extends Robot> box) { ... }
    
    // 두 번째 매개벼수로 인해 오버로딩 가능
    public static void inBox(Box<? super Toy> box, Toy n) { ... }
    public static void inBox(Box<? super Robot> box, Robot n) { ... }
    
}

outBox의 경우 오버로딩이 성립하지 않는다. 컴파일 과정에서 메서드의 매개변수는 제네릭과 와일드 카드 관련 정보를 지우는 과정을 거친다. 컴파일 과정에서 매개변수들은 모두 Box box 로 수정이 되므로 오버로딩이 불가능한 상태가 된다.

이처럼 컴파일러가 제네릭 정보를 지우는 행위를 Type Erasure 라고 한다.

inBox의 경우 제네릭과 관련 없는 두 번째 매개변수의 자료형이 다르기 때문에 오버로딩이 가능하다.

 

class BoxHandler {
    public static void outBox(Box<? extends T> box) { ... }
    public static void inBox(Box<? super T> box, T n) { ... }
}

오버로딩이 불가능 하므로, <? extends T> 와 <? super T>를 사용하면 Box<Toy> 인스턴스와 Box<Robot> 인스턴스를 모두 허용할 수 있도록 정의할 수 있다.

 

 

참조
윤성우의 열혈 JAVA  프로그래밍
https://medium.com/%EC%8A%AC%EA%B8%B0%EB%A1%9C%EC%9A%B4-%EA%B0%9C%EB%B0%9C%EC%83%9D%ED%99%9C/java-generic-%EC%9E%90%EB%B0%94-%EC%A0%9C%EB%84%A4%EB%A6%AD-f4343fa222df
https://st-lab.tistory.com/153

댓글