※ 본문은 혼자 공부한 내용을 기록한 글입니다. 오개념이 있다면 댓글로 알려주세요!
[ 1 ] 기본 개념
제네릭(Generic): 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법이다. C++에 익숙한 사람이라면 template가 떠오를 것이다(단, 역할은 비슷하지만, 차이점도 많다!)
class Student<T> {
public T info;
}
Student<String> s1 = new Student<String>();
Student<Integer> s2 = new Student<Integer>();
위의 <T>가 Generic이다. Student라는 class를 정의하는 시점에서는 info의 DataType을 명시적으로 지정하지 않고 있다가, s1이라는 Student의 인스턴스를 Student<String>으로 만들어낼 때 info가 String이라는 DataType을 갖게 된다. 즉, <>을 통해 외부에서 클래스 내부에서 사용할 데이터 타입을 지정할 수 있는 것이다.
s1과 마찬가지로 s2는 <Integer>로 선언했으므로 s2의 info는 Integer라는 DataType을 갖게 된다.
- 사용 시 유의할 점
(1) 인스턴스 생성 시 반드시 타입을 명시해야 한다. 단, 생성자 매개변수를 넘겨줄 경우에는 매개변수를 통해 데이터 타입을 유추할 수 있으므로 생략가능하다. |
(2) Generic에는 기본형 데이터 타입을 제외한 참조형 데이터 타입만 올 수 있다. 기본형 데이터 타입을 사용하려면 Wrapper class를 사용한다. |
(3) class Student<T, S> 처럼 복수의 Generic을 사용할 수 있다. |
[ 2 ] Generic을 사용하는 이유?
(1) 코드를 간결하게 작성할 수 있다.
ArrayList al = new ArrayList();
al.add("generic");
al.add("test");
System.out.println((String)al.get(0));
System.out.println((String)al.get(1));
Generic을 사용하지 않을 시, 일일이 casting을 해줘야 한다.
ArrayList<String> al = new ArrayList<String>();
al.add("generic");
al.add("test");
System.out.println(al.get(0));
System.out.println(al.get(1));
반면, Generic 사용 시 casting을 할 필요가 없다.
(2) 타입의 안정성이 높아진다.
ArrayList al = new ArrayList();
al.add("generic");
al.add("test");
al.add(5); // 컴파일 오류 발생하지 않는다.
System.out.println((String)al.get(0));
System.out.println((String)al.get(1));
System.out.println((String)al.get(2)); // 런타임 오류 발생
Generic을 사용하지 않으면, 위처럼 내가 의도하지 않은 DataType이 들어갈 수도 있다. 하지만, 위 코드는 정상적으로 compile은 되고 런타임 오류가 발생하는데, 런타임 오류는 컴파일 오류에 비해 발견하기 어렵고 프로그램의 안정성을 낮춘다. 또한, 시간이 지나고 나서 자신이 작성한 코드의 의도를 알기 어려워 진다는 문제도 있다.
ArrayList<String> al = new ArrayList<>();
al.add("generic");
al.add("test");
al.add(5); // 컴파일 오류 발생!
반면, Generic을 사용하면 내가 의도하지 않은 DataType이 들어왔을 때 컴파일 오류가 발생한다. 즉, 타입의 안정성을 높여준다.
[ 3 ] 추가 내용
(1) Generic은 class뿐만 아니라 method에도 활용 가능하다.
class Student<T, S>
{
public T info;
public S id;
Student(T info, S id)
{
this.info = info;
this.id = id;
}
public <U> void printInfo(U info)
{
System.out.println(info);
}
}
별 의미없는 method이지만, 이해를 돕기 위해 작성했다.
(2) Generic을 사용할 경우 모든 데이터 타입이 들어올 수 있으므로 복잡해질 수 있다. 이를 방지하기 위해 특정 클래스 및 특정 클래스의 자식 클래스만 제네릭의 타입으로 들어갈 수 있도록 "제한"할 수 있다.
class A{
public int rank;
}
class B extends A{
public int info;
}
class C<T extends A>
{
public int test;
}
public class generic_practice2 {
public static void main(String[] args) {
C<B> data = new C<B>();
C<A> data2 = new C<A>();
C<String> data3 = new C<String>(); // 컴파일 에러
}
}
class C의 Generic이 <T extends A>로 설정되어 있다. 이는 class A와 부모 클래스가 A인 자식 클래스만 C의 데이터 타입으로 들어갈 수 있음을 의미한다. 그 이외의 클래스로 C의 인스턴스를 생성할 경우 컴파일 에러가 발생한다. 위 코드에서 C의 인스턴스를 String으로 생성할 때 컴파일 에러가 발생하는 것이 그 예시이다.
'Programming > Java' 카테고리의 다른 글
[Java] 추상 클래스(abstract class)와 인터페이스(interface) (1) | 2022.08.11 |
---|---|
[Java] 패키지(Package) (0) | 2022.08.05 |
[Java] Overriding, Overloading (0) | 2022.08.04 |
[Java] 생성자 및 상속에서의 생성자 (0) | 2022.08.02 |
[Java] 클래스(class)의 구성과 클래스 멤버 및 인스턴스 멤버 (1) | 2022.08.01 |