목표
자바의 제네릭에 대해 학습하기
학습할 것
- 제네릭 사용법
- 제네릭 주요 개념(바운디드 타입, 와일드 카드)
- 제네릭 메소드 만들기
- Erasure
제네릭(Generic)이란? 일반화한다! 그리고 그 일반화의 대상은 자료형이다.
자바의 제네릭은 명료하며 쉽게 익숙해질 수 있다. (라고 책에서 말했다)
제네릭은 어떠한 자료형을 기반으로도 인스턴스의 생성이 가능하도록 자료형에 일반적인 클래스를 정의하는 문법이다.
제네릭 사용법
우선 결론부터 살펴보자면,
실행과정에서 발견되는 오류를 컴파일 과정에서 발견되도록 코드를 작성하는 것은 매우 의미 있는 일이다!
예를 들어보자!
사과를 담는 AppleBox 클래스와 오렌지를 담는 OrangeBox 클래스를 정의하였다. (Apple, Orange 클래스는 정의 되었다고 가정하자)
class AppleBox{
Apple item;
public void store(Apple item){ this.item = item; }
public Apple pullOut() { return item; }
}
class OrangeBox{
Orange item;
public void store(Orange item){ this.item = item; }
public Orange pullOut() { return item; }
}
AppleBox, OrangeBox 모두 무언가를 담고 있으니,
Obejct 클래스를 기반으로 하나만 정의하는게 낫지 않나? 생각할 수 있다.
Object 클래스를 기반으로 하나의 클래스만 정의하면, Apple이든 Orange든 관계없이 모든 인스턴스를 저장할 수 있다.
class FruitBox{
Object item;
public void store(Object item){ this.item = item; }
public Object pullOut() { return item; }
}
이렇게 FruitBox 클래스를 정의하면 모든 과일을 정의할 수 있지만, Object를 기반으로 정의하지 않는다.
왜 그럴까?
class Orange{
int sugarContent;
public Orange(int sugar){ sugarContent = sugar; }
public void showSugarContent(){
System.out.println("당도 " + sugarContent);
}
}
class FruitBox{
Object item;
public void store(Object item){ this.item = item; }
public Object pullOut() { return item; }
}
class ObjectBaseFruitBox{
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(); // ERROR!
fBox2.store("오렌지"); // ERROR!
/**
* FruitBox는 과일을 담는 클래스이다.
* 문자열을 담기 위해 정의한 클래스가 아니다.
* 그러나 컴파일 오류가 발생하지 않는다(Point!!!!)
*/
Orange org2 = (Orange)fBox2.pullOut();
org2.showSugarContent();
}
}
fBox2에서는 문법적으로 오류가 없으며, 컴파일 오류가 발생하지 않는다.
컴파일 오류가 발생하지 않기 때문에 문제가 발견되지 않는다!!!
따라서 fBox2.store("오렌지"); 는 저장되지 않으며 Orange org2 = (Orange)fBox2.pullOut(); 에서도 예외가 발생한다.
결과적으로 org2의 당도를 확인할 수 없다.
컴파일 과정에서 발견하지 못한 오류로 예외가 발생했다.
그렇다면 과일을 FruitBox 클래스를 이용하여 저장하지 않고, 맨 처음 정의했던 OrangeBox를 기반으로 작성한다면?
class Orange{
int sugarContent;
public Orange(int sugar){ sugarContent = sugar; }
public void showSugarContent(){
System.out.println("당도 " + sugarContent);
}
}
class OrangeBox{
Orange item;
public void store(Orange item){ this.item = item; }
public Orange pullOut() { return item; }
}
class ObjectBaseFruitBox{
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("오렌지"); // Compile ERROR!
Orange org2 = (Orange)fBox2.pullOut();
org2.showSugarContent();
}
}
이 코드에서는 컴파일 할 때 컴파일 과정에서 에러 메시지를 확인할 수 있다.
이렇게 컴파일 과정에서 발견되는 오류는 쉽게 해결 가능하다.
FruitBox를 이용했을 때와 차이가 없어 보이지만, 프로그램의 규모가 커진다면 차이 또한 매우 극명하게 드러난다.
따라서 실행 과정에서 발견되는 오류를 컴파일 과정에서 발견되도록 코드를 작성하는 것은 매우 의미있는 일이다!
OrangeBox를 이용하면 자료형에 대한 안전성이 보장된다. 또한 단점으로는 상황에 따라서 둘 이상의 클래스를 정의해야 한다는 점이 있다.
이러한 단점을 보완하기 위한 문법이 바로 Generic이다!
그래서 제네릭 클래스의 정의 방법은?
앞서 사용한 FruitBox이다.
이 FruitBox를 이용하여 Apple, Orange, Banana, Grape 등을 모두 사용하고 싶다..
class FruitBox{
Object item;
public void store(Object item){ this.item = item; }
public Object pullOut() { return item; }
}
이것을 제네릭 클래스로 변환하면?
class FruitBox <T>{
T item;
public void store(T item){ this.item = item; }
public T pullOut() { return item; }
}
T는 Type의 약자이며, 매개변수화 된 자료형임을 나타낸다.
이 클래스의 인스턴스를 생성하려면 자료형 정보를 인자로 전달해야한다. 전달되는 인자는 클래스에 존재하는 T를 대체하여 인스턴스를 생성한다.
FruitBox<Orange> orBox = new FruitBox<Orange>();
FruitBox<Apple> apBox = new 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 <T>{
T 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>();
orBox.store(new Orange(10));
Orange org = orBox.pullOut();
org.showSugarContent();
FruitBox<Apple> apBox = new FruitBox<Apple>();
apBox.store(new Apple(30));
Apple app = apBox.pullOut();
app.showAppleWeight();
}
}
/** output
* 당도 : 10
* 무게 : 30
*/
제네릭을 이용하여 하나의 클래스 정의로 둘 이상의 클래스를 정의한 효과를 볼 수 있다.
또한 자료형이 일치하지 않아도 컴파일 과정에서 오류가 발생하지 않는다.!
제네릭 주요 개념(바운디드 타입, 와일드 카드)
제네릭 매개변수로는 Object 클래스에 정의된 메소드만 호출 가능하다.
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){
((SimpleInterface)param).showYourName();
}
public static <T> void showInstanceName(T param){
((UpperClass).param).showYourAncestor();
}
public static void main(String args[]){
AAA aaa = new AAA();
BBB bbb = new BBB();
showInstanceAncestor(aaa);
showInstanceName(aaa);
showInstanceAncestor(bbb);
showInstanceName(bbb);
}
}
/**output
* Class AAA
* UpperClass
* Class BBB
* UpperClass
*/
위의 예제는 매개변수 param을 강제 형변환하고 있으며 SimpleInterface 인터페이스를 구현하지 않은 인스턴스와 UpperClass를 상속하지 않은 인스턴스의 참조 값이 메소드에 전달되어도 컴파일과 실행이 되기 때문에 제네릭의 장점이 사라진다.
그래서 자바는 제네릭 매개변수의 자료형에 제한을 둘 수 있는 문법을 제공한다!
이를 기반으로 다음과 같이 작성할 수 있다.
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{
/**
* T가 SimpleInterface를 상속 또는 구현하는 클래스의 자료형이 되어야 함을 명시한다.
* 그래서 이 인터페이스에 정의 되어 있는 메소드의 호출이 가능하다
*/
public static <T extends SimpleInterface> void showInstanceAncestor(T param){
param.showYourName();
}
/**
* T가 UpperClass를 상속 또는 구현하는 클래스의 자료형이 되어야 함을 명시한다.
*/
public static <T extends UpperClass> void showInstanceName(T param){
param.showYourAncestor();
}
public static void main(String args[]){
AAA aaa = new AAA();
BBB bbb = new BBB();
showInstanceAncestor(aaa);
showInstanceName(aaa);
showInstanceAncestor(bbb);
showInstanceName(bbb);
}
}
클래스의 상속에는 extends를, 인터페이스의 구현에는 implement를 사용하지만,
제네릭의 자료형 제한에는 클래스와 인터페이스를 구분하지 않고 extends를 사용한다!
예상과는 다른 제네릭 변수의 참조와 상속의 관계
public void hiMethod(Apple param) { ... } 과 같은 메소드에서는 매개변수로 전달될 수 있는 대상은 Apple 인스턴스 또는 Apple을 상속하는 인스턴스의 참조 값이 가능하다.
public void ohMethod(FruitBox<Fruit> param) { ... } 과 같은 메소드에서는 FruitBox<Fruit> 인스턴스의 참조 값이 매개변수로 전달 될 수 있다.
만약 Apple 클래스가 Fruit 클래스를 상속한다면?????
FruitBox<Fruit> 과 FruitBox<Apple>이 상속 관계에 놓이는 것은 아니다.
-> 상속 관계가 되려면 클래스가 정의 될 때 extends를 통해 명시되어야 하기 때문이다.
그.렇.다.면!
class Apple extends Fruit 관계 일 때, FruitBox<Fruit> 인스턴스 참조 값과 FruitBox<Apple> 인스턴스의 참조 값도 인자로 전달 받을 수 있는 매개변수를 선언하기 위해서는?
자바에서 와일드카드를 이용한 자료형의 명시를 허용한다.
와일드카드
- 와일드카드란 이름 또는 문자열에 제한을 가하지 않음을 명시하는 용도로 사용되는 특별한 기호이다.
- 명령 프롬프트 상에서는 기호 * 가 와일드 카드로 사용되며
- 자바는 기호 ? 를 와일드 카드로 사용한다.
와일드카드를 사용하여 다음과 같이 선언할 수 있다.
FruitBox<? extends Fruit> box1 = new FruitBox<Fruit>();
FruitBox<? extends Fruit> box2 = new FruitBox<Apple>();
<? extends Fruit> 는 Fruit를 상속하는 모든 클래스라는 의미이다.
제네릭 매개변수 T에 Fruit 클래스를 포함하여 Fruit을 상속하는 클래스는 어떤것이든 올 수 있다.
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{
/**
* Fruit 또는 Fruit을 상속하는 인스턴스가 메소드의 인자로 전달 가능
*/
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.stroe(new Fruit());
FruitBox<Apple> box2 = new FruitBox<Apple>();
box2.store(new Apple());
openAndShowFruitBox(box1);
openAndShowFruitBox(box2);
}
}
/**output
* 난 과일
* 난 과일
* 난 붉은 과일
*/
하위 클래스를 제한하는 용도의 와일드 카드
FruitBox<? super Apple> boundedBox;
extends를 대신하여 super를 사용하여 선언할 수 있다.
즉 boundedBox는 FruitBox<T>의 인스턴스를 참조하며, T가 Apple 클래스또는 Apple 클래스가 직간접적으로 상속하는 클래스인 경우에만 참조할 수 있다.
제네릭 메소드 만들기
자바는 클래스 전부가 아닌 특정 메소드에 대해서만 제네릭으로 선언하는 것을 허용한다.
class AAA{
public String toString(){ return "Class AAA"; }
}
class BBB{
public String toString(){ return "Class BBB"; }
}
class InstanceTypeShower{
int showCnt = 0;
public <T> void showInstType(T inst){
System.out.println(inst);
showCnt++;
}
void showPrintCnt(){
System.out.println("show count : " + showCnt);
}
}
class IntroGenericMethod{
public static void main(String args[]){
AAA aaa = new AAA();
BBB bbb = new BBB();
InstanceTypeShower shower = new InstanceTypeShower():
shower.<AAA>showInstType(aaa); // 제네릭 메소드의 호출 방법!
shower.<BBB>showInstType(bbb);
shower.showPrintCnt();
}
}
/** output
* Class AAA
* Class BBB
* Show count : 2
*/
shower.<AAA>showInstType(aaa); 는 <AAA>를 통해 T를 AAA로 간주하여 호출한다.
이것은 일반적으로 shower.showInstType(aaa); 로도 호출 가능하다.
<AAA>를 명시하지 않아도 컴파일러가 메소드 호출 시 전달되는 참조변수 aaa의 자료형을 근거로 자료형 정보를 판단할 수 있기 때문이다.
또한, 제네릭 메소드에서는 둘 이상의 자료형 매개변수를 선언하고, 각각 다른 자료형 정보를 전달 할 수도 있다.
class AAA{
public String toString(){ return "Class AAA"; }
}
class BBB{
public String toString(){ return "Class BBB"; }
}
class InstanceTypeShower{
int showCnt = 0;
public <T, U> void showInstType(T inst1, U inst2){
System.out.println(inst1);
System.out.println(inst2);
}
}
class IntroGenericMethod{
public static void main(String args[]){
AAA aaa = new AAA();
BBB bbb = new BBB();
InstanceTypeShower shower = new InstanceTypeShower():
shower.<AAA, BBB>showInstType(aaa, bbb);
shower.showInstType();
}
}
/**
* Class AAA
* Class BBB
* Class AAA
* Class BBB
*/
위 예제는 제네릭 메소드에 대한 것이며, 또한 제네릭 클래스에서도 동일하게 적용된다.
제네릭 클래스에서라면?
class GenericTwoParam<T, U>{
T item1;
U item2;
public void setItem1(T item){
item1 = item;
}
public void setItem2(U item){
item2 = item;
}
}
Erasure
생소한 개념! 스터디 라이브보고 보충하자!
Reference
- 난 정말 JAVA를 공부한 적이 없다구요, 윤성우 저
'Java Live Study' 카테고리의 다른 글
Live Study 13주차 : I/O (0) | 2021.03.05 |
---|---|
Live Study 12주차 : 애노테이션 (0) | 2021.02.06 |
Live Study 9주차 : 예외 처리 (0) | 2021.01.16 |
Live Study 8주차 : 인터페이스 (0) | 2021.01.16 |
Live Study 7주차 : 패키지 (0) | 2021.01.02 |