들어가기 전
에러가 발생하는 시간을 기준으로 컴파일 에러와 런타임 에러로 나눌 수 있다.
둘 중에 어떤 게 더 치명적일까?
컴파일 에러는 IDE가 바로 알려주기 때문에 고치기 쉽다.
반면 런타임 에러는 그 코드가 실제 사용되기 전까지는 발견하기가 어렵다.
게다가 그 에러로 인해 실제 데이터에 영향을 준다면 더 치명적일 것이다.
런타임 에러가 발생할 여지가 있는 코드를 컴파일 단계에서 검사를 한다면 치명적인 에러를 줄일 수 있다.
제네릭이란
클래스, 인터페이스, 메서드를 정의할 때 타입(클래스 및 인터페이스)을 매개변수로 사용하는 기능
사용하는 이유?
1. 컴파일 시 타입 검사를 해 코드의 안정성을 높임
2. 형변환 번거로움이 줄어듦
여기까지만 보면 설명이 추상적이라 명확하게 이해가 되지 않을 수 있다.
제네릭이 사용되지 않은 코드와 사용된 코드를 비교해 보면 이해에 도움이 될 것이다
제네릭이 사용되지 않은 코드
Box: 과일을 담고, 돌려주는 박스
Apple, Banana: 과일
public class Box {
Object item;
public void setItem(Object item) {
this.item = item;
}
public Object getItem() {
return item;
}
}
public class Apple {
public void printApple() {
System.out.println("apple");
}
}
public class Banana {
public void printBanana() {
System.out.println("banana");
}
}
@Test
void test() {
Box box = new Box();
box.setItem(new Banana()); // Box에 Banana 객체를 집어넣는다
Apple apple = (Apple) box.getItem(); // Box에서 꺼내어 Apple로 형변환을 한다
apple.printApple(); //apple 클래스에만 있는 printApple 메서드를 호출한다
// 컴파일 에러는 나타나지 않았다
}
컴파일 에러가 나타나지 않았으므로 테스트를 실행해 본다.
테스트 결과
java.lang.ClassCastException: class Banana cannot be cast to class Apple
Banana는 Apple로 형변환 할 수 없다는 ClassCastException 에러가 나타났다.
ClassCastException은 RuntimeException을 상속했다.
앞서 말한 런타임 에러가 발생한 것이다.
제네릭을 사용한 코드
public class Box<T> {
T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
컴파일 에러 발생
Inconvertible types; cannot cast 'Banana' to 'Apple' 메시지가 나온다.
컴파일 에러를 수정하여 Box에서 Banana를 꺼내본다.
(Banana) box.getItem(); 로 형변환할 필요가 없이 바로 Banana 객체가 꺼내진다.
제네릭을 사용하여 1. 기존의 런타임 에러를 컴파일 단계에서 검사할 수 있고 2. 별도의 형변환을 할 필요가 없어졌다
이러한 특징으로 여러 타입을 혼용하여 담을 경우에 유용하게 쓰인다. 대표적으로 자바 컬렉션이 있다.
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
제한된 제네릭 타입 파라미터 Bounded Type Parameters
- <T extends ..>를 사용하면 특정 클래스의 자식 타입 또는 특정 인터페이스의 구현 타입만 매개변수로 사용할 수 있다
- extends라서 오해할 수 있지만 인터페이스의 구현체도 사용 가능하다
public class Box<T extends Fruits> {
private List<T> list = new ArrayList<>();
public void add(T t) {
list.add(t);
}
}
public static void main(String[] args) {
Box<Fruit> box = new Box<>();
box.add(new Apple());
box.add(new Banana());
}
와일드카드
1. 제네릭 클래스가 아닌 경우
2. static 메서드(아래에서 언급하지만 static에는 제네릭 매개변수를 사용할 수 없다) 경우
위 두 경우에도 제네릭을 사용할 수 있다. 바로 와일드카드 기능 덕분이다.
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = '';
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
}
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
...
System.out.println(Juicer.makeJuice(fruitBox)); //OK. FruitBox<Fruit>
System.out.println(Juicer.makeJuice(appleBox)); //에러. FruitBox<Apple>
// 그럼 오버로딩으로 이 문제를 해결할 수 있을까?
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = "";
for (Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
static Juice makeJuice(FruitBox<Apple> box) {
String tmp = "";
for (Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
// 컴파일 에러 발생
// 제네릭 타입이 다른 것만으로는 오버로딩에 성립하지 않는다
와일드 카드 사용
- ?를 사용한다
- ?만 단독으로 사용하면 Object와 같기 때문에 extends로 상한, super로 하한 조건을 같이 사용한다
- <? extends T> 와일드카드의 상항 제한. T와 그 자식들만 가능
<? super T> 와일드카드의 하한 제한. T와 그 부모들만 가능
와일드카드를 사용한 코드
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for (Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
<T extends .. >와 다른 점이 뭐냐? 와일드카드는 참조가 불가능하기 때문에 메서드에서 참조될 수 없다.
제네릭 타입 제거 Type Erasure
- 컴파일러는 제네릭 타입을 이용해 소스파일을 검사하고
- 필요한 곳에 형변환 코드를 넣어주고
- 제네릭 타입을 제거한다
- 즉, 컴파일된 .class 파일엔 제네릭이 없다!
아래는 제네릭 타입 파라미터를 제거하는 규칙이다.
- 제네릭 타입 파라미터가 상한이 있는 경우에는 타입 파라미터를 부모 타입으로
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
// 컴파일러가 Type Erasure를 한다면
// 아래와 같은 코드로 바뀌는 셈이다
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
- 제네릭 타입 파라미터의 상/하한이 없는 경우에는 타입 파라미터를 Object로 변환
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
// 컴파일러가 Type Erasure를 한다면
// 아래와 같은 코드로 바뀌는 셈이다
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
- type-safety를 유지하기 위해 필요한 경우 타입 캐스팅을 사용
- 제네릭 타입을 상속받은 클래스에서는 다형성을 유지하기 위해 브릿지 메서드*를 생성
브릿지 메서드?
제네릭 클래스 Node와 Node를 상속받은 클래스 MyNode가 있다.
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
Type Erasure 후
- 이 경우 Mynode의 setData(Integer data)와 Node의 setData(Object data)의 매개변수가 다르다
- 오버라이딩이라고 볼 수 없다
public class Node<Object> {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
// 이 경우 Mynode의 setData(Integer data)와 Node의 setData(Object data)의 매개변수가 다르다
MyNode mn = new MyNode(5);
Node n = mn;
n.setData("Hello"); // Causes a ClassCastException to be thrown.
Integer x = mn.data;
브릿지 메서드 생성 후
- 위 문제를 해결하기 위해 브릿지 메서드가 생성된다
- 브릿지 메서드에는 Object로 받은 객체를 Integer로 형변환한 뒤에 setData(Integer data) 메서드를 호출하는 로직이 들어있다
class MyNode extends Node {
// Bridge method generated by the compiler
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
주의 사항
- static 키워드에는 제네릭스를 사용할 수 없다
제네릭스는 인스턴스 변수로 취급되기 때문이다 - 제네릭의 매개변수명을 임의로 정할 순 있지만 관습적으로 사용되는 것들이 있다
(T: Type. 보통의 경우, E: Element. 리스트의 요소일 경우, K: Key, V: Value)
참고
<자바의 정석>
https://docs.oracle.com/javase/tutorial/java/generics/why.html
'Study > Java' 카테고리의 다른 글
@ExceptionHandler는 어떻게 예외를 처리할 수 있을까? (0) | 2023.07.20 |
---|---|
자바의 함수형 프로그래밍 전략, 메서드 참조 (0) | 2023.05.04 |
JVM 명세 - Run-Time Data Areas (0) | 2022.06.25 |
자바로 간단한 http 웹 서버 구현 (0) | 2022.06.11 |
간단한 자바 TCP 통신 구현 (0) | 2022.06.02 |