제네릭 소스코드1
(1)
class Apple {
public String toString() {
return "I am an apple.";
}
}
class Orange {
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;
}
}
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 og = oBox.get();
System.out.println(ap);
System.out.println(og);
}
}
(2) 오브젝트를 넣은 형태(box에 오렌지, 사과 모두 저장이 가능한 형태)
class Apple {
public String toString() {
return "I am an apple.";
}
}
class Orange {
public String toString() {
return "I am an orange.";
}
}
class Box {
private Object ob;
public void set(Object o) {
ob = o;
}
public Object get() {
return ob;
}
}
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 og = (Orange)oBox.get();
System.out.println(ap);
System.out.println(og);
}
}
/*
Box 인스턴스에서 내용물을 꺼낼 때 형 변환을 해야 한다.
*/
(3)
class Apple {
public String toString() {
return "I am an apple.";
}
}
class Orange {
public String toString() {
return "I am an orange.";
}
}
class Box {
private Object ob;
public void set(Object o) {
ob = o;
}
public Object get() {
return ob;
}
}
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 og = (Orange)oBox.get();
System.out.println(ap);
System.out.println(og);
}
}
/*
위 예제의 다음 두 문장은 Apple 인스턴스와 Orange 인스턴스를 담으려 한 프로그래머의 의도와 달리 실수로 만들어진 문장이다.
aBox.set("Apple"); // 문자열 "Apple" 담았는데 실수다.
oBox.set("Orange"); // 문자열 "Orange" 담았는데 실수다.
그런데 문제는 이러한 실수가 컴파일 과정에서 발견되지 않았다는데 있다. 대신 다음 문장을 실행하는 순간 예외가 발생하기는 했다.
Apple ap = (Apple)aBox.get(); // 형 변환 좌정에서 예외 발생
모든 실수는 컴파일 단계에서 드러나는 것이 좋다. 컴파일 오류는 원인을 바로 찾을 수 있기 때문이다. 그러나 실행 중에 발생하는 예외는 다르다. 예외의 원인은 쉽게 발견되지 않는 경우도 많다. 뿐만 아니라 위와 같은 실수는 드러나지 않을 수도 있다.
*/
(4)
class Apple {
public String toString() {
return "I am an apple.";
}
}
class Orange {
public String toString() {
return "I am an orange.";
}
}
class Box {
private Object ob;
public void set(Object o) {
ob = o;
}
public Object get() {
return ob;
}
}
class FruitAndBoxFault2 {
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());
}
}
/*
위 예제는 흔히 하는 말로 '대형 사고'로 이어질 수 있다. 사고가 발생했는지 조차 모르고 넘어갈 수 있기 때문이다.
지금까지 제네릭 등장 이전의 자바 코드가 갖는 불편함과 문제점을 설명하였다. 불편함이라 하면 상자에서 물건을 꺼낼 때 형 변환을 해야 한다는 것이고, 문제점이라 하면 프로그래머가 실수를 해도 그 실수가 드러나지 않을 수 있다는 것이다.
*/
(5)
class Apple {
public String toString() {
return "I am an apple.";
}
}
class Orange {
public String toString() {
return "I am an orange.";
}
}
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class FruitAndBox2_Generic {
public static void main(String[] args) {
Box<Apple> aBox = new Box<Apple>();
Box<Orange> oBox = new Box<Orange>();
// 과일을 박스에 담는다.
aBox.set(new Apple());
oBox.set(new Orange());
// 박스에서 과일을 꺼내는데 형 변환 하지 않는다.
Apple ap = aBox.get();
Orange og = oBox.get();
System.out.println(ap);
System.out.println(og);
}
}
/*
용어 정리
* 타입 매개변수(Type Parameter) Box<T>에서 T
* 타입 인자(Type Argument) Box<Apple>에서 Apple
* 매개변수화 타입(Parameterized Type) Box<Apple>
그리고 '매개변수화 타입'은 '제네릭 타입(Generic Type)'이라고도 한다.
제네릭 이후의 코드는 제네릭 이전의 코드와 비교하여 다음 불편함과 문제점이 사라졌다.
* 필요시 형 변환을 해야 한다.
* 자료형과 관련된 프로그래머의 실수가 컴파일 과정에서 드러나지 않는다.
*/
(6)
class Apple {
public String toString() {
return "I am an apple.";
}
}
class Orange {
public String toString() {
return "I am an orange.";
}
}
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class FruitAndBoxFault_Generic {
public static void main(String[] args) {
Box<Apple> aBox = new Box<Apple>();
Box<Orange> oBox = new Box<Orange>();
// 과일을 박스에 담은 것일까?
aBox.set("Apple");
oBox.set("Orange");
// 박스에서 과일을 제대로 꺼낼 수 있을까?
Apple ap = aBox.get();
Orange og = oBox.get();
System.out.println(ap);
System.out.println(og);
}
}
/*
[실행결과]
c:\work\java>javac FruitAndBoxFault_Generic.java
FruitAndBoxFault_Generic.java:30: error: incompatible types: String cannot be converted to Apple
aBox.set("Apple");
^
FruitAndBoxFault_Generic.java:31: error: incompatible types: String cannot be converted to Orange
oBox.set("Orange");
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
2 errors
*/
(7)
class DBox<L, R> {
private L left; // 왼쪽 수납 공간
private R right; // 오른쪽 수납 공간
public void set(L o, R r) {
left = o;
right = r;
}
@Override
public String toString() {
return left + " & " +right;
}
}
class MultiTypeParam {
public static void main(String[] args) {
DBox<String, Integer> box = new DBox<String, Integer>();
box.set("Apple", 25);
System.out.println(box);
}
}
/*
타입 매개변수의 이름은 짓기 나름이다. 그러나 일반적으로 다음 두 가지 규칙을 지켜서 이름을 짓는다.
* 한 문자로 이름을 짓는다.
* 대문자로 이름을 짓는다.
이렇게 이름을 지으면 다른 종류의 이름들과 구분이 된다. 그리고 위 예제와 같이 한 글자로 이름을 짓더라도 가급적 의미를 두어 이름을 짓는 것이 좋다. 보편적으로 자주 사용하는 타입 매개변수의 이름과 그 의미는 다음과 같으니 이를 주로 사용하는 것도 괜찮은 선택이다.
E Element
K Key
N Number
T Type
V Value
제네릭 클래스에 대하여 Box<Apple>과 같이 '매개변수화 타입'을 구성할 때 기본 자료형의 이름은 '타입 인자'로 쓸 수 없다. 즉 다음과 같은 문장 구성은 불가능하다.
Box<int> box = new Box<int>();
-> 타입 인자로 기본 자료형이 올 수 없으므로 컴파일 오류 발생
하지만 기본 자료형에 대한 래퍼 클래스가 존재하고, 또 필요한 상황에서 박싱과 언박싱이 자동으로 이뤄진다.
*/
(8)
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class PrimitivesAndGeneric {
public static void main(String[] args) {
Box<Integer> iBox = new Box<Integer>();
iBox.set(125); // 오토 박싱 진행
int num = iBox.get(); // 오토 언박싱 진행
System.out.println(num);
}
}
/*
기본 자료형에 대한 제한 그리고 래퍼 클래스
제네릭 클래스에 대하여 Box<Apple>과 같이 '매개변수화 타입'을 구성할 때 기본 자료형의 이름은 '타입 인자'로 쓸 수 없다. 즉 다음과 같은 문장 구성은 불가능하다.
Box<int> box = new Box<int>();
-> 타입 인자로 기본 자료형이 올 수 없으므로 컴파일 오류 발생
하지만 기본 자료형에 대한 래퍼 클래스가 존재하고, 또 필요한 상황에서 박싱과 언박싱이 자동으로 이뤄지기 때문에 위와 같은 수준의 코드를 작성할 수 있다.
*/
(9)
// '매개변수화 타입'을 '타입 인자'로 전달하기
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class BoxInBox {
public static void main(String[] args) {
Box<String> sBox = new Box<>();
sBox.set("I am so happy.");
Box<Box<String>> wBox = new Box<>();
wBox.set(sBox);
Box<Box<Box<String>>> zBox = new Box<>();
zBox.set(wBox);
System.out.println(zBox.get().get().get());
}
}
/*
타입 인자의 생략 : 다이아몬드(Diamond) 기호
컴파일러는 프로그래머가 작성하는 제네릭 관련 문장에서 자료형의 이름을 추론하는 능력을 갖고 있다. 따라서 다음 문장을 대신하여,
Box<Apple> aBox = new Box<Apple>();
다음과 같이 쓸 수 있다.
Box<Apple> aBox = new Box<>();
이 경우 참조변수의 선언을 통해서 <> 안에 Apple이 생략되었다고 컴파일러는 판단한다. 쉽게 말해서 왼쪽을 보고 오른쪽의 빈 공간을 채운다. 그리고 <>을 가리켜 '다이아몬드(Diamond) 기호' 또는 '다이아몬드 표시'라 부른다. 이는 비공식적인 표현이긴 하지만 공식적인 표현으로 인식될 만큼 널리 사용되는 표현이다.
위 예제를 통해서 Box<String>과 같은 '매개변수화 타입'이 다음과 같이 '타입 인자'로 사용이 될 수 있음을 말하고자 하였다.
Box<Box<String>> wBox = new Box<>();
*/
(10)
class Box<T extends Number> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class BoundedBox {
public static void main(String[] args) {
Box<Integer> iBox = new Box<>(); // Integer는 Number를 상속
iBox.set(24);
Box<Double> dBox = new Box<>(); // Double은 Number를 상속
dBox.set(5.97);
System.out.println(iBox.get());
System.out.println(dBox.get());
}
}
/*
제네릭 클래스의 타입 인자 제한하기
앞서 정의한 Box<T>에는 무엇이든 담을 수 있다. String 인스턴스를 담고 싶으면 다음과 같이 상자를 생성하면 되고,
Box<String> sBox = new Box<>();
Apple 인스턴스를 담고 싶으면 다음과 같이 상자를 생성하면 된다.
Box<Apple> sBox = new Box<>();
그러나 상자에도 특성과 용도가 있다. 따라서 담고 싶은 것을 제한할 수 있어야 한다. (얇고 작은 상자에 수박을 넣을 수 없듯이) 그리고 이때 사용하는 것이 extends 이다. 예를 들어서 Number 클래스를 상속하는 클래스의 인스턴스만 담고 싶다면 다음과 같이 클래스를 정의하면 된다.
class Box<T extends Number> {...}
-> 인스턴스 생성 시 타입 인자로 Number 또는 이를 상속하는 클래스만 올 수 있음
위 예제에서는 제네릭 클래스의 타입 인자를 Number 또는 이를 상속하는 하위 클래스로 제한을 하였다. 그리고 이렇게 제한을 하면 또 다른 특성이 생긴다. Box<T> 클래스에는 다음과 같은 코드를 넣을 수 없다.
class Box<T>{
private T ob;
...
public int toIntValue() {
return ob.intValue(); // Error!
}
}
참조변수 ob가 참조하게 될 것은 인스턴스이다. 하지만 어떠한 클래스의 인스턴스를 참조하게 될지 알수 없기 때문에 ob를 통해서 호출할 수 있는 메소드는 Object 클래스의 메소드로 제한이 된다. 반면 다음과 같이 타입 이자를 제한하면 Number 클래스의 intValue 메소드를 호출할 수 있다. ob가 참조하는 인스턴스는 intValue 메소드를 가지고 있음을 100퍼센트 보장할 수 있기 때문이다.
class Box<T extends Number> {
private T ob;
...
public int toIntValue() {
return ob.intValue(); // OK!
}
}
이렇듯 타입 인자를 제한했을 때 얻게 되는 특성 때문에 타입 인자를 제한하는 경우도 많다.
*/
(11)
interface Eatable {
public String eat();
}
class Apple implements Eatable {
public String toString() {
return "I am an apple.";
}
@Override
public String eat() {
return "It tastes so good!";
}
}
class Box<T extends Eatable> {
private T ob;
public void set(T o) {
ob = o;
}
// 한 입 먹고 반환하는 행위의 메소드로 수정
public T get() {
System.out.println(ob.eat()); // Eatable로 제한하였기에 eat 호출 가능
return ob;
}
}
class BoundedInterfaceBox {
public static void main(String[] args) {
Box<Apple> box = new Box<>();
box.set(new Apple()); // 사과 저장
Apple ap = box.get(); // 사과 꺼내기
System.out.println(ap);
}
}
/*
제네릭 클래스의 타입 인자를 인터페이스로 제한하기
다음과 같이 타입 인자를 제한할 수 있음을 위에서 설명하였다.
class Box<T extends Number> {...}
이와 유사하게 인터페이스로도 타입 인자를 제한할 수 있다.
예제에서 보이듯이, 제네릭 클래스의 타입 인자를 다음과 같이 인터페이스의 이름으로 제한할 수 있다. 그리고 제한할 때에는 클래스와 마찬가지로 extends를 사용한다.
class Box<T extends Eatable> {...}
그리고 Eatable 인터페이스를 구현하는 클래스로 타입 인자를 제한했기 때문에 다음과 같이 인터페이스에 선언되어 있는 메소드 eat의 호출이 가능하게 되었다.
class Box<T extends Eatable> {
...
public T get(){
System.out.println(ob.eat()); // Eatable로 제한하였기에 eat 호출 가능
return ob;
}
}
그리고 타입 인자를 제한할 때에는 하나의 클래스와 하나 이상의 인터페이스에 대해 동시에 제한을 할수가 있으며 그 방법은 다음과 같다.
class Box<T extends Number & Eatable> {...}
이 경우 Number를 상속하면서 동시에 Eatable 인터페이스를 구현하는 클래스만이 타입 인자로 올 수 있다.
*/
(12)
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class BoxFactory {
public static <T> Box<T> makeBox(T o) { // 제네릭 메소드의 정의
Box<T> box = new Box<T>(); // 상자를 생성하고,
box.set(o); // 전달된 인스턴스를 상자에 담아서,
return box; // 이 상자를 반환한다.
}
}
class GenericMethodBoxMaker {
public static void main(String[] args) {
Box<String> sBox = BoxFactory.makeBox("Sweet");
System.out.println(sBox.get());
Box<Double> dBox = BoxFactory.makeBox(7.59);
System.out.println(dBox.get());
}
}
/*
제네릭 메소드의 정의
지금까지는 클래스를 제네릭으로 정의하였는데, 이렇듯 클래스 전부가 아닌 일부 메소드에 대해서만 제네릭으로 정의하는 것도 가능하며, 이렇게 정의된 메소드를 가리켜 '제네릭 메소드'라 한다. 제네릭 메소드는 인스턴스 메소드 뿐만 아니라 다음과 같이 클래스 메소드에 대해서도 정의가 가능하다. 쉽게 말해서 static 선언의 유무에 상관없이 제네릭 메소드의 정의가 가능하다.
public static Box<T> makeBox(T o) {...}
위의 메소드 정의에 대해서 다음 내용을 파악할 수 있어야 한다.
"메소드의 이름은 makeBox이고 반환형은 Box<T>이다."
그러나 위의 메소드 정의는 완전하지 않다. 이 상태에서 컴파일러는 T가 무엇이냐고 물어보며 컴파일 오류를 일으킨다. 따라서 T가 타입 매개변수의 선언임을 다음과 같이 표시해야 한다.
public static <T> Box<T> makeBox(T o) {...}
-> static과 Box<T> 사이에 위치한 <T>가 T 타입 매개변수임을 알리는 표시
이후로도 위와 같은 메소드 정의를 보면 Box<T>가 반환형임을, 그리고 그 앞에 위치한 <T>는 T가 타입 매개변수임을 알리는 표시임을 알 수 있어야 한다.
class BoxFactory{
public static <T> Box<T> makeBox(T o){
Box<T> box = new Box<T>(); // 상자를 생성하고,
box.set(o); // 전달된 인스턴스를 상자에 담아서,
return box; // 상자를 반환한다.
}
}
제네릭 클래스는 인스턴스 생성 시 자료형이 결정된다. 반면 제네릭 메소드는 '메소드 호출시에 자료형이 결정'된다. 따라서 위 클래스에 정의되어 있는 makeBox 제네릭 메소드는 다음과 같이 호출해야 한다.
Box<String> sBox = BoxFactory.<String>makeBox("Sweet");
Box<Double> dBox = BoxFactory.<Double>makeBox(7.59); // 7.59에 대해 오토 박싱 진행됨
위의 두 문장에서 메소드의 이름 앞에 표시한 <String>과 <Double>이 T에 대한 타입 인자이다. 즉 첫 번째 문장에서는 T를 String으로 결정하여 호출하였고, 두 번째 문장에서는 Double로 결정하여 호출하였다. 그런데 위의 두 문장을 다음 두 문장으로 대신할 수도 있다.
Box<String> sBox = BoxFactory.makeBox("Sweet");
Box<Double> dBox = BoxFactory.makeBox(7.59); // 7.59에 대해 오토 박싱 진행됨
위의 두 문장에서는 T에 대한 타입 인자 정보가 생략되었다. 그러나 컴파일러는 makeBox에 전달되는 인자를 보고 T를 각각 String과 Double로 유추한다. 그리고 이러한 자료형의 유추는 오토 박싱까지 감안하여 이뤄진다.
*/
(13)
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class Unboxer {
public static <T> T openBox(Box<T> box) {
return box.get();
}
}
class GenericMethodBoxMaker2 {
public static void main(String[] args) {
Box<String> box = new Box<>();
box.set("My Generic Method");
String str = Unboxer.<String>openBox(box);
System.out.println(str);
}
}
/*
위 예제에 정의된 제네릭 메소드는 다음과 같다. 인자로 전달된 상자에서 내용물을 꺼내 반환하는 메소드이다.
class Unboxer {
public static <T> T openBox(Box<T> box){
return box.get();
}
}
위의 메소드는 반환형이 T이고 전달인자의 자료형이 Box<T>인 경우이다. 그리고 이 메소드의 호출방법은 다음과 같다.
public static void main(String[] args){
Box<String> box = new Box<>();
box.set("My Generic Method");
String str = Unboxer.<String>openBox(box);
...
}
위의 메소드 호출에서는 T가 sTring이어야 하므로 타입 인자가 <String>으로 결정되었다. 물론 다음과 같이 이 정보를 생략할 수 있고 또 이것이 일반적이다.
String str = Unboxer.openBox(box);
*/
(14)
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class BoxFactory {
public static <T extends Number> Box<T> makeBox(T o) {
Box<T> box = new Box<T>();
box.set(o);
System.out.println("Boxed data: " + o.intValue());
return box;
}
}
class Unboxer {
public static <T extends Number> T openBox(Box<T> box) {
System.out.println("Unboxed data: " + box.get().intValue());
return box.get();
}
}
class BoundedGenericMethod {
public static void main(String[] args) {
Box<Integer> sBox = BoxFactory.makeBox(new Integer(5959));
int n = Unboxer.openBox(sBox);
System.out.println("Returned data: " + n);
}
}
/*
제네릭 메소드의 제한된 타입 매개변수 선언
앞서 제네릭 클래스를 정의할 때 다음과 같이 타입 인자를 제한할 수 있음을 설명하였다.
class Box<T extends Eatable> {...}
마찬가지로 제네릭 메소드도 호출 시 전달되는 타입 인자를 제한할 수 있다. 그리고 제네릭 클래스의 타입 인자를 제한할 때 생기는 특성이 제네릭 메소드의 타입 인자를 제한할 때에도 생긴다.
위 예제에서는 다음과 같이 제네릭 메소드에 전달되는 타입 인자를 제한하였다. Number를 상속하는 클래스로 타입 인자를 제한하였다.
// <T extends Number>는 타입 인자를 Number를 상속하는 클래스로 제한함을 의미
public static <T extends Number> Box<T> makeBox(T o){
...
// 타입 인자 제한으로 intValue 호출 가능
System.out.println("Boxed data: " + o.intValue());
return box;
}
// 타입 인자를 Number를 상속하는 클래스로 제한
public static <T extends Number> T openBox(Box<T> box) {
// 타입 인자 제한으로 intValue 호출 가능
System.out.println("Unboxed data: " + box.get().intValue());
return box.get();
}
*/
제네릭 문제1 (제네릭 클래스 정의하기)
문제1. 다음은 앞서 예제에서 작성한 수납공간이 둘로 나눠져 있는 상자를 표현한 제네릭 클래스이다.
class DBox<L, R>{
private L left;
private R right;
public void set(L o, R r) {
left = o;
right = r;
}
public String toString() { return left + " & " + right; }
}
이어서 수납공간이 둘로 나눠져 있는 상자를 표현한 클래스를 DDBox<U, D>라는 이름으로 하나 더 정의하여
DBox<L, R> 인스턴스 둘을 이 상자에 저장하고자 한다. 그럼 다음 main 메소드를 기반으로 컴파일 및 실행이 가능하도록 DDBox<U, D> 제네릭 클래스를 정의해보자.
public static void main(String[] args){
DBox<String, Integer> box1 = new DBox<>();
box1.set("Apple", 25);
DBox<String, Integer> box2 = new DBox<>();
box2.set("Orange", 33);
DDBox<DBox<String, Integer>, DBox<String, Integer>> ddbox = new DDBox<>();
ddbox.set(box1, box2); // 두 개의 상자를 하나의 상자에 담음
System.out.println(ddbox); // 상자의 내용물 출력
그리고 위 main 메소드의 실행 결과로 다음의 출력을 보이게 하자.
(출력 형태는 이와 달라도 괜찮다. 내용물만 전부 출력이 되면 된다.)
c:\JavaStudy>java DDBoxDemo
Apple & 25
Orange & 33
▶▶▶
class DBox<L, R> {
private L left; // 왼쪽 수납 공간
private R right; // 오른쪽 수납 공간
public void set(L o, R r) {
left = o;
right = r;
}
@Override
public String toString() {
return left + " & " +right;
}
}
class DDBox<U, D> {
private U up;
private D down;
public void set(U u, D d) {
up = u;
down = d;
}
@Override
public String toString() {
return up.toString() + "\n" + down.toString();
}
}
class DDBoxDemo {
public static void main(String[] args) {
DBox<String, Integer> box1 = new DBox<>();
box1.set("Apple", 25);
DBox<String, Integer> box2 = new DBox<>();
box2.set("Orange", 33);
DDBox<DBox<String, Integer>, DBox<String, Integer>> ddbox = new DDBox<>();
ddbox.set(box1, box2);
System.out.println(ddbox);
}
}
문제2. 문제1의 내용에 해당하는 프로그램은 사실 별도의 클래스를 정의하지 않고 DBox 하나로 충분히 편성할 수 있다.
따라서 이번에는 문제 1의 내용과 결과를 보이는 프로그램을 작성하되 DBox 클래스 하나만 활용하여 작성해보자.
(상자에 담긴 내용물의 출력 형태는 달라도 괜찮다. 내용물만 전부 출력이 되면 된다.)
class DBox<L, R> {
private L left; // 왼쪽 수납 공간
private R right; // 오른쪽 수납 공간
public void set(L o, R r) {
left = o;
right = r;
}
@Override
public String toString() {
return left + " & " +right;
}
}
class DDBoxDemo {
public static void main(String[] args) {
DBox<String, Integer> box1 = new DBox<>();
box1.set("Apple", 25);
DBox<String, Integer> box2 = new DBox<>();
box2.set("Orange", 33);
DBox<DBox<String, Integer>, DBox<String, Integer>> ddbox = new DBox<>();
ddbox.set(box1, box2);
System.out.println(ddbox);
}
}
제네릭 문제2 (제네릭 메소드의 정의와 전달인자의 제한)
다음 코드가 실행되도록 swapBox 메소드를 정의하되, Box<T> 인스턴스를 인자로 전달받을 수 있도록 정의하자.
단 이때 Box<T> 인스턴스의 T는 Number 또는 이를 상속하는 하위 클래스만 올 수 있도록 제한된 매개변수 선언을 하자.
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class BoxSwapDemo {
// 이 위치에 swapBox 메소드 정의하자.
public static void main(String[] args) {
Box<Integer> box1 = new Box<>();
box1.set(99);
Box<Integer> box2 = new Box<>();
box2.set(55);
System.out.println(box1.get() + " & " + box2.get());
swapBox(box1, box2); // 정의해야 할 swapBox 메소드
System.out.println(box1.get() + " & " + box2.get());
}
}
그리고 실행 결과는 다음과 같아야 한다. 즉 swapBox 메소드의 호출 결과로 인자로 전달된 두 상자 안에 저장된 내용물이 서로 바뀌어야 한다.
c:\JavaStudy>java BoxSwapDemo
99 & 55
55 & 99
▶▶▶
class Box<T> {
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class BoxSwapDemo {
public static <T extends Number> void swapBox(Box<T> box1, Box<T> box2) {
T temp = box1.get();
box1.set(box2.get());
box2.set(temp);
}
public static void main(String[] args) {
Box<Integer> box1 = new Box<>();
box1.set(99);
Box<Integer> box2 = new Box<>();
box2.set(55);
System.out.println(box1.get() + " & " + box2.get());
swapBox(box1, box2);
System.out.println(box1.get() + " & " + box2.get());
}
}
제네릭 소스코드2
(1)
class Box<T> {
protected T ob;
public void set(T o) { ob = o; }
public T get() { return ob; }
}
class SteelBox<T> extends Box<T> {
public SteelBox(T o) { // 제네릭 클래스의 생성자
ob = o;
}
}
class GenericInheritance {
public static void main(String[] args) {
Box<Integer> iBox = new SteelBox<>(7959);
Box<String> sBox = new SteelBox<>("Simple");
System.out.println(iBox.get());
System.out.println(sBox.get());
}
}
제네릭 클래스도 상속이 가능하다.
제네릭 클래스의 상속을 설명하기 위해서, 예제에서는 Box<T>를 상속하는 하위 클래스를 다음과 같이 간단히 정의하였다.
class SteelBox<T> extends Box<T> {
public SteelBox(T o) { // 생성자
ob= o;
}
}
그리고 이로 인하여 다음과 같이 Box<T>의 참조변수로 SteelBox<T> 인스턴스를 참조하는 문장을 구성할 수 있게 되었다.
Box<Integer> iBox = new SteelBox<>(7759); <-> Box<Integer> iBox = new SteelBox<Integer>(7759);
Box<String> sBox = new SteelBox<>("Simple"); <-> Box<String> sBox = new SteelBox<String>("Simple");
즉, 두 제네릭 클래스가 다음의 상속 관계를 구성하면,
Box<T>
↑
SteelBox<T>
[제네릭 클래스의 상속]
다음 관계도 성립한다. 때문에 예제에서 SteelBox<Integer> 인스턴스를 Box<Integer>형 참조변수로 참조할 수 있었다.
Box<Integer> Box<String>
↑ ↑
SteelBox<Integer> SteelBox<String>
[제네릭 클래스의 상속으로 인해 형성되는 관계]
앞에서 Box<Integer>와 같은 것을 '매개변수화 타입' 또는 '제네릭 타입'이라 함을 설명하였는데, 이렇듯 '타입(Type)'이라는 단어가 포함된 것은 Box<Integer>를 일종의 자료형으로 정확히는 클래스의 이름으로 간주함을 뜻한다. 따라서 위와 같은 상속의 관계가 형성될 수 있고 이를 다음과 같이 표현할 수 있다.
"SteelBox<Integer> 클래스는 Box<Integer> 클래스를 상속한다."
물론 다음과 같이 표현하는 것이 보편적이긴 하다.
"SteelBox<Integer> 제네릭 타입은 Box<Integer> 지네릭 타입을 상속한다."
그렇다면 다음 문장도 컴파일이 가능할까? Number를 Integer가 상속하니 컴파일이 되지 않을까?
Box<Number> box = new Box<Integer>();
Number를 Integer가 상속하지만 Box<Number>와 Box<Integer>는 상속 관계를 형성하지 않는다. 따라서 컴파일 되지 않는다. Box<Number>와 Box<Integer>이 상속 관계를 형성하지 않는 것은 언어를 디자인 한 설계자의 결정이므로 이해보다는 인식이 우선인 부분이다. 그러나 조금만 생각해 보면 이러한 결정이 합리적임을 알 수 있다.
예를 들어서 SteelBox<Integer>와 Box<Integer>가 상속 관계를 형성하는데, 여기에 더해 Box<Integer>와 Box<Number>가 상속 관계를 형성한다면? 매우 혼란스러운 상속의 구조가 만들어진다. 그에 따른 이점은 별로 보이지 않는데 말이다.
(2)
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<T>(); // 상자 생성
return box; // 생성한 상자 반환
}
}
class TargetTypes {
public static void main(String[] args) {
Box<Integer> iBox = EmptyBoxFactory.<Integer>makeBox();
iBox.set(25);
System.out.println(iBox.get());
}
}
위의 예제에서는 다음과 같이 상자를 생성해서 반환하는 '제네릭 메소드'를 정의하였다.
public static <T> Box<T> makeBox() {
Box<T> box = new Box<T>();
return box;
}
그런데 이전에 구현했던 BoxFactory 클래스의 makeBox 메소드와 달리 인자를 전달받지 않는다. 당시에는 인자를 전달받았기 때문에 컴파일러가 이 인자를 통해서 T를 유추할 수 있었다. 그러나 위의 메소드는 인자를 전달받지 않으므로 다음과 같이 T에 대한 타입 인자를 전달해야 한다.
Box<Integer> iBox = EmptyBoxFactory.<Integer>makeBox();
그런데 자바 7부터 다음과 같이 호출하는 것이 가능하게 되었다. 자바 7부터 컴파일러의 자료형 유추범위가가 넓어졌기 때문이다.
Box<Integer> iBox = EmptyBoxFactory.makeBox(); // 자바 7부터 컴파일 되는 문장
어떻게 가능한 것일까?
우리는 위의 문장을 보면서 makeBox 메소드는 Box<Integer> 인스턴스의 참조 값을 반환해야 한다고 판단할 수 있다. 왼편에 선언된 매개변수의 형을 보고 이러한 판단을 할 수 있다. 따라서 makeBox 메소드 호출 시 T는 Integer가 되어야 함을 알 수 있다. 그런데 이러한 판단을 자바 7부터 컴파일러도 할 수 있게 되었다.
지금 설명한 상황에서 T의 유추에 사용된 정보 Box<Integer>를 가리켜 '타겟 타입'이라 한다. 그리고 이러한 유추는 당연한 듯 보이지만, 대입 연산자의 왼편에 있는 정보를 가지고 컴파일러가 이러한 유추를 진행한다는 것은 주목할 만한 일이다.
(3)
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 <T> void peekBox(Box<T> box) {
System.out.println(box);
}
}
class WildcardUnboxer {
public static void main(String[] args) {
Box<String> box = new Box<>();
box.set("So Simple String");
Unboxer.peekBox(box); // 상자 안의 내용물을 확인해본다.
}
}
위 예제에서 다음 제네릭 메소드를 추가하였다.
public static <T> void peekBox(Box<T> box) {
System.out.println(box);
}
그런데 이 메소드를 제네릭으로 정의한 이유가 Box<Integer>, Box<String>의 인스턴스를
인자로 전달받도록 하기 위함이니 다음과 같이 정의해도 되지 않겠는가?
public static <T> void peekBox(Box<Object> box) {
System.out.println(box);
}
안된다! 이유는?
"Box<Object>와 Box<String>은 상속 관계를 형성하지 않는다."
"Box<Object>와 Box<Integer>은 상속 관계를 형성하지 않는다."
즉 Object와 String이 상속 관계에 있더라도 Box<Object>와 Box<String>은 상속 관계를 형성하지 않는 별개의 자료형이다. 대신 '와일드카드'라는 것을 사용하면 원하는 바를 이룰 수 있다.
(4)
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) { // 와일드카드 사용
System.out.println(box);
}
}
class WildcardUnboxer2 {
public static void main(String[] args) {
Box<String> box = new Box<>();
box.set("So Simple String");
Unboxer.peekBox(box);
}
}
위 예제에서 보이듯이 물음표 기호로 표시되는 와일드카드를 이용해서 메소드의 매개변수를 다음과 같이 선언하면
public static void peekBox(Box<?> box) {
System.out.println(box);
}
Box<T>를 기반으로 생성된, Box<Integer> 인스턴스나 Box<String> 인스턴스들을 인자로 받을 수 있다. 그렇다면 다음 두 메소드에는 어떠한 차이가 있을까? 위에서 제시한 두 예제에서 보인 결과를 보면 아무런 차이가 없다.
public static <T> void peekBox(Box<T> box){
System.out.println(box);
} // 제네릭 메소드의 정의
public static void peekBox(Box<?> box) {
System.out.println(box);
} // 와일드 카드 기반 메소드 정의
사실 기능적인 측면에서 보면 위의 두 메소드는 완전히 동일하다. 즉 제네릭 메소드와 와일드카드 기반 메소드는 상호 대체 가능한 측면이 있다. 그러나 코드가 조금 더 간결하다는 이유로 와일드카드 기반 메소드의 정의를 선호한다.
참고 - 와일드 카드 기반 메소드 정의를 보다 간결하다고 한 이유는?
앞서 제시한 두 메소드를 보면 제네릭 메소드 정의에는 다음과 같이 <T>가 두 번 등장한다.
public static <T> void peekBox(Box<T> box)
반면 와일드 카드 기반 메소드 정의에는 <?>가 매개변수 선언에서 한 번만 등장한다.
public static void peekBox(Box<?> box)
지금은 이 차이가 별것 아닌 것 같지만 <T> 또는 <?>에 추가적인 선언이 들어가면 이러한 차이는 더 커진다. 그리고 개인적인 취향과 상관 없이 이러한 보편적인 선호도를 따라서 코드를 작성하는 것도 중요하다.
(5)
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 void peekBox(Box<? extends Number> box) {
System.out.println(box);
}
}
class UpperBoundedWildcard {
public static void main(String[] args) {
Box<Integer> iBox = new Box<>();
iBox.set(1234);
Box<Double> dBox = new Box<>();
dBox.set(10.009);
Unboxer.peekBox(iBox);
Unboxer.peekBox(dBox);
}
}
public static void peekBox(Box<?> box){
System.out.println(box);
}
위 메소드의 인자로, Box<T>에서 T가 Number 또는 Number의 하위 클래스인 제네릭 타입의 인스턴스만 전달되도록
제한할 때 다음과 같이 '상한 제한된 와일드카드(Upper-Bounded Wildcards)'라는 것을 사용한다.
Box<? extends Number> box
-> box는 Box<T> 인스턴스를 참조하는 참조변수이다.
-> 단 이때 Box<T> 인스턴스의 T는 Number 또는 이를 상속하는 하위 클래스이어야 함
따라서 메소드 peekBox의 매개변수에 다음과 같이 제한을 걸어서 Box<Integer>, Box<Double>과 같은 제네릭 타입의
인스턴스만 인자로 전달되도록 할 수 있다.
public static void peekBox(Box<? extends Number> box) {
System.out.println(box);
}
(6)
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 void peekBox(Box<? super Integer> box) {
System.out.println(box);
}
}
class LowerBoundedWildcard {
public static void main(String[] args) {
Box<Integer> iBox = new Box<Integer>();
iBox.set(5577);
Box<Number> nBox = new Box<Number>();
nBox.set(new Integer(9955));
Box<Object> oBox = new Box<Object>();
oBox.set("My Simple Instance");
Unboxer.peekBox(iBox);
Unboxer.peekBox(nBox);
Unboxer.peekBox(oBox);
}
}
그리고 다음과 같이 참조변수에 '하한 제한된 와일드카드(Lower-Bounded Wildcards)'선언을 할수도 있다.
Box<? super Integer> box
-> box는 Box<T> 인스턴스를 참조하는 참조변수이다.
-> 단 이때 Box<T> 인스턴스의 T는 Integer 또는 Integer가 상속하는 클래스이어야 함
예를 들어서 메소드의 매개변수를 다음과 같이 선언하면
public static void peekBox(Box<? super Integer> box){
System.out.println(box);
}
위 메소드의 인자로 전달될 수 있는 인스턴스의 타입 종류는 다음과 같이 제한된다.
Box<Integer>, Box<Number>, Box<Object>
(7)
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); // 박스에 넣기
}
}
class BoundedWildcardBase {
public static void main(String[] args) {
Box<Toy> box = new Box<>();
BoxHandler.inBox(box, new Toy());
BoxHandler.outBox(box);
}
}
언제 와일드 카드에 제한을 걸어야 하는가?
다음 메소드의 매개변수 선언에 대해서 설명하라고 하면,
public static void peekBox(Box<? extends Number> box) {...}
인자로 전달할 수 있는 인스턴스의 형과 관련하여 다음 내용으로 설명하고 마무리하는 경우가 대부분이다.
"Box<T>의 T를 Number 또는 Number를 직간접적으로 상속하는 클래스로 제한하기 위한 것"
물론 정확한 설명이다. 그리고 인자로 전달되는 대상을 제한하는 것은 그 자체로 프로그램에 안정성을 높여 의미가 있다.
그러나 다른 관점에서 '상한 제한된 와일드카드'의 의미르를 설명할 수 있어야 한다. 마찬가지로 다음 메소드의 매개변수 선언에 대해서 설명하라고 하면
public static void peekBox(Box<? super Integer> box) {...}
인자로 전달할 수 있는 인스턴스의 형과 관련하여 다음 내용으로 설명하고 마무리하는 경우가 대부분이다.
"Box<T>의 T를 Integer 또는 Integer가 직간접적으로 상속하는 클래스로 제한하기 위한 것"
그러나 이 경우에도 다른 관점에서 하한 제한된 와일드카드의 의미를 설명할 수 있어야 한다. 그렇지 않으면 자바에서 제공하는 다음과 같은 메소드의 사용은 부담스러울 수 밖에 없다.
public static <T> void copy(List<? super T> dest, List<? extends T> src)
-> Collections 클래스의 복사 메소드
위 예제에서 관심을 두어야 할 부분은 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는 상자에서 물건을 꺼낼 때 사용하는 메소드이다. 반면 inBox는 상자에 물건을 넣을 때 사용하는 메소드이다. 둘 다 잘 정의되었고 잘 동작한다. 그러나 잘 만들어진 코드는 다음의 조건을 추가로 만족해야 하는데, 위 의 두 메소드는 이 조건까지 만족하는 형태로 정의되지 않았다.
"필요한 만큼만 기능을 허용하여, 코드의 오류가 컴파일 과정에서 최대한 발견되도록 한다."
먼저 다음 메소드를 보자.
public static void outBox(Box<Toy> box) {...}
-> 매개변수 box가 참조하는 상자에서 인스턴스를 꺼내는 기능
이 메소드를 정의할 당시 프로그래머의 생각은 다음과 같다.
"상자에서 내용물을 꺼내는 기능의 메소드를 정의하자."
그런데 매개변수 box를 대상으로는 다음과 같이 get은 물론 set의 호출도 가능하다.
public static void outBox(Box<Toy> box) {
Toy t = box.get(); // 꺼내는 것! OK!
box.set(new Toy()); // 넣는 것! 이것도 OK!
}
따라서 다음과 같은 유형의 오류를 범할 수 있는 상황이다.
"outBox 메소드 내에서 실수로 set 메소드를 호출하여 임의의 인스턴스를 넣었다."
이러한 다음과 같은 유형의 오류를 범할 수 있는 상황이다.
"outBox 메소드 내에서 실수로 set 메소드를 호출하여 임의의 인스턴스를 넣었다."
이러한 실수는 누구나 할 수 있다. 그러나 이러한 오류는 컴파일 과정에서 발견되지 않는다.
때문에 outBox 메소드를 정의할 때에는 매개변수 box를 대상으로 get은 가능하지만 set은 불가능하도록 제한을 거는 것이 좋다. 그리고 이러한 일이 '필요한 만큼만 기능을 허용하여, 코드의 오류가 컴파일 과정에서 최대한 발견되도록 하는 일'이다.
그렇다면 어떻게 outBox 메소드를 정의해야 할까? 다음과 같이 정의하면 된다. 다음과 같이 매개변수 선언을 하면 상자에서 꺼내는 것은 가능하지만 넣는 것은 불가능하게 된다. 넣으려고 하면 컴파일 오류가 발생한다.
public static void outBox(Box<? extends Toy> box) {
Toy t = box.get(); // 꺼내는 것! OK!
box.set(new Toy()); // 넣는 것! ERROR!
}
위의 상황에서 SET 메소드의 호출이 불가능한 이유는 무엇일까? 바로 결론을 말하면, 위 메소드의 매개변수로 Toy 인스턴스를 저장할 수 있는 상자만(Box<T> 인스턴스만) 전달된다는 사실을 보장할 수 없기 때문이다. 이에 대해 보충 설명을 하면, Toy 클래스는 다음과 같이 다른 클래스들에 의해 얼마든지 상속이 될 수 있다.
class Car extends Toy {...} // 자동차 장난감
class Robot extends Toy {...} // 로봇 장난감
그리고 이렇게 상속 관계를 맺으면 위의 outBox 메소드에 Box<Car> 또는 Box<Robot>인스턴스가 인자로 저달될 수 있다. 이러한 상황에서 다음과 같이 Toy 인스턴스를 상자에 담을 수 있겠는가?
public static void outBox(Box<? extends Toy> box) {
// box로 Box<Car> 또는 Box<Robot> 인스턴스가 전달된다면?
box.set(new Toy()); // 넣는 것! ERROR!
}
바로 이러한 문제점 때문에 다음과 같이 선언된 매개변수를 대상으로 저장하는(전달하는) 메소드의 호출이 불가능하다.
Box<? extends Toy> box
지금까지 설명한 내용을 정리하면, 다음과 같은 매개변수 선언을 보았을 때
public static void outBox(Box<? extends Toy> box){
// 이 안에서는 box가 참조하는 인스턴스에
// Toy 인스턴스를 저장하는(전달하는) 메소드 호출은 불가능하다.
}
다음과 같은 판단을 할 수 있어야 한다.
"box가 참조하는 인스턴스를 대상을 저장하는 기능의 메소드 호출은 불가능하다."
(8)
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) {
Toy t = box.get(); // 박스에서 꺼내기
System.out.println(t);
}
public static void inBox(Box<Toy> box, Toy n) {
box.set(n); // 박스에 넣기
}
}
class BoundedWildcardUsage {
public static void main(String[] args) {
Box<Toy> box = new Box<>();
BoxHandler.inBox(box, new Toy());
BoxHandler.outBox(box);
}
}
언제 와일드카드에 제한을 걸어야 하는가? : 하한 제한의 목적
이번에는 다음 클래스의 두 번째 메소드에 주목하자.
class BoxHandler{
...
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(); // 꺼내는 것! 이것도 OK!
}
그러나 이러한 실수는 컴파일 과정에서 발견되지 않는다. 따라서 이러한 실수가 컴파일 과정에서 발견될 수 있도록 매개변수를 다음과 같이 선언해야 한다.
public static void inBox(Box<Toy> box, Toy n){
box.set(n); // 넣는 것! OK!
Toy myToy = box.get(); // 꺼내는 것! Error!
}
위와 같이 매개변수를 선언하면 get 메소드의 호출문에서 컴파일 오류가 발생한다. 이유는 반환형을 Toy로 결정할 수 없기 때문이다. 즉 get 메소드 호출 자체는 문제 되지 않으나, 반환되는 값을 저장하기 위해 선언한 참조변수의 형을 Toy로 결정했다는 사실에서 문제가 발생한다. 이와 관련하여 보충 설명을 하기 위해 Toy 클래스의 상속 관계가 다음과 같다고 가정하자.
class Plastic {...}
class Toy extends Plastic {...}
그러면 inBox 메소드의 첫 번째 인자로 전달 가능한 두 가지 유형의 Box<T> 인스턴스는 다음과 같다.
Box<Toy> tBox = new Box<Toy>();
Box<Plastic> pBox = new Box<Plastic>();
그리고 위의 inBox 메소드에 tBox가 전달되면 메소드 내에서 다음 문장을 실행하는데 문제가 없지만,
Toy myToy = box.get(); // get이 반환하는 것이 Toy 인스턴스이므로 문제가 없지만,
pBox가 전달되면, 메소드 내에서 다음 문장을 실행하는데 있어서 문제가 된다. 그래서 컴파일러는 이 문장 자체를 허용하지 않는다.
Toy myToy = box.get(); // get이 반환하는 것이 Plastic 인스턴스이므로 문제가 된다.
자!그럼 지금 설명한 내용을 이렇게 정리하자. 다음과 같은 매개변수 선언을 보았을 때
public static void outBox(Box<? super Toy> box)
{
// 이 안에서는 box가 참조하는 인스턴스에서
// Toy 인스턴스를 꺼내는(반환하는) 메소드 호출은 불가능하다.
}
다음과 같은 판단을 할 수 있어야 한다.
"box가 참조하는 인스턴스를 대상으로 꺼내는 기능의 메소드 호출은 불가능하다."
실제 문제를 일으키는 부분은 메소드 호출 자체가 아닌, 매개변수의 반환형 선언이지만 이렇게 정리해 두는 것이 여러모로 도움이 된다.
참고 - 참조변수를 Object형으로 선언한다면? 앞서 설명한 내용과 관련하여 다음과 같이 참조변수 myToy를 Object형으로 선언하면 컴파일이 되지 않으냐고 질문할 수 있다.
public static void inBox(Box<? super Toy> box, Toy n)
{
Object myToy = box.get();
}
위의 상황에서 get 메소드의 반환형을 결정할 수 없기 때문에 컴파일러는 get의 반환형을 Object로 결정해버린다.
그래서 위의 메소드 정의는 컴파일 된다.
그러나 자바는 Object형 참조변수의 선언이나 Object형으로의 형 변환이 불필요하도록 문법을 개선시켜왔다. Object라는 이름의 코드에 직접 등장하는 것은 컴파일러를 통한 오류의 발견 가능성을 낮추는 행위이기 때문이다.
그러니 지금 설명하는 부분에서 참조변수를 Object 형으로 선언하는 것은 논외로 해야 한다. 동시에 당연히 피해야 할 일이기도 하다.
'Java > Day28' 카테고리의 다른 글
[Java] Generic 제네릭 Step (0) | 2021.12.13 |
---|---|
[Java] Generic 제네릭 개념 (0) | 2021.12.12 |