JDK 5부터 제공되는 제네릭은 소스에서 데이터 타입을 프로그래밍할 때 결정하는 것이 아니고 실행할 때 결정하게 하는 기능으로, 매개변수 타입이라고도 한다.
제네릭이란 ?
다음 그림처럼 가방에 책, 연필통, 노트를 담는 작업을 자바로 구현한다고 가정한다.
class Bag{
Book book;
Pecil pencil;
}
class Book{}
class Pencil{}
Bag에 객체를 담는 작업은 자바에서 "has a"관계로 표현하고 이는 필드 선언으로 나타낸다.
만약 가방에 책만 들어가면 연필은 사용하지 않으므로 메모리를 낭비하는 코드가 된다.
이런 문제점을 해결하기 위한 방법이 제네릭으로 데이터 타입을 매개변수로 지정하는 것을 의미한다.
타입 매개변수는 실행 시 인자로 전달하는 타입을 변수의 타입으로 지정하는 것으로 클래스, 인터페이스, 메서드에서 사용할 수 있고 이를 각각 제네릭 클래스, 제네릭 인터페이스, 제네릭 메서드라고 한다.
제네릭 클래스
제네릭 클래스 선언
제네릭 클래스를 선언할 때는 클래스 선언부에서 클래스 이름 다음 꺽쇠 기호<>를 표시한다.
그리고 <>안에는 타입 매개변수의 이름을 넣고 제네릭 클래스의 인스턴스를 생성할 때 타입 매개변수는 인자로 전달받은 타입으로 대체된다. 일반적으로 타입 매개변수 이름은 T, V처럼 알파벳 대문자 한 글자로 표현한다.
public class Bag<T>{
T thing;
public Bag(T thing){
this.thing = thing;
}
}
Bag클래스에서 사용한 <T>는 인스턴스 생성 시 전달되는 타입으로 대체된다. 타입 매개변수로 전달되는 값을 '타입 인자'라고 한다
제네릭 클래스 생성
new Bag<Book>(new Book());
new Bag<Pencil>(new Pencil());
new 명령문으로 클래스 생성 시 클래스 이름 다음 타입인자를 <>기호로 감싼다.
제네릭 클래스를 사용하면 인스턴스 생성 시 타입을 지정할 수 있으므로 동적으로 코드를 재사용을 할 수 있다.
JDK 7부터는 <>안의 타입인자를 생략할 수 있다.
제네릭 클래스 참조
제네릭 클래스의 인스턴스를 생성한 후 참조하는 변수가 있어야 계속 사용할 수 있다.
Bag<Book> bag = new Bag<Book>(new Book());
Bag<Pencil> bag2 = new Bag<Pencil>(new Pencil());
제네릭의 장점
class Bag{
Object thing;
public Bag(Object thing){
this.thing = thing;
}
}
Bag bag = new Bag(new Book());
Bag bag2= new Bag(new Pencil());
제네릭이 나오기 전에는 제네릭과 동일한 효과를 내기 위해 Object로 변수 타입을 선언 했다고 한다.
Bag 객체 생성까지는 똑같이 자바 객체 타입을 전달할 수 있다. 하지만 그럼에도 제네릭이 필요한 이유가 있는데
1. 불필요한 타입변경을 막는다.
Book book = (Book)bag.thing;
Pencil pencil = (Pencil)bag2.thing;
Book 내의 객체가 Object이므로 캐스팅이 필요하다. 하지만 제네릭을 사용하면 타입 매개변수에 지정한 타입으로 자동변경된다.
2. 엄격한 타입 검사를 통해 안전성을 높여준다.
class Bag{
Object thing;
public Bag(Object thing){
this.thing = thing;
}
}
Bag bag = new Bag(new Book());
Bag bag2= new Bag(new Pencil());
bag = bag2 // 오류 발생하지 않음
위의 코드처럼 Object 객체를 사용하면 bag, bag2는 모두 Bag 타입이기 때문에 오류가 발생하지 않는다.
하지만 bag은 Book을 가진 Bag, bag2는 Pencil을 가진 Bag 객체를 참조하므로 bag = bag2는 잘못된 코드다.
Bag<Book> bag = new Bag<Book>(new Book());
Bag<Pencil> bag2 = new Bag<Pencil>(new Pencil());
bag = bag2 // 오류 발생
이처럼 제네릭은 컴파일러가 타입 검사를 엄격하게 타입의 안정성을 보장 받을 수 있다.
타입 매개변수
타입제한
public class Bag<T>{
T thing;
public Bag(T thing){
this.thing = thing;
}
}
다음처럼 선언하면 어떤 타입인자도 받을 수 있다. 하지만 물이너 커피처럼 액체로 된 물건은 담을 수 없도록 하려고 한다.
제네릭 클래스 선언 시에 <T extends superclass>로 슈퍼클래스나 슈퍼클래스의 서브 클래스만 가능하게 제한을 걸 수 있다.
class Bag<T extends Solid>{
T thing;
public Bag(T thing){
this.thing = thing;
}
}
class Solid{}
class Liquid{}
class Book extends Solid{}
class Pencil extends Solid{}
class Water extends Liquid{}
class Coffee extends Liquid{}
Bag<Water> bag = new Bag<Water>(new Water()); // Solid의 subClass가 아니므로 오류 발생
와일드카드
public class Bag<T>{
T thing;
String owner;
public Bag(T thing){
this.thing = thing;
}
}
class Main{
static boolean isSameOwner(Bag<T> aBag, Bag<T> bBag){ // 매개변수 타입이 다르므로 오류 발생
if(aBag.owner.equals(bBag.owner))return true;
return false;
}
public static void main(Stirng[] args){
Bag<Book> bag = new Bag<Book>(new Book());
bag.owner = "jack";
Bag<Pencil> bag2 = new Bag<Pencil>(new Pencil());
bag2.owenr = "jack";
isSameOwner(bag, bag2); // 매개변수 타입이 달라서 컴파일 오류 발생
}
}
이럴 때 사용할 수 있는 것이 와일드 카드인데 와일드 카드는 ? 기호로 표현하고 <?> 와일드카드를 사용하면 현재 객체의 타입 매개변수와 같지 않은 타입의 Bag을 인자로 받을 수 있다.
boolean isSameOwner(Bag<?> aBag, Bag<?> bBag){}
매개변수의 타입보다는 사용 자체가 중요할 때 사용한다.
와일드카드 제한
하지만 와일드카드를 사용하면 Object를 사용하는 것과 다름이 없으므로 제네릭 타입 한정연산자인 extends, super 키워드로 하위타입으로만 제한할지, 상위 타입으로만 제한할지에 따라 쓰면 된다.
class GrantParent{}
class Parent extends GrantParent{}
class Child extends Parent{}
<? extends X> 상한 경계 와일드카드
X와 X를 상속하는 SubClass를 담을 수 있다.
void printCollection(Collection<? extends Parent> x) {
for(Child y : x) {} // 상한 경계보다 하위 단계의 클래스 >> 컴파일 오류
for(Parent y : x) {}
for(GrantParent y : x) {}
}
사용할 때는 X를 포함한 SuperClass는 꺼내 쓸 수 있다.(일시적 gettr o)
X를 상속하는 SubClass는 구체화된 특징을 특정할 수 없어 컴파일 오류가 난다.
void add(Collection<? extends Parent> x) {
x.add(new Child()); // >> 오류
x.add(new Parent());// >> 오류 Parent를 포함한 어떤 SubClass로 이루어 져있는지 모르기 때문에 모두 컴파일 오류
x.add(new GrantParent()); // >> 오류
}
하지만 객체 내의 와일드 카드를 담당하는 객체의 추가는 불가능하다.(settr x)
<? super X > 하한 경계 와일드카드
X와 X보다 상위의 SuperClass를 담을 수 있다.
void add(Collection<? super Parent> x) {
x.add(new Child()); //
x.add(new Parent());//
x.add(new GrantParent()); // >> 오류
}
X와 X를 상속받는 SubClass는 객체의 추가가 가능하다(일시적 settor o)
X의 SuperClass를 넣으면 컴파일 오류가 난다.
void printCollection(Collection<? super Parent> x) {
for(Child y : x) {} // Parent 위로 어떤 클래스인지 특정할 수 없기에 컴파일 오류
for(Parent y : x) {} // >> 오류
for(GrantParent y : x) {} // >> 오류
for(Object y : x) {}
}
어떤 객체가 담겨있는지 모르기에 최상위 클래스인 Object로만 getter가 가능하다.
참고
https://mangkyu.tistory.com/241
처음해보는 자바프로그래밍 / 저자 오정임