제네릭 클래스도 상속이 가능하다.
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가 아닌 결정한 클래스 타입으로 명시하고 구현해야 한다.