- 컬렉션 클래스에 저장할 형(type)을 지정하고 데이터를 읽을 때 지정된 형으로 데이터를 입력받게 하는 방식.
- 제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법이다.
public class ClassName <T> { ... }
public Interface InterfaceName <T> { ... }
T 타입은 해당 블럭 { ... } 안에서까지 유효하다.
public class ClassName <T, K> { ... }
public Interface InterfaceName <T, K> { ... }
// HashMap의 경우 아래와 같이 선언되어있을 것이다.
public class HashMap <K, V> { ... }
이렇듯 데이터 타입을 외부로부터 지정할 수 있도록 할 수 있다.
생성된 제네릭 클래스를 사용. 즉, 객체를 생성해야하는데 이 때 구체적인 타입을 명시해주어야 한다.
public class ClassName <T, K> { ... }
public class Main {
public static void main(String[] args) {
ClassName<String, Integer> a = new ClassName<String, Integer>();
}
위 예시에서는 T는 String이 되고, K는 Integer가 된다.
❗️주의❗️ 타입 파라미터로 명시할 수 있는 것은 참조 타입(Reference Type) 밖에 올 수 없다. 즉, int, double, char 같은 primitive type은 올 수 없다는 것이다. 그래서 int형 double형 등 primitive Type의 경우 Integer, Double 같은 Wrapper Type으로 쓰는 이유가 바로 위와같은 이유다.
public class ClassName <T> { ... }
public class Student { ... }
public class Main {
public static void main(String[] args) {
ClassName<Student> a = new ClassName<Student>();
}
}
또한 참조 타입이 올 수 있다는 것은 사용자가 정의한 클래스도 타입으로 올 수 있다는 것을 의미한다.
class MyArray<T> {
T element;
void setElement(T element) { this.element = element; }
T getElement() { return element; }
}
class ClassName<E> {
private E element; // 제네릭 타입 변수
void set(E element) { // 제네릭 파라미터 메소드
this.element = element;
}
E get() { // 제네릭 타입 반환 메소드
return element;
}
}
class Main {
public static void main(String[] args) {
ClassName<String> a = new ClassName<String>();
ClassName<Integer> b = new ClassName<Integer>();
a.set("10");
b.set(10);
System.out.println("a data : " + a.get());
// 반환된 변수의 타입 출력
System.out.println("a E Type : " + a.get().getClass().getName());
System.out.println();
System.out.println("b data : " + b.get());
// 반환된 변수의 타입 출력
System.out.println("b E Type : " + b.get().getClass().getName());
}
}
위의 예제에서 'ClassName'이란 객체를 생성할 때 <> 안에 타입 파라미터(Type parameter)를 지정한다.
그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.
반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.
실제로 위 코드를 실행시키면 다음과 같이 출력된다.
a data : 10
a E Type : java.lang.String
b data : 10
b E Type : java.lang.Integer
class ClassName<K, V> {
private K first; // K 타입(제네릭)
private V second; // V 타입(제네릭)
void set(K first, V second) {
this.first = first;
this.second = second;
}
K getFirst() {
return first;
}
V getSecond() {
return second;
}
}
// 메인 클래스
class Main {
public static void main(String[] args) {
ClassName<String, Integer> a = new ClassName<String, Integer>();
a.set("10", 10);
System.out.println(" fisrt data : " + a.getFirst());
// 반환된 변수의 타입 출력
System.out.println(" K Type : " + a.getFirst().getClass().getName());
System.out.println(" second data : " + a.getSecond());
// 반환된 변수의 타입 출력
System.out.println(" V Type : " + a.getSecond().getClass().getName());
}
}
fisrt data : 10
K Type : java.lang.String
second data : 10
V Type : java.lang.Integer
[접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터]) {
// 텍스트
}
public <T> T genericMethod(T o) { // 제네릭 메소드
...
}
반환타입 이전에 <> 제네릭 타입을 선언한다.
// 제네릭 클래스
class ClassName<E> {
private E element; // 제네릭 타입 변수
void set(E element) { // 제네릭 파라미터 메소드
this.element = element;
}
E get() { // 제네릭 타입 반환 메소드
return element;
}
<T> T genericMethod(T o) { // 제네릭 메소드
return o;
}
}
public class Main {
public static void main(String[] args) {
ClassName<String> a = new ClassName<>();
ClassName<Integer> b = new ClassName<>();
a.set("10");
b.set(10);
System.out.println("a data : " + a.get());
// 반환된 변수의 타입 출력
System.out.println("a E Type : " + a.get().getClass().getName());
System.out.println();
System.out.println("b data : " + b.get());
// 반환된 변수의 타입 출력
System.out.println("b E Type : " + b.get().getClass().getName());
System.out.println();
// 제네릭 메소드 Integer
System.out.println("<T> data : " +a.genericMethod(3));
System.out.println("<T> returnType : " + a.genericMethod(3).getClass().getName());
System.out.println();
// 제네릭 메소드 String
System.out.println("<T> data : " +a.genericMethod("ABCD"));
System.out.println("<T> returnType : " + a.genericMethod("ABCD").getClass().getName());
System.out.println();
// 제네릭 메소드 ClassName b
System.out.println("<T> returnType : " + a.genericMethod(b).getClass().getName());
}
}
a data : 10
a E Type : java.lang.String
b data : 10
b E Type : java.lang.Integer
<T> data : 3
<T> returnType : java.lang.Integer
<T> data : ABCD
<T> returnType : java.lang.String
<T> returnType : generic_03.ClassName
ClassName이란 객체를 생성할 때 <> 안에 타입 파라미터를 지정한다.
그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.
반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.
genericMethod()는 파라미터 타입에 따라 T 타입이 결정된다.
👉 즉, 클래스에서 지정한 제네릭유형과 별도로 메소드에서 독립적으로 제네릭 유형을 선언하여 쓸 수 있다.
'정적 메소드'로 선언할 때 필요하기 때문이다.
제네릭은 타입을 외부에서 지정해준다. 즉 해당 클래스 객체가 인스턴스화 했을 때, 쉽게 말해 new 생성자로 클래스 객체를 생성하고 <> 괄호 사이에 파라미터로 넘겨준 타입으로 지정이 된다.
하지만 static 정적이라는 뜻으로 static 변수, static 함수 등 static이 붙은 것들은 기본적으로 프로그램 실행시 메모리에 이미 올라 가 있다.
즉 객체 생성을 통해 접근할 필요 없이 이미 메모리에 올라가 있기 때문에 클래스 이름을 통해 바로 쓸 수 있다.
이때, static 메소드는 객체가 생성되기 전에 이미 메모리에 올라가게 되는데 타입을 어디서 얻어올 수 있을까?
아래 예제를 보면,
class ClassName<E> {
/*
* 클래스와 같은 E 타입이더라도
* static 메소드는 객체가 생성되기 이전 시점에
* 메모리에 먼저 올라가기 때문에
* E 타입을 클래스로부터 얻어올 방법이 없다.
*/
static E genericMethod(E o) { // error!
return o;
}
}
class Main {
public static void main(String[] args) {
// ClassName 객체가 생성되기 전에 접근할 수 있으나 유형을 지정할 방법이 없어 에러남
ClassName.getnerMethod(3);
}
}
클래스와 같은 E 타입이더라도 static 메소드는 객체가 생성되기 이전 시점에 메모리에 먼저 올라가기 때문에 E 타입을 클래스로부터 얻어올 방법이 없다.
따라서 제네릭이 사용되는 메소드를 정적 메소드로 두고 싶은 경우 제네릭 클래스와 별도로 독립적인 제네릭이 사용되어야 한다.
// 제네릭 클래스
class ClassName<E> {
private E element; // 제네릭 타입 변수
void set(E element) { // 제네릭 파라미터 메소드
this.element = element;
}
E get() { // 제네릭 타입 반환 메소드
return element;
}
// 아래 메소드의 E타입은 제네릭 클래스의 E타입과 다른 독립적인 타입이다.
static <E> E genericMethod1(E o) { // 제네릭 메소드
return o;
}
static <T> T genericMethod2(T o) { // 제네릭 메소드
return o;
}
}
public class Main {
public static void main(String[] args) {
ClassName<String> a = new ClassName<String>();
ClassName<Integer> b = new ClassName<Integer>();
a.set("10");
b.set(10);
System.out.println("a data : " + a.get());
// 반환된 변수의 타입 출력
System.out.println("a E Type : " + a.get().getClass().getName());
System.out.println();
System.out.println("b data : " + b.get());
// 반환된 변수의 타입 출력
System.out.println("b E Type : " + b.get().getClass().getName());
System.out.println();
// 제네릭 메소드1 Integer
System.out.println("<E> returnType : " + ClassName.genericMethod1(3).getClass().getName());
// 제네릭 메소드1 String
System.out.println("<E> returnType : " + ClassName.genericMethod1("ABCD").getClass().getName());
// 제네릭 메소드2 ClassName a
System.out.println("<T> returnType : " + ClassName.genericMethod1(a).getClass().getName());
// 제네릭 메소드2 Double
System.out.println("<T> returnType : " + ClassName.genericMethod1(3.0).getClass().getName());
}
}
a data : 10
a E Type : java.lang.String
b data : 10
b E Type : java.lang.Integer
<E> returnType : java.lang.Integer
<E> returnType : java.lang.String
<T> returnType : generic_04.ClassName
<T> returnType : java.lang.Double
이렇듯 제네릭 메소드는 제네릭 클래스 타입과 별도로 지정된다는 것을 볼 수 있다.
- 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
- 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
- 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.