제네릭은 타입 형 변환에서 발생할 수 있는 문제점을 사전에 없애기 위해서 만들어졌다. 사전이라는 것은 실행 시에 예외가 발생하는 것을 처리하는 게 아니라 컴파일시에 점검할 수 있도록 한 것을 말한다.
아래처럼 DTO 클래스를 선언하였다.
package javaonly.d;
import java.io.Serializable;
public class CastingDTO implements Serializable {
private Object object;
public void setObject(Object object) {
this.object = object;
}
public Object getObject() {
return object;
}
}
object 필드는 Object 클래스이므로 어떤 객체든 올 수 있을 것이다.
그리고 이 CastingDTO를 사용하는 GenericSample이라는 클래스를 만들었다.
package javaonly.d;
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 StringBuilder());
}
}
코드를 보면 dto1, 2, 3에 각각 다른 타입의 객체를 object 필드에 저장하였다. CastingDTO의 object는 Object 타입이므로 문제 없이 컴파일이 된다. 이런 상황에서 dto1, 2, 3에서 getObject()로 객체를 받아오고 싶다면
String s1 = (String)dto1.getObject();
StringBuffer s2 = (StringBuffer)dto2.getObject();
StringBuilder s3 = (StringBuilder)dto3.getObject();
이렇게 각각을 형변환 해주어야 한다. (형변환 해주지 않으면 에러가 발생한다. getObject()는 Object 타입의 인스턴스를 리턴하는데 그걸 자식 클래스인 String에 담으려고 하기 때문. instanceof로 확인해봤을 때, String은 Object이지만, Object는 String이 아니다.)
그런데 dto1, 2, 3에 어떤 타입의 객체를 넣었었는지 기억하지 못한다면? if문과 instanceof 키워드로 일일이 확인해보는 방법이 있을 것이다. 하지만 이는 불편하다. 이런 단점을 제네릭이 보완해준다.
아래 코드는 위에서 본 CastingDTO를 제네릭으로 선언한 것이다.
package javaonly.d;
import java.io.Serializable;
public class CastingDTO<T> implements Serializable {
private T object;
public void setObject(T object) {
this.object = object;
}
public T getObject() {
return object;
}
}
T는 아무 이름이나 지정해도 된다. 단, 되도록이면 클래스 이름의 명명 규칙과 동일하게 지정해주는 것이 좋다. <>안에 쓴 것(여기서는 T)을 타입 이름처럼 클래스 안에서 사용하면 된다. 정리하자면 <> 안의 이름은 가상의 타입이름이다.
GenericSample.java도 다음과 같이 고쳐주자.
package javaonly.d;
public class GenericSample {
public static void main(String[] args) {
GenericSample sample = new GenericSample();
sample.checkCastingDTO();
}
private void checkCastingDTO() {
CastingDTO<String> dto1 = new CastingDTO<String>(); // 바뀐 부분
dto1.setObject(new String());
CastingDTO<StringBuffer> dto2 = new CastingDTO<StringBuffer>(); // 바뀐 부분
dto2.setObject(new StringBuffer());
CastingDTO<StringBuilder> dto3 = new CastingDTO<StringBuilder>(); // 바뀐 부분
dto3.setObject(new StringBuilder());
String s1 = dto1.getObject();
StringBuffer s2 = dto2.getObject();
StringBuilder s3 = dto3.getObject();
}
}
이렇게하면 getObject()를 통해 인스턴스를 리턴 받을 때에도 형변환을 해주지 않아도 잘 실행된다.
String s1 = dto2.getObject();
위처럼 잘못된 타입에 받으려고 하면(s1은 String, dto2의 object는 StringBuffer) 에러가 뜨면서 컴파일이 되지 않는다. 이렇게 타입 형변환에서 발생할 수 있는 문제점을 런타임이 아닌 컴파일 시점에 해결하기 위한 것이 제네릭의 기본 쓰임이다.
Genceric의 <> 안에 어떤 이름이 들어가든 상관없지만, 자바에서 정의한 규칙이 존재한다. 다른 개발자가 보더라도 쉽게 이해할 수 있도록 이 규칙을 준수하는 것이 좋다.
Integer, Long 등의 wrapper 클래스는 각각 Comparable<Integer>, Comparable<Long>을 implement한다.