[JAVA] 제네릭 (Generic)
안녕하세요. 이번 글은 제네릭에 대한 설명입니다.
- 제네릭이란?
제네릭 (Generic)은 사전적인 의미로 '일반적'이라는 뜻이다. 클래스 타입으로서 하나의 공간 안에 여러 개의 자료형을 지정하는 방식으로 다채롭게 사용하도록 하는 기법을 의미한다. 제네릭을 사용함으로써 기존에 자료형 내지 기능이나 목적에 맞게 하나하나 클래스와 객체를 생성하는 비효율성을 해결할 수 있다. 또한 전에 미리 변수마다 자료형을 설정해둔다는 점에서 별도의 타입 확인 및 변환과정이 필요없다.
예를 들어 제조사별로 노트북의 정보를 관리해야 되는 상황을 가정해보자. 각 클래스라는 공간에는 다른 제조사의 상품 정보를 담을 수 없다. A사의 공간에는 A사의 상품에 대한 정보만 다룰 수 있다. 그러면 D사의 상품을 관리해야 되면 해당 제조사의 클래스를 별도로 생성해주고 메인 메서드에 객체 또한 생성해야 한다. 얼마나 비효율적인가? 어차피 정보를 관리한다는 점에서 보면 다 같은 클래스로 통합해서 관리하면 되는데 말이다. 또한 저장된 데이터를 각각의 제조사에 맞게 가져오려면 아무래도 다른 부분이 존재하는 만큼 그 부분에 맞게 조정해서 가져와야 한다.(캐스팅) 이러한 문제를 해결하기 위한 기법이 제네릭(Generic)이다. 이를 사용하면 모든 타입의 정보를 관리하면서 별도의 타입변환으로 인한 오류를 예방할 수 있다.
- 제네릭 사용법
제네릭은 클래스와 인터페이스 두 가지로 구성되어있다.
- [접근지정자] class [클래스명] <K, V, M>
- [접근지정자] interface [인터페이스명] <K, V, M>
이때 제네릭은 다른 일반적인 클래스, 인터페이스와는 다르게 뒤에 <>가 붙으면서 안에 변수가 들어있는 것을 확인할 수 있다. 이를 제네릭 타입 변수명이라고 하며 각 알파벳에는 관례적인 의미가 담겨있다.
변수명 | 의미 |
T | Type |
K | Key |
V | Value |
N | Number |
E | Element |
package classes;
public class Generic_Pract<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
Generic_Pract<String> gp = new Generic_Pract<>();
gp.setT("Hello");
System.out.println(gp.getT()); // Hello
Generic_Pract<Integer> gp2 = new Generic_Pract<>();
gp2.setT(20);
System.out.println(gp2.getT()); // 20
System.out.println(gp2.t); // 20
}
}
다음 소스코드를 통해 제네릭의 객체를 생성하고 이를 활용하는 과정을 통해 제네릭의 효율성을 확인할 수 있다.
왜냐하면 제네릭은 기본적으로 자바의 최상위 클래스인 Object를 기반으로 작동하기 때문에 어떠한 형식의 객체를 저장해도 다운캐스팅을 전제로 진행하기 때문에 오류가 발생하지 않는다는 점이 이유이다. 또한 제네릭 클래스는 클래스를 정의하는 시점이 아닌 객체를 생성하는 시점에서 타입을 지정한다.
- 제네릭 메서드
제네릭 메서드란 일반 클래스 내부에 특정 부분만 제네릭 형식으로 선언하는 형식의 메서드를 의미한다. 이는 클래스와 달리 호출되는 시점에서 실제적인 제네릭 타입을 지정하는 것이 특징이다. 지정하는 방법은 다음과 같다.
- [접근지정자] <T> T [메서드명] (T t) { }
- [접근지정자] <T, V> T [메서드명] (T t, V v) { }
- [접근지정자] <T> void [메서드명] (T t) { }
- [접근지정자] <T> T [메서드명] (int k)
package classes;
public class Generic_Pract<T> {
<T> boolean gEqual (T t01, T t02) {
return t01.equals(t02);
}
<T> void plus_Method (T t01, T t02) {
System.out.println(t01.equals(t02));
}
<K, V> void printing (K k, V v) {
System.out.println(k + " " + v);
}
public static void main(String[] args) {
Generic_Pract<String> gp = new Generic_Pract<>();
System.out.println(gp.gEqual("Hello", "Hello")); // true
gp.printing("Tistory", "Blog"); // Tistory Blog
gp.plus_Method("KOREA", "korea"); // false
Generic_Pract<Integer> gp02 = new Generic_Pract<>();
System.out.println(gp02.gEqual(0, 0)); // true
gp.plus_Method(0, 9); // false
gp.printing("2023", "0606"); // 2023 0606
}
}
상단의 소스코드를 통해 일반 클래스 안에 제네릭 형식의 메서드를 생성할 수 있으며 제네릭의 본질을 그대로 사용하면서 객체 생성을 통해 메서드를 구현할 수 있음을 확인할 수 있다.
- 상속을 통한 제네릭 타입 범위 제한
- [접근지정자] class [클래스명] <T extends [최상위 클래스 / 인터페이스명]>
상단과 같이 클래스에 제네릭 클래스 또는 인터페이스를 extends 형식으로 정의하면 상속된 제네릭 클래스(인터페이스) 객체 또는 그의 자식 클래스 객체만 대입 가능하게 설정할 수 있다.
package classes;
class J_First {}
class J_Second extends J_First {}
class J_Third extends J_Second {}
class J_Fourth <T extends J_Second> { // Second and Third Access
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
System.out.println(t.getClass());
}
}
public class Generic_Pract<T> {
public static void main(String[] args) {
J_Fourth<J_Second> d = new J_Fourth<>();
J_Fourth<J_Third> d2 = new J_Fourth<>();
// J_Fourth<J_First> dd = new J_Fourth<>(); // Error -> extends한 클래스와 그의 자식 클래스만 접근 가능
J_Fourth d3 = new J_Fourth();
d.setT(new J_Second()); // class classes.J_Second
d.setT(new J_Third()); // class classes.J_Third
d2.setT(new J_Third()); // class classes.J_Third
d3.setT(new J_Second()); // class classes.J_Second
d3.setT(new J_Third()); // class classes.J_Third
}
}
상단 소스코드를 보면 J_Fourth 클래스 뒤에 extends를 통해 제네릭 타입으로 접근 가능한 클래스의 범위를 설정한 점을 볼 수 있다. 이때 접근 가능한 대상은 extends 뒤에 제네릭 형식으로 붙은 클래스와 그의 자식 클래스다. 그렇기 때문에 대상이 아닌 J_First 클래스는 사용할 수 없다. 이를 통해서 메서드에 제네릭 형식으로 extends 함으로서 접근 가능한 메서드를 설정할 수 있다는 점을 확인할 수 있다.
package classes;
class J_First {
<T extends String> void mExtend_Type(T t) {
System.out.println(t);
}
}
public class Generic_Pract<T> {
public static void main(String[] args) {
J_First obj = new J_First();
obj.mExtend_Type("OK");
//obj.mExtend_Type(56); // Error -> String 외 다른 자료형의 데이터는 사용할 수 없다.
}
}
메서드의 접근 범위 뿐만 아니라 제네릭으로 올 수 있는 클래스의 타입 또한 제한할 수 있다. 상단의 소스코드와 같이 <T extends String>를 메서드 반환타입 앞에 언급함으로서 소괄호 안에 매개변수의 타입을 지정할 수 있다는 점을 알 수 있다.
package classes;
class J_First {
<T extends String> void mExtend_Type(T t) {
System.out.println(t);
}
}
class J_Second extends J_First {
}
public class Generic_Pract<T> {
public static void main(String[] args) {
J_First obj = new J_First();
obj.mExtend_Type("String");
J_Second obj02 = new J_Second();
obj02.mExtend_Type("String");
}
}
추가로 일반적으로 제네릭이 제네릭을 상속받는 상황에서는 클래스와 메서드 모두 부모 클래스가 제네릭이면 자식 클래스도 제네릭이며 부모 클래스 내 제네릭 메서드가 있으면 자식 클래스 내에도 제네릭 메서드가 그대로 존재한다.
다음 글은 쓰레드에 대한 설명입니다. 감사합니다.