오랜만에 왔다...
생각은 하는데 쓰는게 쉽지가 않네 머쓱..
오늘은 제네릭에 대해 알아보자.
public class Test<T> {}
public class MyMap<K, V> {}
이런거 본 적 있을거다.
ArrayList<String> list = new ArrayList<>();
맞다. 리스트할 때 봤던 이녀석도 제네릭이다!
제네릭은 쉽게 말해서 <> 부호 사이에 타입 파라미터가 위치한다.
제네릭이 뭐냐면 클래스, 인터페이스, 메서드에서 사용할 타입을 파라미터로 지정할 수 있게 해주는 기능이다. 제네릭을 사용하면 컴파일 타임에 타입을 체크할 수 있어 코드의 안전성과 가독성을 높일 수 있다.
1. 캐스팅 제거
// 제네릭 사용 전
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0);
// 제네릭 사용 후
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 캐스팅 불필요
제네릭을 사용함으로써 런타임 오류를 줄일 수 있고 불필요한 타입 캐스팅을 제거하여 가독성과 유지보수를 높일 수 있다.
2. 코드 재사용성 증가
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
Box<String> stringBox = new Box<>();
Box<Integer> intBox = new Box<>();
타입을 지정하지 않았기 때문에 다양한 타입에 대해 재사용 가능한 범용 코드를 작성할 수 있다. 이는 코드 중복을 줄이고 유지보수성을 향상
3. 컴파일 타임 타입 체크
public <T> void add(T a, T b) {
// 코드 내부에서 T 타입의 변수 사용
}
이게 무슨 소린가 싶은데 메서드가 호출될 떄 컴파일러가 전달된 인수의 타입을 보고 'T'의 실제 타입을 추론한다는 소리다.
public class GenericExample {
public <T> void add(T a, T b) {
// 여기서는 T 타입의 변수 a와 b를 사용할 수 있다.
}
public static void main(String[] args) {
GenericExample example = new GenericExample();
example.add(1, 2); // 컴파일 타임에 T가 Integer로 치환됨
example.add("Hello", "World"); // 컴파일 타임에 T가 String으로 치환됨
// example.add(1, "World"); // 컴파일 오류: 서로 다른 타입은 허용되지 않음
}
}
예를 들어 example.add(1, 2); 가 호출될 때 T는 Integer로 추론되고, example.add("Hello", "World");는 String으로 추론된다는 소리다.
4. 의도 명확성
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
}
제네릭을 사용하는 경우 타입 매개변수를 명시함으로써 클래스나 메서드가 다루는 데이터 타입을 구체적으로 정의할 수 있다. 이를 통해 코드의 가독성이 향상 된다.
이게 무슨 소리냐면 제네릭을 사용하면 객체를 선언할 때
Pair<String, Integer> pair = new Pair<>("Age", 30);
이런식으로 선언하기 때문에 String타입의 키와 Integer타입의 값을 가진다는 것을 명시적으로 나타낼 수 있다는 것이다.
여기서 또 봐야되는데 타입 매개변수는 T 뿐만 아니라 V , K 등 여러가지가 있는데 자주 사용되는 매개변수를 알아보자.
T | Type - 주로 클래스, 메서드에서 임의의 타입을 나타낼 때 사용 |
E | Element - 주로 컬렉션에서 사용되며 요소를 나타냄 |
K | Key - 주로 Map에서 키를 나타냄 |
V | Value - 주로 Map에서 값을 나타냄 |
N | Number - 숫자를 나타냄 |
R | Result - 결과를 나타냄 |
마지막으로 제네릭 와일드 카드에 대해 알아보자.
코드를 보다보면 <?>라고 되어있는걸 볼 수 있다. 제네릭 타입을 좀 더 유연하게 사용하기 위해 도입된 기능인데 특정 타입을 대신에 사용할 수 있다.
1. 무제한 와일드 카드
public void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
주로 인수로 사용할 때 어떤 타입이든 받아들이고자 사용된다.
2. 상한 경계 와일드 카드
public void processNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num.doubleValue());
}
}
특정 타입의 하위 타입만을 받을 수 있다. 주로 읽기 전용 메서드에 사용된다. Number 타입이나 그 하위 타입인 Integer, Double, Float등을 받아들일 수 있다.
3. 하한 경계 와일드 카드
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
특정 타입의 상위 타입만을 받아들일 수 있다. 주로 쓰기 전용 메서드에 쓰인다. 예시에서는 Integer 타입이나 그 상위 타입인 Number, Object타입을 받아들일 수 있다.
근데 나만 그냥 타입 매개변수와 와일드 카드의 차이를 모르겠는가..? 타입 매개변수도 여러 타입을 받아들일 수 있잖아..?
제네릭 타입 매개변수는 클래스나 메서드를 정의할 때 사용되고, 와일드카드는 클래스나 메서드를 사용할 때, 매개변수로 사용되며 구체적인 타입을 알 필요가 없거나 제한할 때 사용된다고 한다.
내가 이해하기론 와일드 카드는 이미 정의된 제네릭 타입을 다룰 때 사용된다. (아니면 말해주세요)
마지막 예시
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
이렇게 정의된 제네릭 타입 클래스가 있으면
public void printBox(Box<?> box) {
System.out.println(box.getContent());
}
이런 식으로 어떤 타입의 Box라도 받아들일 수 있지만 내부 타입을 알 필요가 없다는 것이다.
'Java' 카테고리의 다른 글
(spring) Exception 이녀석........ (0) | 2024.06.18 |
---|---|
Logger 딱 대 (2) | 2024.06.17 |
Set / Map / Iterator (0) | 2024.06.14 |
익명 클래스 / 람다식 / 메소드 참조 (1) | 2024.06.08 |
형 변환 / valueOf() / stream (0) | 2024.06.07 |