제네릭… 이라고 생각했을 때, 그냥 아무 타입 넣고 싶을 떄 쓰는거 아니야? 라는 막연한 생각만이 있었다.
구현 과제 내줬을 때 제네릭을 떠올리며 썼던 적도 없고… 그냥 개념적으로 알고 있었지 활용까지는 힘든 것 같다.
유데미 자바 강의를 듣고 제가 이해 한 대로 쓰는 것이니, 틀린 부분 있으면 지적 환영 호감도 99 드리겠습니다.
- 자바에서 데이터 타입을 일반화 하는 것을 의미한다.
- 제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시 미리 지정하는 방식이다.
- 런타임 환경에 아무런 영향이 없는 컴파일 시점의 전처리 기술이다.
이것만 보고 제네릭을 이해했다면 그냥 천재하세요.
이해엔 코드가 딱이야… 이코딱
ArrayList<Integer> list = new ArrayList<>();
저저 각괄호가 보이는가? 저게 제네릭이다 이말이야 (<>
를 다이아몬드라고 하더라)
저 괄호 안에 무엇을 넣어도 그에 맞는 ArrayList를 쓸 수 있지 않았나 ?
범용성 있게 데이터 타입에 구애받지 않고 쓸 수 있게 하는 것이 제네릭이다.
→ <>
를 가지는 클래스와 인터페이스를 말한다 ! 메소드도 가능
코드를 보자
public class CustomList {
ArrayList<Integer> list = new ArrayList<>();
public void addElement(int element) {
list.add(element);
}
}
나만의 List를 만들어보자. 인스턴스를 만든다면 원소는 int형만 만들수 있다. 아까도 말했 듯이 데이터 타입에 구애받지 않고 범용성 있도록 만들어준다.
public class CustomList<T> {
ArrayList<T> list = new ArrayList<>();
public void addElement(E element) {
list.add(element);
}
}
제네릭을 활용하면 데이터 타입에 구애 받지 않을 수 있다.
두번째로 불필요한 타입 변환을 제거해준다.
이거슨 또 무슨말이나면
List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0);
제네릭을 사용하지 않으면 이렇게 귀찮게 해줘야 한다. 우리의 시간은 소중하니까 애용하자
마지막으로 컴파일때 알아서 강하게 타입 체크를 해준다.
컴파일 때 에러를 미리 다 잡아버려서, 실행중에 에러 RuntimeException
을 방지해준다.
이렇게 잡아주면 디버깅할 때 편하다고 GPT가 그랬다.
정리
이건 또 뭐냐? 싶겠지만 라이브러리 좀 뜯어본 사람들이라면 많이 봤을 것이다.
public static double sum(List<? extends Number> list) {
double sum = 0d;
for (Number number : list) {
sum += number.doubleValue();
}
return sum;
}
매개변수를 보면 List<? extends Number>
에서 “?”가 와일드 카드다.
저게 머임 ??? 싶겠지만… 쉽게 말해서 제네릭에 올 수 있는 타입을 제한 하도록 쓰는거다.
코드를 대충 해석해보면
리스트를 받는다 → 리스트의 모든 원소들을 더한다 → 그 더한 값을 리턴한다.
근데 더할 수 있는건 상식적으로 숫자만 가능하다.
그러면 Number Class를 상속 받은 애들만 오도록 하는거다. String이나 char이 오면 더하지를 못하니까 !
와일드카드는 제네릭으로 올 수 있는 타입을 제한한다.
와일드 카드는 크게 두개로 볼 수 있다.
extends
를 사용해서 와일드카드 타입의 최상위 타입을 정의한다. 위 예시로 든 코드가 상한 경계 와일드카드라고 볼 수 있겠다.super
를 이용해 와일드카드 타입의 최하위 타입을 정의한다. 요런식으로 말이지static void addNumbers(List<? super Number> numbers) {...}
마무리에서 언급했듯이, 와일드카드가 이해 안되는 부분에 다뤄보겠다.
상한에서 get, 하한에서 set이 안되는 이유를 알아보기 전에 알아야 할 것이 있다.
T<A>
가 T<B>
의 하위 타입이면, T는 공변T<A>
가 T<B>
의 하위 타입이 아니면, T는 불공변갑자기 이걸 왜 알아야하냐고 물어본다면, 자바에서 배열은 공변으로 정했고, 제네릭은 불공변으로 하기로 했기 때문에 알아야한다.
이를 바탕으로 예제 코드를 보자.
public static void main(String[] args) {
Integer[] integers = new Integer[] {1, 2, 3};
printArray(integers);
}
void printArray(Object[] arr) {
for (Object e : arr) {
System.out.println(e);
}
}
위 코드는 배열에 따른 공변을 보여주기 위함이다. 정상적으로 잘 실행되며, Integet는 Obeject를 상속하고, 배열은 공변이므로 Integer를 넣어도 잘 실행된다.
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3);
printCollection(list);
}
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
위 코드는 컴파일 에러가 발생한다. 이유는 제네릭은 불공변이기 때문이다.
그러면 어케함? 이라는 생각이 들텐데, 그래서 와일드카드를 배운 이유가 여기있다.
직감적으로 위 코드를 보았을 때, 될 것 같지만 안되는 상황을 보며 불편함을 호소할 수 밖에 없다. 그래서 와일드카드가 있는거다. 요로케 사용하면 된다.
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3);
printCollection(list);
}
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
하지만 와일드카드는 모든 타입에 대해서가 아닌, 익명의 타입을 뜻한다. (unknown type)
이 부분을 코드로 보자
public static void main(String[] args) {
Collection<?> c = new ArrayList<String>();
c.add(new Object());
위 코드를 보자. 컴파일 에러가 발생한다. 왜???????
일단 add를 해주려면 제네릭타입인, E 혹은 E의 자식을 매개변수로 줘야한다. 근데 와일드카드는 익명이므로 Number, Integer, 사용자 정의 클래스 등등 아무거나 다~넣을 수 있다. add로 넘겨주는 파라미터가 unknown 타입을 상속받은 클래스여야 하는데, 제한을 두지 않아 파라미터가 자식인지 뭔지 구분을 할 수가 없다.
근데 get은 가능하다. 왜냐면 값을 꺼낸 결과가 무엇이든 간에 확인이 필요하지도 않고, 적어도 Object를 상속받는다는 것을 보장가능하기 때문이다.
당신도 이제 제네릭을 쓸 수 있게 되었다.
하지만 와일드카드는 내가 찾다 보니 이해가 좀 많이 안되는 부분이 있었다.
(상한에서 set, 하한에서 get이 외않되?)
20231229추가
조금씩 정리해서 다시 포스팅 하도록 그러도록 노력해보도록 하겠다.
생각보다 자바에 대해 모르는게 천지삐까리였다. 내일도 해야징 나 자신 화이팅!!!!!!!!!!!!!!!
Reference
[Java] 제네릭과 와일드카드 타입에 대해 쉽고 완벽하게 이해하기(공변과 불공변, 상한 타입과 하한 타입) [MangKyu's Diary:티스토리]