멘토분께서 이런 말씀을 하셨다.
"제네릭을 어떻게 활용하는가가 주니어에서 중니어로 판별이 되는 기준이 될 수 있다!"
그러면 제네릭이 무엇인지부터 알아봐야겠다. Generic을 배웠긴했지만, 그냥 그런가보다 싶었다. 타입을 나중에 정해서 쓰면 되는건가보다 싶었다.
제네릭이란 데이터 타입을 일반화 하는 것을 의미한다. 좀 더 구체적으로 얘기하자면 제네릭은 특정 데이터 타입에 의존하지 않고 일반적인 알고리즘을 작성할 수 있도록 하는 프로그래밍 패러다임이다.
알고리즘을 작성하는데 특정 데이터 타입에만 쓸 수 있는게 아니라, 어떤 데이터 타입도 쓸 수 있게 만들어주는 역할을 한다는 것이다.
그렇기에 제네릭은 주로 컬렉션 라이브러에서 볼 수 있다.
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
위의 HashMap을 보면 고정된 데이터타입을 받아서 쓰는 알고리즘이 아니라 제네릭을 통해 다양한 데이터타입을 받을 수 있게 함으로써 일반적으로 쓸 수 있는 알고리즘이 된 것이다.
이는 곧 코드의 재사용성과 유연성을 증가시켜 같은 알고리즘을 다양한 데이터 타입에 적용할 수 있게 해준 것이다.
타입 매개변수
타입 매개변수는 클래스나 인터페이스 내에서 사용되며, 제네릭한 형태를 가지는 클래스나 인터페이스를 정의할 때 사용된다.
클래스나 인테페이스의 필드, 메서드 반환 타입, 지역 변수 등 여러 곳에서 사용된다.
타입 매개변수는 타입을 파라미터도 받아들이는 역할을 하는 것이다.
package com.example.demospring.DI;
import java.util.List;
public class Drive {
// 타입 매개변수를 이용
public static <T> void printList(List<T> list) {
for (T item : list) {
System.out.println(item);
}
}
public static void main(String[] args) {
// String 타입의 리스트 생성
List<String> stringList = List.of("Hello", "World");
// Integer 타입의 리스트 생성
List<Integer> intList = List.of(1, 2, 3);
// printList 메서드에 각각의 리스트를 전달
printList(stringList);
printList(intList);
}
}
와일드카드
와일드카드는 주로 메서드의 매개변수로 전달되는 인자의 타입을 제한하기 위해 쓰인다. 주로 메서드에서만 사용되며, 클래스나 인터페이스 선언에는 사용되지 않는다.
위의 예시를 와일드카드로 바꿔서 해본다면?
package com.example.demospring.DI;
import java.util.List;
public class Drive {
// 와일드카드를 이용
public static <?> void printList(List<?> list) {
for (? item : list) {
System.out.println(item);
}
}
public static void main(String[] args) {
// String 타입의 리스트 생성
List<String> stringList = List.of("Hello", "World");
// Integer 타입의 리스트 생성
List<Integer> intList = List.of(1, 2, 3);
// printList 메서드에 각각의 리스트를 전달
printList(stringList);
printList(intList);
}
}
안된다! 왜 안될까??
왜냐하면 와일드카드는 타입 매개변수처럼 특정한 타입을 대신하는 것이 아니라 모든 타입을 나타내는 특별한 기호일 뿐이다. 컴파일러에게 해당 메소드가 어떤 타입일지 알려주는 역할을 하는 것 뿐이다.
여자친구의 생일이 다가오자 남자친구는 초조해진다. 대체 어떤 선물을 받고 싶은지 알 수 없기 때문이다. 만약 여자친구가 받고 싶은 선물의 카테고리라도 정해줬으면, 여자친구가 기뻐할 선물을 고를 확률이 훨씬 높아질 것이다.
와일드카드는 모든 타입을 대체할 수 있다. 이것이 장점이지만 때로는 단점이 될 수 있다. 왜냐하면 아예 필요하지 않는 타입도 쓰게 될 수 도 있기 때문이다. 이럴 경우 와일드카드를 사용하여 메서드나 클래스를 설계할 때 타입을 한정적으로 제한함으로써 API의 사용범위를 명확하게 할 수 있다.
와일드 카드는 크게 두가지로 나눌 수 있다.
여기서 공변성과 반공변성이라는 개념이 있는데,
공변성은 타입 클래스 기준으로 타입 클래스의 하위 클래스들을 사용할 수 있다는 것이고 <? extends T>
반공변성은 타입 클래스 타입 클래스의 상위 클래스들을 사용할 수 있다는 것이다. <? super T>
import java.util.ArrayList;
import java.util.List;
class Animal {
public void sound() {
System.out.println("동물 소리를 내다.");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("멍멍");
}
}
class Cat extends Animal {
@Override
public void sound() {
System.out.println("야옹");
}
}
public class Main {
public static void main(String[] args) {
List<? extends Animal> animals = new ArrayList<>();
// animals.add(new Dog()); // 컴파일 에러
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
animals = dogs; // 와일드카드 공변성
makeSounds(animals);
}
public static void makeSounds(List<? extends Animal> animals) {
for (Animal animal : animals) {
animal.sound();
}
}
}
만약 Dog과 Cat이 아닌 Person타입을 상속 받는 Man이 있다면, Animal 타입을 상속 받지 않는 타입인 Man은 makeSounds를 할 수 없도록 만드는 것이다. 만약 Man이 makeSounds를 하게 된다면 그때는 컴파일 에러가 발생하게 된다.
이렇게 와일드카드와 공변성(또는 반공변성)을 활요해서 쓰면 타입의 안정성을 보장하는 동시에 어떤 타입을 써야하는지 알 수 있는 것이다.
참고 자료