예시를 통해 이해해보자.
public class CastingDTO {
private Object object;
public void setObject(Object object) {
this.object = object;
}
public Object getObject() {
return object;
}
}
public class GenericSample {
public static void main(String[] args) {
GenericSample sample = new GenericSample();
sample.checkCastingDTO();
}
private void checkCastingDTO() {
CastingDTO dto1 = new CastingDTO();
dto1.setObject(new String());
CastingDTO dto2 = new CastingDTO();
dto2.setObject(new StringBuffer());
CastingDTO dto3 = new CastingDTO();
dto3.setObject(new StringBuffer());
}
}
setter 메서드 매개변수 타입이 Object 로 자동 형변환 덕분에 setter 를 호출할 때 매개변수로 어떤 타입이든 넘겨줄 수 있다.
다만, getter 를 사용할 땐 문제가 된다. getter 의 리턴 타입이 Object 여서 instanceof
연산자로 타입을 확인해야 한다.
private void checkDTO(CastingDTO dto) {
Object object = dto.getObject();
if (object instanceof String) {
System.out.println("String type");
} else if (object instanceof StringBuilder) {
System.out.println("StringBuilder type");
} else {
System.out.println("StringBuffer type");
}
}
이렇게 매번 타입을 점검해야 할까? 또한 개발자 실수로 타입 캐스팅을 잘못 선언할 수 있지도 않을까?
이런 단점을 보완하기 위해 Java 5 부터 Generic 이란것이 도입되었다.
public class GenericCastingDTO<T> implements Serializable {
private T object;
public T getObject() {
return object;
}
public void setObject(T object) {
this.object = object;
}
}
GenericCastingDTO<String> dto = new GenericCastingDTO<>();
dto.setObject("String");
GenericCastingDTO<StringBuffer> dto1 = new GenericCastingDTO<>();
dto1.setObject(new StringBuffer());
GenericCastingDTO<StringBuilder> dto2 = new GenericCastingDTO<>();
dto2.setObject(new StringBuilder());
// 제네릭을 사용하면 형 변환이 필요없다.
StringBuilder builder = dto2.getObject();
StringBuffer buffer = dto1.getObject();
String str = dto.getObject();
매개변수 타입을 Object로 선언했을 때와 달리 제네릭을 사용하면 잘못된 타입으로 치환하더라도 컴파일 자체가 되지 않는다.
class name<T1, T1, ... T> {
}
클래스 선언부의 클래스 이름 뒤에 <> 꺾쇠 사이에 타입 파라미터를 나열하면 된다.
Box<Integer>
Box<Integer> box = new Box<>();
public class Box<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
<>
꺽쇠를 Diamond 라 칭한다.
<> 안에 타입 파라미터를 콤마로 나열하면 된다.
public class Pair<K, V> {
private K k;
private V v;
public Pair(K k, V v) {
this.k = k;
this.v = v;
}
public K getK() {
return k;
}
public V getV() {
return v;
}
}
입력된 매개변수 타입을 보고 컴파일러가 type argument 의 타입을 추론한다.
Pair<Integer, String> value = new Pair<>(1, "value");
Pair<String, Integer> key = new Pair<>("key", 1);
타입 파라미터를 parameterized type 로 치환할 수도 있다.
Pair<Integer, ArrayList<Integer>> pair = new Pair<>(1, new ArrayList<Integer>());
raw types 은 제네릭 클래스를 사용할 때 type argument 를 제거하고 사용하는 것이다.
Box box = new Box<>();
언뜻보면, 일반 클래스처럼 사용하는 것 같지만 제네릭 클래스를 위와같이 사용하는 경우를 raw types 이라한다.
raw types 형태로 사용하는 경우가 등장하는데 그 이유는 아래와 같다.
Raw types show up in legacy code because lots of API classes (such as the Collections classes) were not generic prior to JDK 5.0.
raw types 형태로 사용하면 아래 코드는 문제없이 실행된다.
컴파일러가 타입 추론을 못하기 때문에 컴파일 시 타입 안정성을 보장하지 못한다.
Box rawBox2 = new Box();
rawBox2.set("S");
System.out.println(rawBox2.get());
rawBox2.set(1);
System.out.println(rawBox2.get());
메서드를 제네릭하게 선언할 수 있다.
매개 변수로 사용된 객체에 값을 추가할 수 있다.
public class GenericList {
public <T> void add(List<T> list, T value) {
list.add(value);
}
}
public static void main(String[] args) {
GenericList genericList = new GenericList();
ArrayList<Pair<String, Integer>> arrayList = new ArrayList<>();
genericList.add(arrayList, new Pair<>("key1", 1));
genericList.add(arrayList, new Pair<>("key2", 1));
}
타입 파라미터 T 에 어떤 타입이든 치환할 수 있다.
Bounded Type Parameters 란 type argument 에 타입의 범위를 지정하는 것이다.
Box<Integer> box = new Box<>();
Box<String> box = new Box<>();
Box<Double> box = new Box<>();
Box<Long> box = new Box<>();
...
public class Box<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
방법은 타입 파라미터에 extends 타입
을 지정하면 된다.
public <U extends Number> void inspect(U u) {
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
U u
매개변수는 Number 의 하위 클래스만 올 수 있다.
아래처럼 문자열을 매개변수로 넘기면 컴파일 에러로 잡을 수 있다.
public static void main(String[] args) {
Box<Integer> box = new Box<>();
box.inspect("234");
}
타입 범위를 지정할 때 여러 타입을 나열할 수 있다. & 기호로 나열하면 된다.
<T extends B1 & B2 & B3>
타입 추론이란 컴파일러가 제네릭 클래스나 제네릭 메서드가 사용될 때 타입을 추론하여 결정하는 것을 말한다.
제네릭에서 wildcard 는 ?
키워드로 표시하는데 unknown type 을 의미한다.
와일드 카드를 사용하면 좋은 경우는
printList
메서드 목적은 toString을 호출해 출력하는 것이다.
문제는 Object 타입의 원소만 처리 가능하므로 List, List등 다양한 타입의 List를 매개변수로 받을 수 없다.
public static void printList(List<Object> list) {
for (Object elem : list) {
System.out.println("elem = " + elem);
}
}
public static void main(String[] args) {
List<Car> cars = List.of(
new Car("BMW"),
new Car("Porsche")
);
// 컴파일 에러 발생함.
printList(cars);
}
와일드카드를 사용하면 다양한 타입을 받을 수 있다.
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.println("elem = " + elem);
}
}
type erasure란 컴파일러가 제네릭 파라미터를 실제 타입 또는 bridge 메서드로 교체하는 과정을 뜻한다.
type erasure 를 사용하면 컴파일러가 추가 클래스 생성이 필요하지 않고, 런타임 오버헤드가 발생하지 않음을 보장한다.
Type erasure 예시 코드
package org.flab.chapter21;
/**
* @author gutenlee
* @since 2023/07/02
*/
public class Node<T> {
public T data;
public Node(T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
package org.flab.chapter21;
/**
* @author gutenlee
* @since 2023/07/02
*/
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);
}
}
myNode 변수를 부모 타입으로 형변환 한뒤 setData()
매개변수로 String 값을 전달하면
String 을 Integer 로 형변환할 수 없다는 ClassCastException
발생한다.
public static void main(String[] args) {
MyNode myNode = new MyNode(3);
Node n = myNode;
n.setData("Hello"); // -> Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
Integer x = myNode.data;
}