문득 강의를 듣다가 "제네릭 타입으로 지정해 줍니다" 라는 말을 정확하게 이해 하고 싶어서 포스팅을 하게 되었다.
너무너무 잘 정리되어 있는 블로그가 있어서 내용을 좀 많이 발췌해왔지만!! 나의 언어로 다시 써보도록 한다.
참고 블로그 : https://st-lab.tistory.com/153
1. 제네릭이란?
직역 하면 "일반적인" 이라는 뜻이다.
"데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법"으로
특정(Specific) 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic) 타입이라는 것이다.
제네릭은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정된다.
(좀 더 정확히 말하자면 지정된다는 것 보다는 타입의 경계를 지정하고, 컴파일 때 해당 타입으로 캐스팅하여 매개변수화 된 유형을 삭제하는 것이다.)
2. 언제쓰나?
우리가 흔히 ArrayList, LinkedList 등을 생성할 때, <> 안에 타입을 적어서 객체를 생성한다.
ArrayList<Integer> list1 = new ArrayList<Integer>(); // Integer TYPE
ArrayList<String> list2 = new ArrayList<String>(); // String TYPE
LinkedList<Double> list3 = new LinkedList<Double>(): // Double TYPE
LinkedList<Character> list4 = new LinkedList<Character>(); // Character TYPE
- ArrayList는 Element Type의 Generic 을 지정할 수 있다.!
3. 타입?
가장 일반적으로 사용되는 유형 매개 변수 이름들이다. 암묵적으로 아래와 같이 많이 사용된다.
매개변수 이름을 <E>가 아니라 <Ele> 처럼 지정해서 사용 할 수도 있고, 설명과 일치하지 않아도 된다.
타입 안에는 참조 타입(Reference Type)을 넣을 수 있다.
보통 Integer, String 같은 Wrapper class나 사용자 정의 class 를 넣어서 사용한다.
타입 | 설명 |
<T> | Type |
<E> | Element |
<K> | Key |
<V> | Value |
<N> | Number |
자 이제 제네릭 클래스를 선언하고 사용해보자.!
1. 제네릭 클래스 및 인터페이스 선언
public class ClassName <T> { ... }
public Interface InterfaceName <T> { ... }
위와 같이 제네릭 타입으로 선언을 할 수 있으며, T 타입은 해당블럭 ( {...} ) 안에서까지 유효한다.
제네릭 타입은 두개 이상의 멀티 타입 파라미터로 사용할 수도 있다. ( HashMap< K, V> 가 대표적이다. )
public class ClassName <T, K> { ... }
public Interface InterfaceName <T, K> { ... }
public class HashMap <K, V> { ... }
2. 제네릭 클래스 생성
위에서 말한 대로 타입 안에는 참조 타입(Reference Type)만 넣을 수 있다.
- Wrapper Class로 생성
public class GenericClass <T, K> { ... }
public class Main {
public static void main(String[] args) {
GenericClass<String, Integer> a = new GenericClass<>();
}
}
제네릭 클래스의 객체를 생성해서 T는 String, K는 Integer 타입으로 명시해주었다.
- 사용자 정의 클래스로 생성
public class GenericClass <T> { ... }
public class Student { ... }
public class Main {
public static void main(String[] args) {
GenericClass<Student> a = new GenericClass<>();
}
}
Student 타입의 객체가 생성되었다.
3. 제네릭 클래스 사용
- single type parameter
// 제네릭 클래스
class GenericClass<E> {
// 제네릭 타입 변수
private E element;
// 제네릭 파라미터 메소드
void set(E element){ this.element = element; }
// 제네릭 타입 반환 메소드
E get(){ return element; }
}
class Main {
public static void main(String[] args) {
GenericClass<String> a = new GenericClass<>();
GenericClass<Integer> b = new GenericClass<>();
a.set("10");
b.set(10);
System.out.println(a.get()); // 10
System.out.println(a.get().getClass().getName()); // java.lang.String
System.out.println(b.get()); // 10
System.out.println(b.get().getClass().getName()); // java.lang.Integer
}
}
a 객체의 GenericClass의 E 제네릭 타입은 String으로 변환되었고,
b 객체의 GenericClass의 E 제네릭 타입은 Integer로 변환되었다.
- multi type parameter
// 제네릭 클래스
class GenericClass<E> {
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) {
GenericClass<String, Integer> a = new GenericClass<>();
a.set("value", 10);
System.out.println(a.getFirst()); // value
System.out.println(a.getFirst().getClass().getName()); // java.lang.String
System.out.println(a.getSecond()); // 10
System.out.println(b.getSecond().getClass().getName()); // java.lang.Integer
}
}
외부 클래스에서 제네릭 클래스를 생성할 때 <> 괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해주는 것.!! 이 바로 제네릭 프로그래밍이다.
이제 메서드에 대해 알아보자.
1. 제네릭 메서드 선언
// [접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터])
public <T> T genericMethod(T o) { ... }
<T> 타입으로 선언해봤다.
클래스와는 다르게 반환타입 이전에 <> 제네릭 타입을 선언한다.
2. 제네릭 메서드 사용
// 제네릭 클래스
class GenericClass<E> {
...
// 제네릭 메서드
<T> T genericMethod(T o) { return o; }
}
public class Main {
public static void main(String[] args) {
GenericClass<String> a = new GenericClass<>();
GenericClass<Integer> b = new GenericClass<>();
// Integer
System.out.println("<T> returnType : " + a.genericMethod(3).getClass().getName());
// String
System.out.println("<T> returnType : " + a.genericMethod("ABCD").getClass().getName());
// GenericClass
System.out.println("<T> returnType : " + a.genericMethod(b).getClass().getName());
}
}
method는 class 객체 생성과 동시에 생성된다.
genericMethod의 파라미터 타입에 따라 제네릭의 타입 <T> 가 결정된다.
위 결과로 제네릭 메서드는 클래스의 제네릭 타입과는 독립적으로 제네릭을 선언할 수 있다는 것을 알 수 있다.
그럼 위와같은 독립적 선언 방식이 왜 필요한가? 바로 '정적 메소드로 선언할 때 필요'하기 때문이다.
생각해보자. 제네릭은 타입을 외부에서 지정해준다고 했다. 즉 해당 클래스 객체가 인스턴스화 했을 때, 쉽게 말해 new 생성자로 클래스 객체를 생성하고 <> 괄호 사이에 파라미터로 넘겨준 타입으로 지정이 된다는 뜻이다.
하지만 static 변수, static 함수 등 static이 붙은 것들은 기본적으로 프로그램 실행시 메모리에 이미 올라가있다.
이 말은 객체 생성을 통해 접근할 필요 없이 이미 메모리에 올라가 있기 때문에 클래스 이름을 통해 바로 쓸 수 있다는 것이다.
이때 static으로 메소드를 만들면 객체가 생성되기 전에 이미 메모리에 올라가는데 타입을 어디서 얻어올 수 있을까?
class GenericClass<E> {
/*
* 클래스와 같은 E 타입이더라도
* static 메소드는 객체가 생성되기 이전 시점에
* 메모리에 먼저 올라가기 때문에
* E 유형을 클래스로부터 얻어올 방법이 없다.
*/
static E genericMethod(E o) { // error!
return o;
}
}
class Main {
public static void main(String[] args) {
// ClassName 객체가 생성되기 전에 접근할 수 있으나 유형을 지정할 방법이 없어 에러남
GenericClass.getnerMethod(3);
}
}
그렇기 때문에 제네릭이 사용되는 메소드를 정적메소드로 두고 싶은 경우 제네릭 클래스와 별도로 독립적인 제네릭이 사용되어야 한다는 것이다.
// 제네릭 클래스
class GenericClass<E> {
// 아래 메소드의 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) {
GenericClass<String> a = new GenericClass<>();
// Integer
System.out.println("<E> returnType : " + ClassName.genericMethod1(3).getClass().getName());
// String
System.out.println("<E> returnType : " + ClassName.genericMethod1("A").getClass().getName());
// GenericClass
System.out.println("<T> returnType : " + ClassName.genericMethod1(a).getClass().getName());
// Double
System.out.println("<T> returnType : " + ClassName.genericMethod1(3.0).getClass().getName());
}
}
제네릭 메서드는 제네릭 클래스 타입과 별도로 지정된 것을 볼 수 있다.
외부에서 <> 괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해 주는것. 이것이 제네릭 프로그래밍!!
그런데 이런 의문이 들 수 있다. "아니 그러면 특정 범위만 허용하고 나머지 타입은 제한 할 수 없나요?"라는 얘기가 나오기 마련이다.
지금까지는 제네릭의 가장 일반적인 예시들을 보여주었다. 예로들어 타입을 T라고 하고 외부클래스에서 Integer을 파라미터로 보내면 T 는 Integer가 되고, String을 보내면 T는 String이 된다. 만약 당신이 Student 라는 클래스를 만들었을 때 T 파라미터를 Student로 보내면 T는 Student가 된다. 즉, 제네릭은 참조 타입 모두 될 수 있다.
근데, 만약 특정 범위 내로 좁혀서 제한하고 싶다면 어떻게 해야할까?
이 때 필요한 것이 바로 extends 와 super, 그리고 ?(물음표)다. ? 는 와일드 카드라고 해서 쉽게 말해 '알 수 없는 타입'이라는 의미다.
1. 제한된 Generic
super와 extends에 대한 설명은 따로 포스팅 해 놓았다.
<K extends T> // T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정 됨)
<K super T> // T와 T의 부모(조상) 타입만 가능 (K는 들어오는 타입으로 지정 됨)
<? extends T> // T와 T의 자손 타입만 가능
<? super T> // T와 T의 부모(조상) 타입만 가능
<?> // 모든 타입 가능. <? extends Object>랑 같은 의미
이 때 주의해야 할 게 있다. K extends T와 ? extends T는 비슷한 구조지만 차이점이 있다.
'유형 경계를 지정'하는 것은 같으나 경계가 지정되고 K는 특정 타입으로 지정이 되지만, ?는 타입이 지정되지 않는다는 의미다.
/*
* Number와 이를 상속하는 Integer, Short, Double, Long 등의
* 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 K는
* 지정된 타입으로 변환이 된다.
*/
<K extends Number>
/*
* Number와 이를 상속하는 Integer, Short, Double, Long 등의
* 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 지정 되는 타입이 없어
* 타입 참조를 할 수는 없다.
*/
<? extends T> // T와 T의 자손 타입만 가능
2. <?> (와일드 카드 : Wild Card)
와일드 카드 <?> 은 <? extends Object> 와 마찬가지라고 했다. Object는 자바에서의 모든 API 및 사용자 클래스의 최상위 타입이다. 한마디로 다음과 같은 의미나 마찬가지다.
public class ClassName { ... }
public class ClassName extends Object { ... }
우리가 public class ClassName extends Object {} 를 묵시적으로 상속받는 것이나 다름이 없다.
한마디로 <?>은 무엇이냐. 어떤 타입이든 상관 없다는 의미다.
이는 보통 데이터가 아닌 '기능'의 사용에만 관심이 있는 경우에 <?>로 사용할 수 있다.
ㅁㅏ치며
빡세다.. 하지만 제네릭에 대해서 잘 알수 있게 된듯하다
제네릭은.. 외부 클래스에서 제네릭 클래스를 인스턴스 화 할때 <> 괄호 안에 파라미터로 제네릭 타입을 지정하는 것!! 참조 타입만 올수 있다!!! 잊지말자
처음에 잘 정리 되어있는 글을 봤을때 내용도 길고 복잡시러 보였는데 읽고 이해하려고 노력하니까 챱챱정리가 되었다.
여러가지 제네릭 관련 포스팅을 찾아보았는데 솔직히 잘 정리된 글 하나를 복붙한 내용들이 많았다.
나는 그래서 내가 읽기 편하고 좀더 간결하게? 필요한 내용들만 정리를 한것이다. 나름 잘 한것같다.
다음에는 뭐 해볼까~~
'공부 > JAVA' 카테고리의 다른 글
[JAVA] java 버전, maven(?) error (0) | 2022.07.04 |
---|---|
[JAVA] Extends 와 Super (0) | 2022.06.22 |
[JAVA] 배열복사 메서드 ( Object.clone, Arrays.copyOf(Range), System.arrayCopy) (0) | 2022.06.11 |
ArrayList(LinkedList) 를 String으로 변환하는 방법 (JAVA) (0) | 2022.05.24 |
[JAVA] GUI 계산기 만들기 (0) | 2022.05.21 |
댓글