Generic 제네릭 Step1
문제1.
Orange 클래스를 만들고 인스턴스 변수로는 당도를 둔다.(sugarContent) 당도를 초기화 하는 생성자를 만든다. 그리고 당도를 출력하는 메소드를 둔다.(showSugarContent). Orange 하나를 저장할 수 있는 OrangeBox를 만든다.(배열이 아닌 하나를 저장할 수 있게끔 한다.) Orange를 저장하고 꺼내는 메소드를 만든다.(store, pullOut) 메인메소드에서 Orange의 당도가 10인 Orange를 OrangeBox에 저장한다. 저장되어 있는 Orange를 꺼내서 당도를 확인한다.
Apple 클래스를 만들고 인스턴스 변수로는 무게를 둔다.(weight) 무게를 초기화 하는 생성자를 만든다. 그리고 무게를 출력하는 메소드를 둔다.(showAppleWeight) Apple 하나를 저장할 수 있는 AppleBox를 만든다.(배열이 아닌 하나를 저장할 수 있게끔 한다.) Apple를 저장하고 꺼내는 메소드를 만든다.(store, pullOut) 메인메소드에서 Apple의 무게가 200인 Apple을 AppleBox에 저장한다. 저장되어 있는 Apple를 꺼내서 무게를 확인한다.
class Orange
{
int sugarContent; // 당분 함량
public Orange(int sugar) { sugarContent=sugar; }
public void showSugarContent()
{
System.out.println("당도 "+sugarContent);
}
}
class Apple
{
int weight; // 사과의 무게
public Apple(int weight) { this.weight=weight; }
public void showAppleWeight()
{
System.out.println("무게 "+weight);
}
}
class OrangeBox
{
Orange item;
public void store(Orange item) { this.item=item; }
public Orange pullOut() { return item; }
}
class AppleBox
{
Apple item;
public void store(Apple item) { this.item=item; }
public Apple pullOut() { return item; }
}
class OrangeAppleMain
{
public static void main(String[] args)
{
OrangeBox oBox=new OrangeBox();
oBox.store(new Orange(10));
Orange org1=oBox.pullOut();
org1.showSugarContent();
AppleBox aBox=new AppleBox();
aBox.store(new Apple(200));
Apple app=aBox.pullOut();
app.showAppleWeight();
}
}
문제2.
문제1의 소스코드를 OrangeBox와 AppleBox를 어떤 과일도 받을 수 있는 FruitBox로 바꾸어 본다. 즉 두개의 클래스를 하나로 합친다. (제네릭 클래스를 사용하지 않는다.) 메인메소드에서 Orange의 당도가 10인 Orange를 FruitBox에 저장한다. 저장되어 있는 Orange를 꺼내서 당도를 확인한다. 메인메소드에서 Apple의 무게가 200인 Apple을 FruitBox에 저장한다. 저장되어 있는 Apple를 꺼내서 무게를 확인한다.
class Orange
{
int sugarContent; // 당분 함량
public Orange(int sugar) { sugarContent=sugar; }
public void showSugarContent()
{
System.out.println("당도 "+sugarContent);
}
}
class Apple
{
int weight; // 사과의 무게
public Apple(int weight) { this.weight=weight; }
public void showAppleWeight()
{
System.out.println("무게 "+weight);
}
}
class FruitBox
{
Object item;
public void store(Object item) { this.item=item; }
public Object pullOut() { return item; }
}
class FruitBoxMain
{
public static void main(String[] args)
{
FruitBox fBox1=new FruitBox();
fBox1.store(new Orange(10));
Orange org1=(Orange)fBox1.pullOut();
org1.showSugarContent();
FruitBox fBox2=new FruitBox();
fBox2.store(new Apple(200));
Apple app=(Apple)fBox2.pullOut();
app.showAppleWeight();
}
}
문제3.
문제1과 문제2를 이상없이 풀었다는 과정하에 문제2에 있는 FruitBox에 문자열 "오렌지"를 저장하도록하고 컴파일 해보자.(새로운 소스코드를 작성해서 직접 실행해 본다.) 어떤 현상이 벌어지는가? 예를 들어 소스파일 이름이 FruitBoxMain.java라고 하면 javac FruitBoxMain.java 라고 컴파일을 실행하면 컴파일 시에는 오류가 발생하지 않는다. 하지만 java FruitBoxMain 하고서 프로그램을 실행시키게 되면 ClassCastException이 발생한다.
class Orange
{
int sugarContent; // 당분 함량
public Orange(int sugar) { sugarContent=sugar; }
public void showSugarContent()
{
System.out.println("당도 "+sugarContent);
}
}
class Apple
{
int weight; // 사과의 무게
public Apple(int weight) { this.weight=weight; }
public void showAppleWeight()
{
System.out.println("무게 "+weight);
}
}
class FruitBox
{
Object item;
public void store(Object item) { this.item=item; }
public Object pullOut() { return item; }
}
class FruitBoxMain
{
public static void main(String[] args)
{
FruitBox fBox1=new FruitBox();
fBox1.store("오렌지");
Orange org1=(Orange)fBox1.pullOut();
org1.showSugarContent();
FruitBox fBox2=new FruitBox();
fBox2.store(new Apple(200));
Apple app=(Apple)fBox2.pullOut();
app.showAppleWeight();
}
}
이렇듯 컴파일 과정에서 발견되는 오류는 매우 쉽게 해결이 가능하다. 반면 위에서 만든 예제와 같이 실행과정에서 발생하는 오류는 찾기가 쉽지 않다. 언뜻 보기에는 별 차이가 없어 보이지만, 프로그램의 규모가 크면 클수록 이 둘의 차이는 매우 극명하게 드러난다.
* 실행과정에서 발견되는 오류를 컴파일 과정에서 발견되도록 코드를 작성하는 것은 매우 의미 있는 일이다.
* 자료형에 대한 안전성이 보장된다.
문제4.
문제1과 문제2를 이상없이 풀었다는 과정하에 문제1의 OrangeBox에 문자열 "오렌지"를 저장하도록하고 컴파일해보자. (새로운 소스코드를 작성해서 직접 실행해 본다.) 어떤 현상이 벌어지는가? 예를 들어 소스파일 이름이 OrangeAppleMain.java라고 하면, javac OrangeAppleMain.java를 컴파일하면 컴파일 과정에서 오류가 난다. 자료형에 대한 안전성이 보장이 되지만 과일의 종류별로 상자 클래스를 만들어 주어야 하므로 소스코드의 양이 늘어나게 되고 유지보수가 어려워 진다.
문제5.
문제2를 제네릭 클래스로 바꾼다.
class Orange
{
int sugarContent; // 당분 함량
public Orange(int sugar) { sugarContent=sugar; }
public void showSugarContent()
{
System.out.println("당도 "+sugarContent);
}
}
class Apple
{
int weight; // 사과의 무게
public Apple(int weight) { this.weight=weight; }
public void showAppleWeight()
{
System.out.println("무게 "+weight);
}
}
class FruitBox<T>
{
T item;
public void store(T item) { this.item=item; }
public T pullOut() { return item; }
}
class FruitBoxMain
{
public static void main(String[] args)
{
FruitBox<Orange> fBox1=new FruitBox<Orange>();
fBox1.store(new Orange(10));
Orange org1=fBox1.pullOut();
org1.showSugarContent();
FruitBox<Apple> fBox2=new FruitBox<Apple>();
fBox2.store(new Apple(200));
Apple app=fBox2.pullOut();
app.showAppleWeight();
}
}
문제6.
문제5를 이상없이 풀었다는 가정하에 FruitBox에 문자열 "오렌지"를 저장하도록하고 컴파일해보자. (새로운 소스코드를 작성해서 직접 실행해 본다.) 어떤 현상이 벌어지는가? 예를 들어 소스파일 이름이 FruitBoxMain.java라고 하면, javac FruitBoxMain.java를 컴파일하면 컴파일 과정에서 오류가 난다. 자료형에 안정성도 보장이 되면서 어떤 과일도 저장할 수 있는 상자가 만들어 졌다.
문제7.
예제 GenericBaseFruitBox.java의 FruitBox<T> 클래스에 생성자를 추가하여, 다음의 main 메소드가 컴파일 및 실행됨을 확인해보자.
class Orange
{
int sugarContent; // 당분 함량
public Orange(int sugar) { sugarContent=sugar; }
public void showSugarContent()
{
System.out.println("당도 "+sugarContent);
}
}
class Apple
{
int weight; // 사과의 무게
public Apple(int weight) { this.weight=weight; }
public void showAppleWeight()
{
System.out.println("무게 "+weight);
}
}
class FruitBox<T>
{
T item;
FruitBox(T item)
{
this.item = item;
}
public void store(T item) { this.item=item; }
public T pullOut() { return item; }
}
class GenericBaseFruitBox
{
public static void main(String[] args)
{
FruitBox<Orange> orBox=new FruitBox<Orange>(new Orange(10));
Orange org=orBox.pullOut();
org.showSugarContent();
FruitBox<Apple> apBox=new FruitBox<Apple>(new Apple(200));
Apple app=apBox.pullOut();
app.showAppleWeight();
}
}
Generic 제네릭 Step2
문제1.
AAA 클래스에 매개변수가 한개인 제네릭 메소드를 정의하고 main 메소드가 있는 Test 클래스에서 호출해 보자.
class AAA
{
public <T> void show(T inst)
{
System.out.println(inst);
}
}
class CCC
{
public String toString()
{
return "CCC class";
}
}
public class Test {
public static void main(String[] args) {
AAA aaa = new AAA();
aaa.<CCC>show(new CCC());
aaa.show(new CCC());
// 가능
// 컴파일러가 메소드 호출 시 전달되는 참조변수의
// 자료형을 근거로 자료형 정보를 판단할 수 있기 때문이다.
}
}
문제2.
둘 이상의 매개변수를 받는 제네릭 메소드를 정의하고 호출해 보자.
class AAA
{
public String toString()
{
return "Class AAA";
}
}
class BBB
{
public String toString()
{
return "Class BBB";
}
}
class InstanceTypeShower2
{
public <T, U> void showInstType(T inst1, U inst2)
{
System.out.println(inst1);
System.out.println(inst2);
}
}
class IntroGenericMethod2
{
public static void main(String[] args)
{
AAA aaa=new AAA();
BBB bbb=new BBB();
InstanceTypeShower2 shower=new InstanceTypeShower2();
shower.<AAA, BBB>showInstType(aaa, bbb);
shower.showInstType(aaa, bbb);
}
}
// 컴파일러는 메소드 호출 시 전달되는 참조변수의 자료형을
// 통해서 T가 AAA임을, U가 BBB임을 인식할 수 있기 때문에,
// 이 문장에서 보이듯이 <AAA, BBB>를 생략할 수 있다.
// 그리고 이것이 일반적인 호출방식이다.
문제3.
둘 이상의 인스턴스 변수 기반의 제네릭 클래스를 정의해 보자.
class GenericTwoParam<T, U>
{
T item1;
U item2;
public void setItem1(T item)
{
item1 = item;
}
public void setItem2(U item)
{
item2 = item;
}
}
문제4.
아래에 정의된 클래스는 컴파일 시 에러가 발생한다. 여러분이 직접 컴파일을 해서 문제점이 무엇인지 확인하고, 문제점의 원인을 유추하기 바란다.
class MyClass
{
public <T> void simpleMethod(T param)
{
param.showData();
System.out.println(param);
}
}
문제5.
아래 소스코드는 컴파일시 에러가 발생한다. 이것을 에러가 없게 수정하자. Generic 부분은 수정하지 않고 메소드의 몸통만 수정해서 에러가 없게 하자.(showInstanceAncestor, showInstanceName 메소드의 내용부분)
interface SimpleInterface
{
public void showYourName();
}
class UpperClass
{
public void showYourAncestor()
{
System.out.println("UpperClass");
}
}
class AAA extends UpperClass implements SimpleInterface
{
public void showYourName()
{
System.out.println("Class AAA");
}
}
class BBB extends UpperClass implements SimpleInterface
{
public void showYourName()
{
System.out.println("Class BBB");
}
}
class BoundedTypeParam
{
public static <T> void showInstanceAncestor(T param)
{
param.showYourAncestor();
}
public static <T> void showInstanceName(T param)
{
param.showYourName();
}
public static void main(String[] args)
{
AAA aaa=new AAA();
BBB bbb=new BBB();
showInstanceAncestor(aaa);
showInstanceName(aaa);
showInstanceAncestor(bbb);
showInstanceName(bbb);
}
}
▶▶▶
interface SimpleInterface
{
public void showYourName();
}
class UpperClass
{
public void showYourAncestor()
{
System.out.println("UpperClass");
}
}
class AAA extends UpperClass implements SimpleInterface
{
public void showYourName()
{
System.out.println("Class AAA");
}
}
class BBB extends UpperClass implements SimpleInterface
{
public void showYourName()
{
System.out.println("Class BBB");
}
}
class BoundedTypeParam
{
public static <T> void showInstanceAncestor(T param)
{
((UpperClass)param).showYourAncestor();
}
public static <T> void showInstanceName(T param)
{
((SimpleInterface)param).showYourName();
}
public static void main(String[] args)
{
AAA aaa=new AAA();
BBB bbb=new BBB();
showInstanceAncestor(aaa);
showInstanceName(aaa);
showInstanceAncestor(bbb);
showInstanceName(bbb);
}
}
문제6.
소스코드를 컴파일 해보면 컴파일 시에는 에러가 발생하지 않지만(javac BoundedTypeParam.java)
런타임시에는 오류가 발생한다.(java BoundedTypeParam).
제네릭 매개변수로는 Object 클래스에 정의된 메소드만 호출 가능하기 때문에, 아래 예제에서는
매개변수 param을 강제 형변환하고 있다. 그런데 이렇게 되면, 아래 코드는 자료형에 안전하지 않은 코드가 되어버린다.
쉽게 말해서 SimpleInterface 인터페이스를 구현하지 않은 인스턴스, 또는 UpperClass를 상속하지 않은 인스턴스의 참조 값이 메소드에 전달되어도 컴파일 및 실행이 되기 때문에, 앞서 말한 제네릭의 장점은 완전히 소멸되는 셈이다.
interface SimpleInterface
{
public void showYourName();
}
class UpperClass
{
public void showYourAncestor()
{
System.out.println("UpperClass");
}
}
class AAA implements SimpleInterface
{
public void showYourName()
{
System.out.println("Class BBB");
}
}
class BBB extends UpperClass
{
}
class BoundedTypeParam
{
public static <T> void showInstanceAncestor(T param)
{
((UpperClass)param).showYourAncestor();
}
public static <T> void showInstanceName(T param)
{
((SimpleInterface)param).showYourName();
}
public static void main(String[] args)
{
AAA aaa=new AAA();
BBB bbb=new BBB();
showInstanceAncestor(aaa);
showInstanceName(bbb);
}
}
문제7.
문제5를 안정성있는 형태로 바꾸어 보자.
interface SimpleInterface
{
public void showYourName();
}
class UpperClass
{
public void showYourAncestor()
{
System.out.println("UpperClass");
}
}
class AAA extends UpperClass implements SimpleInterface
{
public void showYourName()
{
System.out.println("Class AAA");
}
}
class BBB extends UpperClass implements SimpleInterface
{
public void showYourName()
{
System.out.println("Class BBB");
}
}
class BoundedTypeParam
{
public static <T> void showInstanceAncestor(T param)
{
param.showYourAncestor();
}
public static <T> void showInstanceName(T param)
{
param.showYourName();
}
public static void main(String[] args)
{
AAA aaa=new AAA();
BBB bbb=new BBB();
showInstanceAncestor(aaa);
showInstanceName(aaa);
showInstanceAncestor(bbb);
showInstanceName(bbb);
}
}
▶▶▶
interface SimpleInterface
{
public void showYourName();
}
class UpperClass
{
public void showYourAncestor()
{
System.out.println("UpperClass");
}
}
class AAA implements SimpleInterface
{
public void showYourName()
{
System.out.println("Class BBB");
}
}
class BBB extends UpperClass
{
}
class BoundedTypeParam2
{
public static <T extends UpperClass> void showInstanceAncestor(T param)
{
param.showYourAncestor();
}
public static <T extends SimpleInterface> void showInstanceName(T param)
{
param.showYourName();
}
public static void main(String[] args)
{
AAA aaa=new AAA();
BBB bbb=new BBB();
showInstanceAncestor(bbb);
showInstanceName(aaa);
}
}
문제8.
문제6을 안정성있는 형태로 바꾸었는데 컴파일 해보면(javac BoundedTypeParam.java) 이제는 컴파일 시에 에러가 나오는 것을 볼 수가 있다. 자료형에 대한 안정성이 보장되었다.
Generic 제네릭 Step3
문제1.
제네릭 메소드에 매개변수로 배열을 전달하는 형태로 정의및 호출해 보자.
class IntroGenericArray
{
public static <T> void showArrayData(T[] arr)
{
for(int i=0; i<arr.length; i++)
System.out.println(arr[i]);
}
public static void main(String[] args)
{
String[] stArr=new String[]{
"Hi!",
"I'm so happy",
"Java Generic Programming"
};
showArrayData(stArr);
}
}
문제2.
다음의 메소드 정의를 보면서 매개변수로 전달될 수 있는 대상의 범위를 정리해 보자.
public void hiMethod(Apple param) { ... }
▶ 매개변수의 자료형이 Apple이거나, Apple 인스턴스 또는 Apple을 상속하는 인스턴스의 참조 값이 매개 변수에 전달될 수 있다.
문제3.
다음의 메소드 정의를 보면서 매개변수로 전달될 수 있는 대상의 범위를 정리해 보자.
public void onMethod(FruitBox<Fruit> param) { ... }
(1) FruitBox<Fruit> 인스턴스의 참조 값이 전달 대상이 될 수 있다.
Fruit
↑
Apple
class Apple extends Fruit
이렇듯 Apple 클래스가 Fruit 클래스를 상속하는 경우에, FruitBox<Apple> 인스턴스의 참조 값이 위의 onMethod의 매개변수에 전달될 수 있겠는가? 정답은 No!
우리가 앞서 배운 상속의 관점에서 보면 Yes라고 답을 하는 것도 무리는 아니니 말이다. 하지만 생각해 보자. Fruit과 Apple이 상속관계에 놓여있다고 해서 FruitBox<Fruit>과 FruitBox<Apple>이 상속관계에 놓이는 것은 아니다. 상속관계에 놓이려면 클래스가 정의되는 과정에서 키워드 extends를 통해서 상속됨이 명시되어야 한다. 그런데 FruitBox<Fruit>와 FruitBox<Apple>가 키워드 extends를 통해서 명시되는 관계는 아니지 않은가?
FruitBox<Apple> 인스턴스의 참조 값도 인자로 전달받을 수 있는 매개변수의 선언은 어떻게 해야 할까?
자바는 이를 위해서 와일드 카드를 이용한 자료형의 명시를 허용한다. 참고로 와일드 카드란, 이름 또는 문자열에 제한을 가하지 않음을 명시하는 용도로 사용되는 특별한 기호를 말한다.
FruitBox<? extends Fruit> box1 = new FruitBox<Fruit>();
FruitBox<? extends Fruit> box2 = new FruitBox<Apple>();
위의 <? extends Fruit>가 의미하는 바는 "Fruit을 상속하는 모든 클래스"이다. 즉 자료형을 결정 짓는 제네릭 매개변수 T에 Fruit클래스를 포함하여, Fruit을 상속하는 클래스면 무엇이든 올 수 있음을 명시하는 것이다. 따라서 참조변수 box1과 box2는 다음의 형태로 생성되는 인스턴스면 무엇이든 참조가 가능하다.
new FruitBox<'Fruit 클래스, 또는 Fruit을 상속하는 클래스의 이름'>()
문제4.
자료형을 결정짓는 제네릭 매개변수 T에 Fruit클래스를 포함하여, Fruit을 상속하는 클래스면 무엇이든 올 수 있음을 명시할려면?
FruitBox<? extends Fruit> box1 = new FruitBox<Fruit>();
문제5.
문제4에 해당하는 소스코드를 작성하시오.
class Fruit
{
public void showYou()
{
System.out.println("난 과일입니다.");
}
}
class Apple extends Fruit
{
public void showYou()
{
super.showYou();
System.out.println("난 붉은 과일입니다.");
}
}
class FruitBox<T>
{
T item;
public void store(T item) { this.item=item; }
public T pullOut() { return item; }
}
class IntroWildCard
{
public static void openAndShowFruitBox(FruitBox<? extends Fruit> box)
{
Fruit fruit=box.pullOut();
fruit.showYou();
}
public static void main(String[] args)
{
FruitBox<Fruit> box1=new FruitBox<Fruit>();
box1.store(new Fruit());
FruitBox<Apple> box2=new FruitBox<Apple>();
box2.store(new Apple());
openAndShowFruitBox(box1);
openAndShowFruitBox(box2);
}
}
문제6.
전달되는 자료형에 상관없이 FruitBox<T>의 인스턴스를 참조하려면?
FruitBox<?> box;
또는 FruitBox<? extends Object> box;
문제7.
FruitBox<T>가 인스턴스를 참조하되, T가 Apple 클래스 또는 Apple 클래스가 직간접적으로 상속하는 클래스인 경우에만 참조할 수 있게 하려면?
FruitBox<? super Apple> boundedBox;
extends는 다음의 의미로 사용된다. "~을 상속하는 클래스라면 무엇이든지"
super는 다음의 의미로 사용이 된다. "~이 상속하는 클래스라면 무엇이든지"
위에 선언된 참조변수 boundedBox는 FruitBox<T>의 인스턴스를 참조하되, T가 Apple 클래스 또는 Apple 클래스가 직간접적으로 상속하는 클래스인 경우에만 참조할 수 있다. 예를 들어서 Apple 클래스가 Fruit 클래스를 상속하는 구조를 갖는다면(Apple extends Fruit), 위의 boundedBox가 참조할 수 있는 인스턴스의 자료형은 다음 세 가지이다.
FruitBox<Object>, FruitBox<Fruit>, FruiBox<Apple>
문제8.
제네릭 클래스 A를 상속하려면?
class AAA<T>
{
T itemAAA;
}
class BBB<T> extends AAA<T>
{
T itemBBB;
}
이렇게 상속이 되면, 하나의 자료형 정보로 인해서 AAA의 자료형과 BBB의 자료형이 모두 결정된다. 즉 다음과 같이 문장을 구성하면, T가 각각 String과 Integer로 대체되어 인스턴스가 생성된다.
BBB<String> myString = new BBB<String>();
BBB<Integer> myInteger = new BBB<Integer>();
문제9.
AAA<T>클래스의 T를 지정해서 상속할려면?
class BBB extends AAA<String>
{
int itemBBB;
}
물론 위의 BBB 클래스는 제네릭으로 정의될 수도 있다. 그러나 제네릭이 아니어도 된다는 것을 강조하기 위해서 일반 클래스로 정의하였다.
문제10.
인터페이스를 제네릭으로 정의해보자.
interface MyInterface<T>
{
public T myFunc(T item);
}
interface AAA<T>
{
void aaa(T item);
}
문제11.
인터페이스를 구현하여 클래스를 정의하는 방식 두가지
(1) T를 그대로 유지하는 방식
class MyImplement<T> implements MyInterface<T>
{
public T myFunc(T item)
{
return item;
}
}
(2) T의 자료형을 결정하는 방식
class MyImplement implements MyInterface<String>
{
public String myFunc(String item)
{
return item;
}
}
주의해야 할 사실은 위의 클래스 정의와 같이 T의 자료형이 String으로 결정되면, MyInterface<T>의 메소드 myFunc를 구현할 때에도 T가 아닌 String으로 명시해야 한다는 점이다.
* 기본자료형의 이름은 제네릭 클래스의 인스턴스 생성에 사용될 수 없다. 즉, 다음의 형태로는 인스턴스의 생성이 불가능하다.
FruitBox<int> fb1 = new FruitBox<int>();
FruitBox<double> fb2 = new FruitBox<double>();
'Java > Day28' 카테고리의 다른 글
| [Java] Generic 제네릭 예제 (0) | 2021.12.13 |
|---|---|
| [Java] Generic 제네릭 개념 (0) | 2021.12.12 |