제네릭이 갖는 의미는 일반화이다.
자바에서 일반화의 대상은 자료형이다.
class Apple {
@Override
public String toString() {
return "I am an apple.";
}
}
class Orange {
@Override
public String toString() {
return "I am an orange.";
}
}
class AppleBox {
private Apple ap;
public void set(Apple a) {
ap = a;
}
public Apple get() {
return ap;
}
}
class OrangeBox {
private Orange or;
public void set(Orange o) {
or = o;
}
public Orange get() {
return or;
}
}
public class FruitAndBox {
public static void main(String[] args) {
AppleBox aBox = new AppleBox();
OrangeBox oBox = new OrangeBox();
aBox.set(new Apple());
oBox.set(new Orange());
Apple ap = aBox.get();
Orange or = oBox.get();
System.out.println(ap);
System.out.println(or);
}
}
// 실행 결과
I am an apple.
I am an orange.
위 예제에서 AppleBox
, OrangeBox
의 역할과 내용은 같다.
따라서 이 둘은 하나로 대체할 수 있다.
class Apple {
@Override
public String toString() {
return "I am an apple.";
}
}
class Orange {
@Override
public String toString() {
return "I am an orange.";
}
}
class Box { // Box 하나로 대체
private Object ob;
public void set(Object o) {
ob = o;
}
public Object get() {
return ob;
}
}
public class FruitAndBox2 {
public static void main(String[] args) {
Box aBox = new Box();
Box oBox = new Box();
aBox.set(new Apple());
oBox.set(new Orange());
Apple ap = (Apple) aBox.get();
Orange or = (Orange) oBox.get();
System.out.println(ap);
System.out.println(or);
}
}
// 실행 결과
I am an apple.
I am an orange.
위 예제에 Object형을 사용하므로 다음을 주의해야한다.
Box 인스턴스에서 내용물을 꺼낼 때 형 변환을 해야 한다.
Box 내에서 인스턴스를 저장하는 참조변수가 Object형이기 때문에, 저장된 인스턴스를 꺼낼 때에는 인스턴스에 맞는 형 변환을 해야만 한다.
이 과정은 다음과 같은 실수가 발생할 수도 있다.
...
public class FruitAndBoxFault {
public static void main(String[] args) {
Box aBox = new Box();
Box oBox = new Box();
// 사과와 오렌지 객체가 아닌 '문자열'을 담았다.
aBox.set("Apple");
oBox.set("Orange");
// 컴파일 과정에서 오류가 발생하지 않고 실행하면 예외가 발생한다.
Apple ap = (Apple) aBox.get();
Orange or = (Orange) oBox.get();
System.out.println(ap);
System.out.println(or);
}
}
모든 실수는 컴파일 단계에서 드러나는 것이 좋다.
컴파일 오류는 원인을 바로 찾을 수 있다.
실행 중 발생하는 예외는 다르다.
예외의 원인은 쉽게 발견되지 않는 경우도 많다.
뿐만 아니라 위와 같은 실수는 드러나지 않을 수도 있다.
public class FruitAndBoxFault {
public static void main(String[] args) {
Box aBox = new Box();
Box oBox = new Box();
// 사과와 오렌지 객체가 아닌 '문자열'을 담았다.
aBox.set("Apple");
oBox.set("Orange");
System.out.println(aBox.get());
System.out.println(oBox.get());
}
}
// 실행 결과
Apple
Orange
위 예제는 대형 사고로 이어질 수 있다.
제네릭이 등장하면서 자료형에 의존적이지 않은 클래스를 정의할 수 있게 된다.
사과를 저장할 목적이면 T를 Apple로 결정하면 되고, 오렌지를 저장할 목적이면 T를 Orange로 결정하면 된다.
인스턴스 생성 시 T의 자료형을 결정하는 것이 제네릭이다.
T는 인스턴스 생성 시 자료형을 결정하기 위한 표식이다.
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
T를 가리켜 타입 매개변수라 한다.
메소드의 매개변수와 유사하게 자료형 정보를 인자로 전달받는 형태이기 때문이다.
또한 T에 전달되는 인자를 가리켜 타입 인자라 한다.
마지막으로 Box을 가리켜 매개변수화 타입이라 한다.
제네릭 기반으로 코드를 작성하면 다음 불편함과 문제점이 사라진다.
제네릭 클래스도 상속이 가능하다.
class Box<T> {
protected T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class StellBox<T> extends Box<T> {
public StellBox(T o) {
ob = o;
}
}
public class GenericInheritance {
public static void main(String[] args) {
Box<Integer> iBox = new StellBox<>(7959);
Box<String> sBox = new StellBox<>("Simple");
System.out.println(iBox.get());
System.out.println(sBox.get());
}
}
// 실행 결과
7959
Simple
Box<Integer>
와 같은 것을 매개변수화 타입
또는 제네릭 타입
이라 한다.
타입이라는 단어는 Box를 일종의 자료형으로, 정확히는 클래스의 이름으로 간주함을 뜻한다.
따라서 다음과 같은 상속 관계를 표현할 수 있다.
하지만 다음은 성립하지 않는다.
Box<Number> box = new Box<Integer>(); // 컴파일 불가
컴파일러가 자료형 유추를 진행하는 상황은 다양하다.
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class EmptyBoxFactory {
public static <T> Box<T> makeBox() {
Box<T> box = new Box<>();
return box;
}
}
public class TargetTypes {
public static void main(String[] args) {
Box<Integer> iBox = EmptyBoxFactory.makeBox();
iBox.set(25);
System.out.println(iBox.get());
}
}
// 실행 결과
25
EmptyBoxFactory
클래스의 makeBox
메소드는 인자를 전달받지 않는다.
따라서 다음과 같이 T에 대한 타입 인자를 전달해야 한다.
Box<Integer> iBox = EmptyBoxFactory.<Integer>makeBox();
그런데 자바 7부터 다음과 같이 호출하는 것이 가능하다.
Box<Integer> iBox = EmptyBoxFactory.makeBox();
makeBox
메소드는 Box 인스턴스의 참조 값을 반환해야 한다고 판단한다.
왼편에 선언된 매개변수의 형을 보고 이러한 판단을 한다.
따라서 makeBox 메소드 호출 시 T는 Integer가 되어야함을 알 수 있다.
위 상황에서 T의 유추에 사용된 정보 Box<Integer>
를 가리켜 타겟 타입
이라 한다.
다음과 같은 제네릭 메소드가 있다.
public static <T> void peekBox(Box<T> box) {
System.out.println(box);
}
이 메소드를 제네릭으로 정의한 이유는 Box<Integer>
, Box<String>
의 인스턴스를 인자로 전달받도록 하기 위함이다.
그러면 다음과 같이 정의하는 것도 생각할 수 있다.
public static void peekBox(Box<Object> box) {
System.out.println(box);
}
하지만 위 메소드는 안된다.
즉 Object와 String이 상속 관계에 있더라도 Box와 Box은 상속 관계를 형성하지 않는 별개의 자료형이다.
대신 와일드카드라는 것을 사용하면 원하는 바를 이룰 수 있다.
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
@Override
public String toString() {
return ob.toString();
}
}
class Unboxer {
public static <T> T openBox(Box<T> box) {
return box.get();
}
public static void peekBox(Box<?> box) { // wildcard 사용
System.out.println(box);
}
}
public class WildcardUnboxer2 {
public static void main(String[] args) {
Box<String> box = new Box<>();
box.set("So Simple String");
Unboxer.peekBox(box);
}
}
// 실행 결과
So Simple String
위 예제에서 다음과 같이 제네릭 메소드와 와일드카드 기반 메소드가 있다.
public static <T> T openBox(Box<T> box) { // 제네릭 메소드
return box.get();
}
public static void peekBox(Box<?> box) { // wildcard 메소드
System.out.println(box);
}
기능적인 측면에서 보면 위 두 메소드는 완전히 동일하다.
제네릭 메소드와 와일드카드 기반 메소드는 상호 대체 가능한 측면이 있다.
하지만 와일드카드 기반 메소드는 코드가 조금 더 간결하다.
public static void peekBox(Box<?> box) { // wildcard 메소드
System.out.println(box);
}
위 메소드의 인자로, Box에서 T가 Number 또는 Number의 하위 클래스인 제네릭 타입의 인스턴스만 전달되도록 제한할 때 다음과 같이 상한 제한된 와일드카드(Upper-Bounded Wildcards)
라는 것을 사용한다.
Box<? extends Number> box
-> box는 Box<T> 인스턴스를 참조하는 변수
-> Box<T> 인스턴스의 T는 Number 또는 이를 상속하는 하위 클래스여야 함
또한 다음과 같이 참조변수에 하한 제한된 와일드카드(Lower-Bounded Wildcards)
선언할 수도 있다.
Box<? super Integer> box
-> box는 Box<T> 인스턴스를 참조하는 변수
-> Box<T> 인스턴스의 T는 Integer 또는 Integer가 상속하는 클래스
-> Box<Integer>, Box<Number>, Box<Object>로 제한한다.
와일드카드의 상한 제한은 다음 코드를 통해 설명한다.
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class Toy {
@Override
public String toString() {
return "I am a 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);
}
}
public class BoundedWildCardBase {
public static void main(String[] args) {
Box<Toy> box = new Box<>();
BoxHandler.inBox(box, new Toy());
BoxHandler.outBox(box);
}
}
위 코드에서 관심을 두어야 할 부분은 BoxHandler
클래스에 정의된 다음 메소드다.
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);
}
}
위 두 메소드는 다음 조건을 만족하지 않는다.
outBox
메소드는 상자에서 내용물을 꺼내는 기능의 메소드로 설계했다.
하지만 매개변수 box를 대상으로는 다음과 같이 get, set 호출이 가능하다.
public static void outBox(Box<Toy> box) {
box.get();
box.set(new Toy());
}
위는 outBox 메소드 내에서 실수로 set 메소드를 호출하는 오류를 범할 수 있다.
이러한 오류는 컴파일 과정에서 발견되지 않는다.
get은 가능하지만, set은 불가능하도록 제한을 것는 것이 좋다.
이러한 일이 필요한 만큼만 기능을 허용하여, 코드의 오류가 컴파일 과정에서 최대한 발견되도록 하는 일이다
.
다음과 같이 정의하면 get은 가능하지만 set은 불가능하다.
public static void outBox(Box<? extends Toy> box) {
box.get();
box.set(new Toy()); // Error
}
위 outBox 메소드의 매개변수로 Box만 전달된다는 사실이 보장되지 않는다.
Toy 클래스는 다음과 같이 다른 클래스들에 의해 상속될 수 있다.
class Car extends Toy {}
class Robot extends Toy {}
이런 상속관계가 있다면 outBox 메소드에 Box 또는 Box 인스턴스가 인자로 전달될 수 있다.
public static void outBox(Box<? extends Toy> box) {
// box로 Box<Car> or Box<Robot> 인스턴스가 전달되면?
box.set(new Toy()); // Toy 인스턴스를 만들 수 없음, Error
}
설명을 바탕으로 위 예제를 다음과 같이 바꾼다.
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class Toy {
@Override
public String toString() {
return "I am a Toy";
}
}
class BoxHandler {
public static void outBox(Box<? extends Toy> box) { // set 제한
Toy t = box.get();
System.out.println(t);
}
public static void inBox(Box<Toy> box, Toy n) {
box.set(n);
}
}
public class BoundedWildCardUsage {
public static void main(String[] args) {
Box<Toy> box = new Box<>();
BoxHandler.inBox(box, new Toy());
BoxHandler.outBox(box);
}
}
이번에는 inBox 메소드를 살펴본다.
public static void inBox(Box<Toy> box, Toy n) {
box.set(n);
}
inBox도 다음 조건을 만족하지 못한다.
이 메소드는 상자에 인스턴스를 저장하는 것이 목적이다.
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
}
위와 같이 매개변수를 선언하면 get 메소드의 호출문에서 컴파일 오류가 발생한다.
이유는 반환형을 Toy로 결정할 수 없기 때문이다.
Toy 클래스의 상속 관계가 다음과 같다고 가정한다.
class Plastic {}
class Toy extends Plastic {}
그러면 inBox
메소드의 첫 번째 인자로 전달 가능한 두 가지 유형의 Box 인스턴스는 다음과 같다.
Box<Toy> tBox = new Box<Toy>();
Box<Plastic> pBox = new Box<Plastic>();
위의 inBox 메소드에 인자로 pBox
가 전달되면 다음 문장은 문제가 된다.
Toy myToy = box.get(); // get이 반환하는 것이 Plastic 인스턴스다.
정리하면 다음과 같다.
public static void inBox(Box<? super Toy> box, Toy n) {
box.set(n);
Toy myToy = box.get(); // error
}
-> 이 안에서는 box가 참조하는 인스턴스에서 Toy 인스턴스를 꺼내는 메소드 호출은 불가능하다.
위 예제를 수정한 코드는 다음과 같다.
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class Toy {
@Override
public String toString() {
return "I am a Toy";
}
}
class BoxHandler {
public static void outBox(Box<? extends Toy> box) { // set 불가
Toy t = box.get();
System.out.println(t);
}
public static void inBox(Box<? super Toy> box, Toy n) { // get 불가
box.set(n);
}
}
public class BoundedWildCardUsage2 {
public static void main(String[] args) {
Box<Toy> box = new Box<>();
BoxHandler.inBox(box, new Toy());
BoxHandler.outBox(box);
}
}
정리하면 다음과 같다.
Box<? extends Toy> box
-> box가 참조하는 인스턴스를 대상으로 꺼내는 작업만 허용, get
Box<? super Toy> box
-> box가 참조하는 인스턴스를 대상으로 넣는 작업만 허용, set
위에서 Toy 클래스를 담은 상자를 기준으로 다음과 같이 inBox
, outBox
메소드를 정의했다.
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);
}
}
위 두 메소드는 Box<Toy>
인스턴스를 대상으로 한다.
이 상황에서 다음 클래스가 추가된다고 가정한다.
class Robot {
@Override
public String toString() {
return "I am a Robot";
}
}
그리고 Box의 인스턴스를 대상으로 outBox
, inBox
메소드를 호출하고 싶다면 다음과 같이 메소드를 오버로딩하는 것을 고려할 수 있다.
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
메소드는 오버로딩이 성립하지 않는다.
자바는 제네릭 등장 이전에 정의된 클래스들과의 상호 호환성 유지를 위해 컴파일 시 제네릭과 와일드카드 관련 정보를 지우는 과정을 거친다.
즉 위의 두 outBox 메소드의 매개변수 선언은 컴파일 과정에서 다음과 같이 수정이된다.
이로 인해 메소드의 오버로딩이 불가능한 상태가 된다.
Box<? extends Toy> box -> Box box
Box<? extends Robot> box -> Box box
위와 같이 컴파일러가 제네릭 정보를 지우는 행위를 Type Erasure
라 한다.
Box와 Box 인스턴스를 동시에 허용할 수 있도록 하려면 제네릭 메소드
를 사용한다.
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class Toy {
@Override
public String toString() {
return "I am a Toy";
}
}
class Robot {
@Override
public String toString() {
return "I am a Robot";
}
}
class BoxHandler {
public static <T> void outBox(Box<? extends T> box) { // 제네릭 메소드
T t = box.get();
System.out.println(t);
}
public static <T> void inBox(Box<? super T> box, T n) { // 제네릭 메소드
box.set(n);
}
}
public class BoundedWildcardGenericMethod {
public static void main(String[] args) {
Box<Toy> tBox = new Box<>();
BoxHandler.inBox(tBox, new Toy());
BoxHandler.outBox(tBox);
Box<Robot> rBox = new Box<>();
BoxHandler.inBox(rBox, new Robot());
BoxHandler.outBox(rBox);
}
}
// 실행 결과
I am a Toy
I am a Robot
위와 같이 메소드를 오버로딩해야 하는 상황에서는 Type Erasure
라는 것 때문에 오버로딩으로 인정이 되지 않으니 제네릭 메소드
정의로 이를 대신해야 한다.
인터페이스 역시 클래스와 마찬가지로 제네릭으로 정의할 수 있다.
interface Getable<T> {
public T get();
}
class Box<T> implements Getable<T> {
private T ob;
public void set(T o) {
ob = o;
}
@Override
public T get() {
return ob;
}
}
class Toy {
@Override
public String toString() {
return "I am a toy";
}
}
public class GetableGenericInterface {
public static void main(String[] args) {
Box<Toy> box = new Box<>();
box.set(new Toy());
Getable<Toy> gt = box;
System.out.println(gt.get());
}
}
// 실행 결과
I am toy
Box<T>
클래스는 Getable<T>
인터페이스를 구현하는 형태로 정의된다.
따라서 Getable 형 참조변수로 Box의 인스턴스를 참조할 수 있다.
단 T를 대신할 자료형이 다음 문장과 같이 동일해야 참조가 가능하다.
Box<Toy> box = new Box<>();
Getable<Toy> gt = box;
제네릭 인터페이스를 구현할 때에는 T를 결정한 상태로 구현할 수 있다.
이럴 때는 메소드를 구현할 때에도 T가 아닌 결정한 클래스 타입으로 명시하고 구현해야 한다.