Generic이란?
Java의 generic은 형변환시 발생할 수 있는 문제를 사전에 없애기 위해 만들어졌습니다.
파라미터 타입이나 리턴 타입을 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정됩니다.
타입을 파라미터화해서 컴파일 시 구체적인 타입이 결정되어 컴파일시에 미리 타입을 강하게 체크해 런타입에러를 사전에 방지해줍니다. 즉, 타입을 미리 지정하는 것이 아닌 필요에 의해 지정할 수 있도록 합니다.
public class ClassName<T> {...}
public interface InterfaceName<T> {...}
위와 같이 클래스 또는 인터페이스 이름 뒤에 <> 부호를 붙여 사용하며, 이 <>를 제너릭 타입이라고 하며 <> 사이에 타입 파라미터가 위치합니다.
Generic 타입
선언시 클래스 또는 인터페이스 이름 뒤에 붙으며, <> 안에는 클래스와 같이 구체적인 타입을 지정해줘야 하며, 어떤 타입이 들어가도 상관은 없지만, Java에서 정한 기본 규칙이 있어 코드의 가독성을 위해 되도록 따라주는 것이 좋습니다.
- E: 키
- K: 키
- N: 숫자
- T: 타입
- V: 값
- S, U, V: 두 번째, 세 번째, 네 번째에 선언된 타입
generic 타입을 선언할 때는 파라미터는 변수명과 동일한 규칙에 따라 작성하며, 일반적으로 대소문자 알파벳 한 글자로 표현합니다.
제한된 타입 파라미터(<T extends 최상위 타입>)
타입 파라미터에 지정되는 타입을 특정 타입(ex. Number) 또는 하위 클래스 타입(e. Byte, Short, Integer, Long, Double)만을 받아야 하는 경우 제한된 타입 파라미터를 사용합니다.
타입 파라미터 뒤에 extends 키워드를 붙이고 상위 타입을 명시합니다. 상위 타입은 클래스 뿐만 아니라 인터페이스도 가능합니다.
public <T extends Number> int compare(T t1, T t2) {...}
와일드카드(<?>, <? extends ..>, <? super ..>
- <?> : 제한 없음, 모든 클래스나 인터페이스 타입이 올 수 있습니다.
- <? extends 상위클래스> : 상위 클래스 제한
- <? super 하위클래스> : 하위 클래스 제한
클래스 generic 선언
public class CastingDla implements Serializable{
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 GenericSampleO;
sample. checkCastingDTO();
}
public void checkCastingDTO() {
CastingDTO dto1=new CastingDTO();
dto1. setObject(new String());
CastingDTO dt02=new CastingDTO();
dt02. setObject(new StringBuffer());
CastingDTO dt03=new CastingDTO();
dto3. setObject(new StringBuilder());
}
}
checkCastingDTO() 메소드를 살펴보면 dto1, dto2, dto3 객체를 생성하고 String, StringBuffer, StringBuilder 객체를 각각 지정했습니다. setObject() 메소드는 Object 타입을 매개 변수로 받고 있어 어떤 참조 자료형을 넘겨도 상관이 없습니다.
하지만, getObject() 메소드를 호출했을 때, 리턴값으로 넘어오는 타입 또한 Object 타입으로 리턴값을 받는 자료형에 맞게 형변환을 해야합니다.
String temp1=(String)dto1 .getObject();
StringBuffer temp2=(StringBuffer)dt02.getObject();
StringBuilder temp3=(StringBuilder)dt03 .getObject();
이때, 인스턴스 변수의 타입을 명확히 알고 있다면 문제가 없지만 변수의 타입을 헷갈리게 되면 런타임에러가 발생할 수 있습니다.
이러한 경우에 instanceof를 사용해 타입을 점검할 수 있지만 코드가 복잡해지고 가독성이 떨어질 수 있습니다.
public void checkDTO(CastingDTO dto) {
Object tempObject=dto.getObject();
if (tempObject instanceof StringBuilder) {
System.out.println("StringBuilder");
} else if (tempObject instanceof StringBuffer) {
System.out.println("StringBuffer");
}
}
import java. io.Serializable;
public class CastingGenericDTO<T> implements Serializable{
private T object;
public void setObject(T obj) {
this.object=obj;
}
public T getObject() {
return object;
}
}
위에서 사용했던 클래스를 제너릭(generic)으로 선언했습니다.
public void checkGenericDTO() {
CastingGenericDTO<String> dtol=new CastingGenericDTO<String>();
dto1.setObject(new String());
CastingGenericDTO<StringBuffer> dt02=new CastingGenericDTO<StringBuffer>();
dt02.setObject(new StringBuffer());
CastingGenericDTO<StringBuilder> dto3=new CastingGenericDTO<StringBuilder>();
dt03.setObject(new StringBuilder());
}
객체를 생성할 때 <> 안에 각 타입을 명시해서 참조 자료형을 넘기고 있습니다.
String templ=dto1 .getObject();
StringBuffer temp2=dto2.getObject();
StringBuilder temp3=dto3.getObject();
위와 같이 제너릭 타입으로 각각 String, StringBuffer, StringBuilder를 명시해주면 getObject로 값을 받아올 때 형변환을 할 필요가 없습니다. 잘못된 타입으로 치환하면 컴파일 자체가 안 되기 때문에 실행시에 다른 타입으로 형변환하여 예외가 발생하는 일은 없게 됩니다.
메소드 generic 선언
generic 메소드는 메소드의 선언 부에 적은 generic으로 리턴 타입, 파라미터의 타입이 정해지며, 클래스와 다르게 메소드를 generic 선언할 대는 반환타입 이전에 <> generic 타입을 선언합니다.
public <T> T genericMethod(T o) {...}
public <타입 파라미터, ...> 리턴타입 메소드명 (매개변수, ...) {...}
generic 메소드를 호출하는 방법은 호출할 때 타입을 지정하는 방법과 암묵적 호출의 두 가지 방법이 있습니다.
public class Car<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
public class SportsCar {
public static<T> Car<T> getNewCar(T t) {
Car<T> car = new Car<T>();
car.setT(t);
return car;
}
}
public class Main {
public static void main(String[] args) {
Car<Integer> car1 = SportsCar.<Integer>getNewCar(100);
Car<String> car2 = SportsCar.getNewCar("암묵적호출");
}
}
첫 번째 방법과 같이 타입을 지정해 호출을 할 때 컴파일러는 <Integer>를 보고 타입을 지정합니다. 그러나 암묵적 호출의 경우 매게변수 타입이 String인 것을 보고 컴파일러가 타입을 추정합니다.