제네릭
데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법
우리가 흔히 쓰는 ArrayList, LinkedList를 생성할 때 다음과 같이 쓴다.
객체<타입> 객체명 = new 객체<타입>();
아래와 같이 <> 다이아몬드 연산자 안에 들어가는 타입을 지정해준다.
ArrayList<Integer> list1 = new ArrayList<Integer>();
ArrayList<Integer> list2 = new ArrayList<Integer>();
LinkedList<Double> list3 = new LinkedList<Double>();
LinkedList<Character> list3 = new LinkedList<Character>();
우리가 어떤 자료 구조를 만들어 배포할 때, String 타입도 지원하고 싶고, Integer 타입도 지원하고 싶고, 많은 타입을 지원하고 싶다. 그러면 String에 대한 클래스, Integer에 대한 클래스 등 하나 하나 타입에 따라 생성하는 건 비효율적이다. 이러한 문제를 해결하기 위해 제네릭을 사용한다.
이렇 듯 제네릭은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미한다. 한마디로 특정 타입을 미리 지정해 주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반 제네릭 타입이다.
제네릭 사용 방법
타입 | 설명 |
<T> | Type |
<E> | Element |
<K> | Key |
<V> | Value |
<N> | Number |
상황별 선언 및 생성 방법
1. 클래스 및 인터페이스 선언
public class className<T>
{
...
}
public Interface InterfaceName<T>
{
...
}
T 타입은 해당 블럭 { ... } 안에서까지 유효하다.
또한, 더 나아가 제네릭 타입을 2개로 둘 수 있다. 대표적으로 타입 인자를 2개 받는 HashMap을 생각해보자.
public class className<T>
{
...
}
public Interface InterfaceName<T>
{
...
}
// HashMap의 경우 아래와 같이 선언될 것이다.
public class HashMap<K, V>
{
...
}
이렇게 데이터 타입을 외부로부터 지정할 수 있다. 즉, 객체를 생성해야 하는데 이 때 구체적인 타입을 명시 해줘야 한다.
public class ClassName<T, N>
{
...
}
public class Main {
public static void main(String[] args) {
ClassName<String, Integer> a = new ClassName<String, Integer>();
}
}
위 예시에서 T는 String이 되고, K는 Integer가 된다.
주의해야 할 점은, 타입 파라미터로 명시할 수 있는 건 참조 타입 밖에 올 수 없다. 즉, int, double, char 같은 primitive type은 올 수 없다. 그래서 int형 double 형 등 primitive Type의 경우 Integer, Double 같은 Wrapper Type을 써야 한다.
참조 타입이 올 수 있다는 것은 사용자가 정의한 클래스도 올 수 있다는 것이다.
public class ClassName<T, N>
{
...
}
public class Student
{
...
}
public class Main {
public static void main(String[] args) {
ClassName<Student> a = new ClassName<Student>();
}
}
2. 제네릭 클래스 활용
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()); // 10
// 변환된 변수의 타입 출력
System.out.println("a E type: " + a.get().getClass().getName()); // java.lang.String
System.out.println();
System.out.println("b data: " + b.get()); // 10
// 반환된 변수의 타입 출력
System.out.println("b E Type: " + b.get().getClass().getName()); // java.lang.Integer
}
}
보면 ClassName 객체를 생성할 때 <> 안에 타입 파라미터를 지정한다.
그러면 a 객체의 ClassName의 E 제네릭 타입은 모두 String으로 변환되고,
b객체의 ClassName의 E 제네릭 타입은 Integer로 모두 변환된다.
제네릭 2개 사용
first data: 10
K Type: java.lang.String
second data: 10
V 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;
}
}
public class Main {
public static void main(String[] args) {
ClassName<String, Integer> a = new ClassName<String, Integer>();
a.set("10", 10);
System.out.println("first 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());
}
}
제네릭 메서드
위 과정까지는 클래스 옆에 <E> 같은 제네릭 타입을 붙여 해당 클래스 내에서 사용할 수 있는 E 타입으로 일반화했다. 이 외 별도로 메서드에 한정한 제네릭도 사용할 수 있다.
선언 방법
public<T> T genericMethod(T o)
{
...
}
[접근 제어자]<제네릭타입>[반환타입][메서드명]([제네릭타입][파라미터])
{
...
}
클래스와는 다르게 반환 타입 이전에 <> 제네릭 타입을 선언한다.
a data: 10
a E Type: java.lang.String
b data: 10
b E Type: java.lang.Integer
<T> returnType: java.lang.Integer
<T> returnType: java.lang.String
<T> returnType: com.sist.collection.ClassName
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<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();
// 제네릭 메서드 Integer
System.out.println("<T> returnType: " + a.genericMethod(3).getClass().getName());
// 제네릭 메서드 String
System.out.println("<T> returnType: " + a.genericMethod("ABCD").getClass().getName());
// 제네릭 메서드 ClassName b
System.out.println("<T> returnType: " + a.genericMethod(b).getClass().getName());
}
}
제네릭 적용 전
public class CastingDTO {
private Object object;
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
}
후
public class CastingDTO<T> {
private T object;
public T getObject() {
return object;
}
public void setObject(T object) {
this.object = object;
}
}
Object 대신 T로 바꾼다. 이런 <>를 제네릭 타입이라 한다.
String
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("Java");
list.add("Oracle");
list.add("HTML");
for(int i=0; i<list.size(); i++)
{
String subject = list.get(i);
System.out.println(subject);
}
}
int
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(100);
list.add(200);
list.add(300);
for(int i=0; i<list.size(); i++)
{
int subject = list.get(i);
System.out.println(subject);
}
}
제네릭 타입
-선언시 클래스 또는 인터페이스 뒤에 <>가 붙는다. <> 안에는 클래스와 같이 구체적인 타입을 지정한다. (어떤 타입이 들어가도 상관 없다.)
- <>에는 타입 파라미터가 위치한다.
타입 파라미터
적용 전
CastingDTO dto1 = new CastingDTO();
dto1.setObject(new String());
CastingDTO dto2 = new CastingDTO();
dto2.setObject(new StringBuffer());
CastingDTO dto3 = new CastingDTO();
dto3.setObject(new StringBuilder());
String temp1 = (String)dto1.getObject();
StringBuffer temp2 = (StringBuffer)dto2.getObject();
StringBuilder temp3 = (StringBuilder)dto3.getObject();
적용 후
CastingDTO<String> dto1 = new CastingDTO<>();
dto1.setObject(new String());
CastingDTO<StringBuffer> dto2 = new CastingDTO<>();
dto2.setObject(new StringBuffer());
CastingDTO<StringBuilder> dto3 = new CastingDTO<>();
dto3.setObject(new StringBuilder());
String temp1 = dto1.getObject();
StringBuffer temp2 = dto2.getObject();
StringBuilder temp3 = dto3.getObject();
참고
https://st-lab.tistory.com/153