✔ 제네릭의 이해
제네릭이란?
- 제네릭이란 자료형을 대상으로 일반화하는 것
- 이로 인해 자료형에 의존적이지 않은 클래스를 정의할 수 있게 됨
- 자료형에 의존적이지 않은 형태로 클래스를 정의하기 위해서는
Object를 타입 매개변수인 T로 대체하고 T가 인스턴스 생성 시 자료형을 결정하기 위한 표식 임을 알린 후,
인스턴스를 생성할 때 T의 자료형을 타입 인자로써 전달하여 매개변수화 타입(제네릭 타입)을 결정하면 됨
- 이로 인해 필요 시 형 변환을 해야 하는 불편함이 사라지고
프로그래머가 자료형과 관련된 실수를 할 경우 컴파일 과정에서 드러나게 할 수 있게 됨
// 제네릭 이전 (AppleBox, OrangeBox)
class Apple {
public String toString() {
return "I am an apple";
}
}
class AppleBox {
private Apple ap;
public void set(Apple a) {
ap = a;
}
public Apple get() {
return ap;
}
}
... Orange, OrangeBox 위와 동일
class FruitAndBox {
public static void main(String[] args) {
AppleBox aBox = new AppleBox();
aBox.set(new Apple());
Apple ap = aBox.get();
}
}
// Box들을 Object 클래스 하나로 대체 (Box)
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();
aBox.set(new Apple());
Apple ap = (Apple)aBox.get();
}
}
// 제네릭 기반의 클래스 (Box<T>)
class Box<T> { // Box<T>에서 T는 타입 매개변수
private T ob;
public void set(T o) {
ob = o;
}
public T get() {
return ob;
}
}
class FruitAndBox3 {
public static void main(String[] args) {
Box<Apple> aBox = new Box<Apple>(); // Apple은 타입 인자, Box<Apple>은 매개변수화 타입
aBox.set(new Apple());
Apple ap = aBox.get();
}
}
✔ 제네릭의 기본 문법
다중 매개변수 기반 제네릭 클래스의 정의
- 둘 이상의 타입 매개변수에 대한 제네릭 클래스도 정의할 수 있음
- 타입 매개변수의 이름은 한 문자로 이름을 짓거나, 대문자로 이름을 지음
- 한 문자로 이름을 지을 경우, 가급적 의미를 두어 이름을 짓는 것이 좋음
- E : Element
- K : Key
- N : Number
- T : Type
- V : Value
class DBox<L, R> {
private L left;
private R right;
public void set(L o, R r) {
left = 0;
right = r;
}
...
}
class MultiTypeParam {
public static void main(String[] args) {
DBox<String, Integer> box = new DBox<String, Integer>();
box.set("Apple", 25);
}
}
기본 자료형에 대한 제한 그리고 래퍼 클래스
- 매개변수화 타입을 구성할 때 기본 자료형의 이름은 타입 인자로 쓸 수 없음
- 그러므로 박싱과 언박싱이 자동으로 이루어지는 기본 자료형에 대한 래퍼 클래스를 사용
Box<int> box = new Box<int>(); // X
Box<Integer> box = new Box<Integer>(); // O
타입 인자의 생략
- 컴파일러는 프로그래머가 작성하는 제네릭 관련 문장에서 자료형의 이름을 추론하는 능력을 갖고 있음
- 그러므로 다이아몬드 기호 안을 빈 공간으로 놔두면 참조 변수의 선언을 통해서 채워지게 됨
Box<Apple> aBox = new Box<Apple>();
Box<Apple> aBox = new Box<>();
매개변수화 타입을 타입 인자로 전달하기
- 매개변수화 타입을 타입 인자로 전달하여 사용할 수 있음
Box<String> sBox = new Box<>(); // 박스
Box<Box<String>> wBox = new Box<>(); // 박스 안에 박스
Box<Box<Box<String>>> zBox = new Box<>(); // 박스 안에 박스 안에 박스
제네릭 클래스의 타입 인자 제한하기
- 특성과 용도에 따라 담고 싶은 것을 제한할 수 있음
- 키워드 extends를 사용하면 타입 인자로 해당하는 클래스와 그 클래스를 상속하는 하위 클래스만 올 수 있게 됨
- 타입 인자를 제한할 경우, 참조하는 인스턴스가 해당 클래스의 메소드를 가지고 있음을 보장할 수 있어 호출할 수 있게 됨
class Box<T extends Number> {
private T ob;
...
public int toIntValue() {
return ob.intValue();
}
}
제네릭 클래스의 타입 인자를 인터페이스로 제한하기
- 위와 유사하게 인터페이스로도 타입 인자를 제한할 수 있음
- 인터페이스를 구현하는 클래스로 타입 인자를 제한할 수 있음
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> {
...
}
class BoundedInterfaceBox {
public static void main(String[] args) {
Box<Apple> box = new Box<>();
box.set(new Apple());
...
}
}
- 타입 인자를 제한할 때에는 하나의 클래스와 하나 이상의 인터페이스에 대해 동시에 제한을 할 수 있음
class Box<T extends Number & Eatable> { ... }
제네릭 메소드의 정의
- 클래스 전부가 아닌 일부 메소드에 대해서만 제네릭으로 정의하는 것도 가능함
- 인스턴스 메소드 뿐만 아니라 클래스 메소드에 대해서도 정의가 가능함
- 제네릭 메소드는 메소드 호출시에 자료형이 결정됨
- 타입 인자 정보를 생략할 수 있으며, 컴파일러가 전달되는 인자를 보고 각각 유추할 수 있음
class BoxFactory {
public static <T> Box<T> makeBox(T o) {
Box<T> box = new Box<T>();
box.set(o);
return box;
}
}
Box<String> sBox = BoxFactory.<String>makeBox("Sweet");
Box<String> sBox = BoxFactory.makeBox("Sweet");
제네릭 메소드의 제한된 타입 매개변수 선언
- 제네릭 메소드도 호출 시 전달되는 매개변수 타입 인자를 제한할 수 있음
- 타입 인자를 제한할 경우, 참조하는 인스턴스가 해당 클래스의 메소드를 가지고 있음을 보장할 수 있어 호출할 수 있게 됨
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 SteelBox<T> extends Box<T> {
public SteelBox(T o) {
ob = o;
}
}
// SteelBox<Integer> 제네릭 타입은 Box<Integer> 제네릭 타입을 상속
Box<Integer> iBox = new SteelBox<Integer>(7959);
Box<Integer> iBox = new SteelBox<>(7959);
타겟 타입
- 자바 컴파일러는 생략된 자료형 정보에 대해 유추하는 능력이 있음
- 컴파일러가 자료형 유추를 진행하는 상황이 생각보다 다양함
- 자바 7부터 컴파일러의 자료형 유추 범위가 넓어져 인자와 타입 인자를 전달하지 않더라도,
선언된 매개변수의 형을 보고 판단을 할 수 있게 되며 T의 유추에 사용된 정보를 타겟 타입이라고 함
class BoxFactory {
public static <T> Box<T> makeBox() {
Box<T> box = new Box<T>();
return box;
}
}
// Box<Integer>는 타겟 타입
Box<Integer> iBox = EmptyBoxFactory.<Interger>makeBox();
Box<Integer> iBox = EmptyBoxFactory.makeBox();
와일드카드
- Object와 String이 상속 관계에 있더라도 Box<Object>와 Box<String>은 상속 관계를 형성하지 않음
- 대신 물음표 기호로 표시되는 와일드카드를 사용하면 Box<Object>와 Box<String>이 상속 관계를 형성하도록 할 수 있음
- 이로 인해 Box<T>를 기반으로 생성된, Box<Integer> 인스턴스나 Box<String> 인스턴스들을 인자로 받을 수 있게 됨
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();
}
public static void peekBox(Box<?> 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);
}
// 와일드카드 기반 메소드
public static void peekBox(Box<?> box) {
System.out.println(box);
}
와일드카드의 상한과 하한의 제한
- T가 Number 또는 Number의 하위 클래스인 제네릭 타입의 인스턴스만 전달되도록 제한할 때 상한 제한된 와일드카드를 사용
- Number 또는 Number를 직간접적으로 상속하는 클래스만으로 제한됨
- 그러므로 Box<Integer>, Box<Double>과 같은 제네릭 타입의 인스턴스만 인자로 전달되게 할 수 있음
- 또한 상한 제한되었기 때문에 box가 참조하는 인스턴스를 대상으로 저장하는 기능의 메소드 호출은 불가능하게 되어
필요한 만큼만 기능을 허용하되, 코드의 오류가 컴파일 과정에서 최대한 발견하도록 할 수 있음
- 즉, box가 참조하는 인스턴스를 대상으로 꺼내는 작업만 허용하겠다는 의미
public static void peekBox(Box<? extends Number> box) {
System.out.println(box);
}
class Car extends Toy { ... }
class Robot extends Toy { ... }
public static void outBox(Box<? extends Toy> box) {
box.get();
box.set(new Toy()); // Toy를 상속 받는 인스턴스가 전달된다면 오류 발생
}
- T가 Integer 또는 Integer의 상위 클래스인 제네릭 타입의 인스턴스만 전달되도록 제한할 때 하한 제한된 와일드카드를 사용
- Integer 또는 Integer가 상속하는 클래스만으로 제한됨
- 그러므로 Box<Integer>, Box<Number>, Box<Object>와 같은 제네릭 타입의 인스턴스만 인자로 전달되게 할 수 있음
- 또한 하한 제한되었기 때문에 box가 참조하는 인스턴스를 대상으로 꺼내는 기능의 메소드 호출은 불가능하게 되어
필요한 만큼만 기능을 허용하되, 코드의 오류가 컴파일 과정에서 최대한 발견하도록 할 수 있음
- 즉, box가 참조하는 인스턴스를 대상으로 넣는 작업만 허용하겠다는 의미
public static void peekBox(Box<? super Integer> box) {
System.out.println(box);
}
class Plastic { ... }
class Toy extends Plastic { ... }
public static void inBox(Box<? super Toy> box, Toy n) {
box.set(n);
Toy myToy = box.get(); // 반환형을 Toy로 결정할 수 없으므로 오류 발생
}
- 와일드카드의 상한과 하한 제한을 이용하여 상자의 내용물을 복사하는 것에 사용할 수 있음
public static void moveBox(Box<? super Toy> to, Box<? extends Toy> from) {
to.set(from.get());
}
제한된 와일드카드 선언을 갖는 제네릭 메소드
- 컴파일러는 컴파일 시 제네릭과 와일드카드 관련 정보를 지우는 Type Erasure 행위를 거치게 됨
- 그러므로 메소드를 오버 로딩할 때 매개변수 동일할 경우 오버로딩 성립이 불가능함
// 오버로딩 인정 안됨
public static void outBox(Box<? extends Toy> box) { ... } // Box box
public static void outBox(Box<? extends Robot> box) { ... } // Box box
// 오버로딩 인정 됨
public static void inBox(Box<? super Toy> box, Toy n) { ... } // Box box, Toy n
public static void inBox(Box<? super Robot> box, Robot n) { ... } // Box box, Robot n
public static <T> void outBox(Box<? extends T> box) { ... }
public static <T> void inBox(Box<? super T> box, T n) { ... }
제네릭 인터페이스의 정의와 구현
- 클래스 또는 메소드만 제네릭으로 정의하였지만 인터페이스 역시 제네릭으로 정의할 수 있음
- 제네릭 인터페이스를 구현할 때 T를 결정한 상태로 구현할 수도 있음
interface Getable<T> {
public T get();
}
class Box<T> implements Getable<T> { // class Box<T> implements Getable<String>
private T ob;
public void set(T o) {
ob = o;
}
@Override
public T get() { // public String get()
return ob;
}
}
class GetableGenericInterface {
public static void main(String[] args) {
Box<Toy> box = new Box<>();
box.set(new Toy());
Getable<Toy> gt = box; // Getable<String> gt = box;
System.out.println(gt.get());
}
}