제네릭 클래스는 자료형을 <T> 타입 변수로 대체한다. 때문에 클래스가 선언될 때 까지는 자료형 결정이 안 되었다가 객체가 생성될 때 결정이 된다.
제네릭 클래스를 작성하려면 타입 변수와 인자 리스트가 필요하다.
- T: 타입 변수 (메소드의 '매개 변수' 역할)
- <>: 인자 리스트 (메소드의 '()' 역할)
제네릭 클래스의 T는 타입 변수로, 이는 메소드의 '매개 변수' 역할을 하며, 인자 리스트는 <>로, 메소드의 '()' 역할을 한다.
타입 변수는 자료형을 전달하는 변수로서 자료형을 마치 매개 변수처럼 취급하고 반드시 참조형만 가능하다는 특징이 있다. 또 타입 변수는 대소문자를 가리지 않으며, aaa, bbb로 작성할 수도 있지만, 보통 1글자 대문자로 적는다.
💡제네릭 클래스의 특징
public class Ex_Generic {
public static void main(String[] args) {
Item<String> item1 = new Item<String>(); // String
item1.c = "문자열";
Item<Integer> item2 = new Item<Integer>(); // Integer
item2.c = 100;
Pen<Boolean> pen = new Pen<Boolean>();
pen.a = true;
pen.b = false;
pen.c = true;
Note<String,Integer> note = new Note<String,Integer>("Isaac", 20);
System.out.println(note.getA()); // String
System.out.println(note.getB()); // Integer
}
}
Item, Pen, Book, Note 제네릭 클래스를 생성하였다.
각각의 제네릭 클래스를 살펴보며 어떤 특징을 가지고 있는지 알아보도록 하자!
Item, Pen 제네릭 클래스
class Item<T> {
public int a;
public int b;
public T c;
}
class Pen<T> {
public T a;
public T b;
public T c;
}
제네릭 클래스는 자료형이 들어갈 곳에 타입변수를 적었다. 그래서 호출하는 자료형에 따라서 클래스 내의 T자료형은 String이 되기도 하고, Integer가 되기도 한다.
제네릭 클래스는 어떤 것이든 다 넣을 수 있다는 점에서 Object 변수와 비슷하지만 Object와의 차이점은 제네릭 클래스는 한 번 정하면 죽을 때까지 자료형이 고정된다는 점이다.
즉, 제네릭 클래스는 런타임 시에 한 번 자료를 픽스하는 순간 지속된다는 특징이 있다.
Book 제네릭 클래스
class Book<T> {
public T a; // 멤버 변수의 자료형
public void set(T a) { // 메소드 매개변수의 자료형
this.a = a;
T b; // 지역변수의 자료형
}
public T get() {
return this.a; // 메소드 반환타입
}
}
그렇다면 제네릭 클래스의 타입 변수는 어느 위치에 사용할 수 있을까?
멤버 변수의 자료형, 메소드 매개변수의 자료형, 지역변수의 자료형, 메소드 반환타입 등 자료형이 들어가는 부분은 모두 타입 변수를 쓸 수 있다.
그리고 지역변수의 자료형으로 사용할 수는 있으나 권장되지 않는다는 점에 주의하자.
Note 제네릭 클래스
class Note<T, U> {
public T a;
public U b;
public Note(T a, U b) {
this.a = a;
this.b = b;
}
public T getA() {
return this.a;
}
public U getB() {
return this.b;
}
}
타입 변수는 한 클래스 내에 여러 개 쓸 수 있다. 객체를 만들 때에도 타입 변수의 개수에 맞게 자료형을 써주면 된다. 그러나 타입변수는 많아야 2개, 보통은 1개를 쓰는 게 일반적이다.
💡제네릭 클래스의 활용
public class Ex_Generic {
public static void main(String[] args) {
// 기본 클래스
WrapperInt n1 = new WrapperInt(100);
System.out.println(n1.toString()); // [data=100]
System.out.println(n1.getData() * 2); // 200
System.out.println();
// Object 클래스
WrapperObject n2 = new WrapperObject(200);
System.out.println(n2.toString()); // WrapperObject [data=200]
System.out.println((int)n2.getData() * 2); // 400
System.out.println();
// Generic 클래스
Wrapper<Integer> n3 = new Wrapper<Integer>(300);
System.out.println(n3.toString()); // Wrapper [data=300]
System.out.println(n3.getData() * 2); // 600
System.out.println();
}
}
기본 클래스 사용
class WrapperInt {
private int data;
public WrapperInt(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
@Override
public String toString() {
return "[data=" + data + "]";
}
}
Object 클래스 사용
// Object
class WrapperString {
private String data;
public WrapperString(String data) {
this.setData(data);
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
@Override
public String toString() {
return "[data=" + data + "]";
}
}
제네릭 클래스 사용
// Generic
class Wrapper<T> {
private T data;
public Wrapper(T data) {
// 생성자는 꺽새 타입 변수 <T>를 붙이지 않는다.
this.setData(data);
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "Wrapper [data=" + data + "]";
}
}
Object 클래스는 루트 클래스이므로 지구상의 모든 데이터를 넣을 수 있다는 장점이 있지만, 제네릭 클래스와 비교하면 상대적으로 데이터를 꺼내 쓸 때 여기에 뭐가 들어있는지 확인이 어렵다는 단점이 있다.
그런 Object 클래스의 불편함을 어느정도 해결할 수 있는 게 바로 Generic 클래스이다!
제네릭 클래스를 사용하면 데이터를 꺼내 쓸 때 타입 형변환을 거치지 않아도 되므로 코드 재사용성이 훨씬 높아지게 된다. 그 결과로 코드의 가독성을 높이고 런타임 오류를 최소화할 수 있다.
제네릭 클래스의 자료형 지정
제네릭 클래스는 앞서 객체의 자료형이 Integer로 객체를 생성한 게 확인되었기 때문에 getData() 메소드에 마우스를 올려보면 Integer로 자료형이 설정되어 있는 것을 확인할 수 있다.