이 글에서는 java의 generic에 대해 다룬다.
Generic
Generic이란 java에서 class, interface 또는 method를 정의할 때 type을 pameter화하는 것이다.(type을 parameter로 사용해 일반화하는 것이다.) Generic은 compile time에 object type을 확인하기 때문에 casting을 줄여주고 type 안정성을 보장받을 수 있다, type별로 작성해야 하는 코드의 양을 크게 줄여준다.
사용 방법 : class<T>, interface<T>
아래와 같이 class나 interface를 선언 시 이름 뒤에 <> keyword를 이용한다. 1개 이상의 type을 넣을 수 있다.
method의 parameter type이나 return type으로 해당 generic을 사용할 수 있다. 아래 예시의 GenericInterface 부분과 같이 사용한다.
// GenericExample.java
public class GenericExample<T, B> {
private T data;
void setData(T data){
this.data = data;
}
void printData(){
System.out.println(this.data);
}
}
interface GenericInterface<T>{
T printData(T data);
}
Generic이 적용된 class를 정의할 때는 선언할 때 했던 것처럼 이름 뒤에 <> keyword를 붙이고 어떤 type을 사용할지 결정한다. 유의할 점은 primitive type은 넣을 수 없다는 것이다.
// App.java
public class App {
public static void main(String[] args) throws Exception {
GenericExample<Integer, Integer> intGenericExample = new GenericExample<Integer, Integer>();
intGenericExample.setData(10);
intGenericExample.printData(); // 10
GenericExample<String, String> stringGenericExample = new GenericExample<String, String>();
stringGenericExample.setData("hello");
stringGenericExample.printData(); // hello
}
}
Generic의 polymorphism (upcasting)
Generic의 type에 넣는 것들에 대해 polymorphism이 작동한다.
// App.java
class Parent{}
class Child extends Parent{}
public class App {
public static void main(String[] args) throws Exception {
ArrayList<Parent> arr = new ArrayList<Parent>();
arr.add(new Child()); // upcasting
arr.add(new Parent());
}
}
유의점
static은 generic을 사용할 수 없다.
generic은 class를 정의할 때 type을 넣어주는 방식이다. 그러나 static은 class가 정의되기 전부터 존재하는 값이므로 그 type을 가지고 있지 못한다.
// GenericExample.java
public class GenericExample<T> {
static T staticAttribute; // compile error : type error
// ...
}
generic type object는 생성이 불가하다
// GenericExample.java
public class GenericExample<T> {
GenericExample(){
T object = new T(); // compile error : Cannot instantiate the type T
}
}
Type 제한과 wildcard
generic은 extends나 super keyword를 이용해 generic type에 오는 것들을 제한할 수 있다.
wildcard는 아무 type이나 올 수 있는 자리를 말하며, ?로 표기한다. 그러나 wildcard는 class나 interface 내부에서 접근할 수 없기 때문에 유의해 사용해야 한다.
- <?> : 아무 type이 올 수 있다.
- <? extends T> : T type과 T를 상속하는 type만이 올 수 있다.
- 외부에서 온 data에 값을 추가한다면 이 방식을 사용하는 것이 좋다.
- <? super T> : T type과 T가 상속하는 type만이 올 수 있다.
- 외부에서 온 data를 사용한다면 이 방식을 사용하는 것이 좋다.
<T>와 <?>의 차이
<T>라고 표기할 경우, 해당 type을 class나 interface 내부에서 다시 불러올 수 있다. 대표적인 예시로 method return type이나 parameter로 사용할 수 있다.
그러나 <?>라고 표기할 경우 해당 type이 무엇인지 class 내부에서 알 수 없기 때문에 사용할 수가 없다.
Generic의 장점
적은 casting과 compile-time type check
아래 예시를 보자.
List list = new ArrayList();
list.add("hello");
String result = (String) list.get(0); // type 변환 필요
List<String> list = new ArrayList<String>();
list.add("hell0");
list.add(0); // compile error : type error
String result = list.get(0); // type 변환 필요 없음
위쪽 코드와 같이 generic을 사용하지 않는다면 모든 object를 개발자가 관리해야 한다. 그렇지만 이 부분을 빼먹으면 오류가 나기 십상이고, 또 매 번 casting을 하면 필요없는 연산을 하기 때문에 성능도 저하될 것이다.
반면 generic을 사용한다면 해당 object가 어떤 type인지 알기 때문에 type을 변환하지 않아도 된다. 만약 type이 잘못 지정되었을 경우 compiler가 해당 오류를 잡아준다.
코드 재사용성 증가
위에서 작성한 GenericExample 부분을 보자. 저 코드에서는 간단한 내용만 들어가지만 만약 Integer형, Double형, String형 등 많은 type으로 GenericExample을 작성해야 한다면 어떨까? Generic이 없다면 type별로 해당 코드를 작성해야 할 것이다. 요구사항이 변한다면 그 모든 코드를 수정해야 한다!
Generic의 사용으로 type을 묶어버리기 때문에 Integer형, Double형, String형, ... 아무리 많은 type을 사용하더라도 <T>로 묶어서 하나의 class로 표현할 수 있다.
Generic type 소거와 heap pollution
Generic이 도입되기 이전 코드와의 호환성을 위해 generic은 compile time까지만 type을 검사해 주고, compile 이후에는 generic type이 사라지게 된다. 실제로 .class 파일을 보면 <T>와 같이 입력한 type이 있는 게 아니라 모든 class의 상위 class인 Object로 표기한다.
- <?>로 표기한 generic은 Object로 치환된다.
- <? extends T>로 표기한 generic은 T로 치환된다.
따라서 아래와 같은 경우에는 runtime exception이 발생한다.
// App.java}
public class App {
public static void main(String[] args) throws Exception {
ArrayList<String> stringList = new ArrayList<>();
stringList.add("string 1");
stringList.add("string 2");
Object obj = stringList;
ArrayList<Integer> intList = (ArrayList<Integer>) obj;
intList.add(1);
intList.add(2);
for(Integer n : intList) {
System.out.println(n); // ClassCastException : String cannot be cast to Integer
}
}
}
StringList를 Object로 upcasting하고, Object를 IntegerList로 downcasting했다. 이것은 compile 상에서는 오류가 없다. 그러나 실제로는 Integer 값이 있어야 하는 intList에 String이 들어가 있는 상황이다. 이러한 상황을 heap pollution이라 한다. 이런 일이 발생하는 이유가 compile 이후에 generic이 소거되기 때문이다.
이를 막기 위해서는 Collection class의 checkedist()를 사용하면 된다.
'Development > Java' 카테고리의 다른 글
[Java] String vs StringBuffer vs StringBuilder (0) | 2023.03.04 |
---|---|
[Java] Primitive Wrapper Class (0) | 2023.03.04 |
[Java] Java Virtual Machine (0) | 2023.03.01 |
[Java] Exception Handling (0) | 2023.03.01 |
[Java] abstract class vs interface (0) | 2023.02.27 |