이글은 자바를 빠르게 끝내기 위해 내가 헷갈리는 부분만을 정리해논 글입니다. Do it! 자바 프로그래밍 입문을 정리하지만 내용을 많이 건너뜀
제네릭은 클래스 / 인터페이스 / 메서드 등의 타입을 파라미터로 사용할 수 있을게 해주는 역할을 한다.
'데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법'
우리가 흔히 쓰는 ArrayList, LinkedList등을 생성할 때 제네릭이 사용가능하다.
객체<타입> 객체명 = new 객체<타입>();
ArrayList<Integer> list1 = new ArrayList<Integer>();
ArrayList<String> list2 = new ArrayList<String>();
LinkedList<Double> list3 = new LinkedList<Double>();
LinkedList<Character> list4 = new LinkedList<Character>();
이렇게 <>괄호 안에 들어가는 타입을 지정해준다.
우리가 어떤 자료 구조(Class)를 만들어 배포하려고 한다. 그런데 String 타입도 지원하고 싶고 Integer 타입도 지원하고 싶고 많은 타입을 지원하고 싶다. 그러면 String에 대한 클래스, Interger에 대한 클래스 등 하나하나 타입에 따라 만들 것인가? 그건 너무 비효율적이다. 이런 문제를 해결하기 위해 제네릭을 사용한다.
제네릭은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미한다.
-> 정확하게 얘기하면 지정된다는 것 보다는 타입의 경계를 지정하고, 컴파일때 해당 타입으로 캐스팅하여 매개변수화된 유형을 삭제하는 것이다.
타입 | 설명 |
---|---|
< K > | Type |
< E > | Element |
< K > | Key |
< V > | Value |
< N > | Number |
보통 제네릭은 위 표 타입들이 많이 사용된다.
하지만 물론 한글자일 필요는 없다. 또한 설명과 반드시 일치해야 할 필요도 없다. 예로들어 < Ele >라고 해도 전혀 무방하다. 다만 대중적으로 통하는 통상적인 선언이 가장 편하기 때문에 위와 같은 암묵적인 규칙이 있을 뿐이다.
public class ClassName <T> {...}
public Interface InterfaceName <T> {...}
기본적으로 제네릭 타입의 클래스나 인터페이스의 경우 위와 같이 선언한다.
T 타입은 해당 블럭{...} 안에서까지 유효하다.
또한 여기서 더 나아가 제네릭 타입을 두 개로 둘 수 있다.(대표적으로 타입 인자로 두 개 받는 대표적인 컬렉션인 HashMap을 생각해보자)
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으로 쓰는 이유가 바로 위와 같은 이유이다.
Reference Type와 primitive Type에 대해서는 아래 링크를 참고하자
참조타입 원시타입 참고 링크
참조타입이 가능하므로 사용자가 정의한 클래스도 타입으로 올 수 있다는 것이다.
public class ClassName <T> { ... }
public class Student { ... }
public class Main {
public static void main(String[] args) {
ClassName<Student> a = new ClassName<Student>();
}
}
// 제네릭 클래스
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으로 모두 변환돈다.
만약 제네릭을 두 개 쓰고 싶다면 이렇게도 가능
// 제네릭 클래스
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());
}
}
결과는 다음과 같다
이렇게 외부 클래스에서 제네릭 클래스를 생성할 때 <>괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해주는 것. 이것이 바로 제네릭 프로그래밍이다.
일반적인 선언 방법은 다음과 같다.
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<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());
}
}
보면 ClassName이란 객체를 생성할 때 <>안에 타입 파라미터를 지정한다.
그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.
반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.
geneicMethod()는 파라미터 타입에 따라 T 타입이 결정된다.
코드 실행 결과
즉, 클래스에서 지정한 제네릭 유형과 별도로 메서드에서 독립적으로 제네릭 유형을 선언하여 쓸 수 있다.
위와 같은 방식이 왜 필요한가? 바로 정적 메소드로 선언할 때 필요 하기 때문이다.
static 메서드는 객체가 생성되기 이전 시점에서 메모리에 먼저 올라가기 때문에 제네릭이 사용되는 메소드를 정적 메소드로 두고 싶은 경우 제네릭 클래스와 별로도 독립적인 제네릭이 사용되어야한다.
더 자세한 내용은 참고문헌을 보자
++ arrayList의 default_capacity는 10이다.
용량이 넘치면 더블링을 한다.