이펙티브 자바 스터디를 하며 제네릭에 대한 다양한 내용을 접했다.
그런데 이해가 안되는 부분도 있었을 뿐더러, 이렇게 제네릭이 깊은 정의를 갖고 있음을 처음 알게 되어! 정리를 해보려고 한다 :)
일단 개념부터 정리해보자.
타입 인자, 타입 매개변수 등등 여러 용어가 나오는데 비슷하게 생겨서 ㅠㅠ 용어에서 부터 헷갈리면 이해하기가 너무 어려워진다.
해당 블로그와 해당 블로그를 참조했다:)
JDK 5에서 소개된 내용이다. 데이터 타입을 프로그래밍시에 결정하는 것이 아니라 실행할 때 결정하게 하는 기능이다. 실행시 인자로 전달하는 타입을 변수의 타입으로 결정하는 것이다.
클래스, 인터페이스, 메서드에서 사용할 수 있으며 이것을 각각 "제네릭 클래스", "제네릭 인터페이스", "제네릭 메서드"라고 한다.
class 클래스명<타입 매개변수> { }
ex) class MyClass<T> {}
제네릭 클래스의 인스턴스를 생성할 때 타입 매개변수는 인자로 전달 받은 타입으로 대체된다! 이때 타입 매개변수로 전달되는 값을 타입 인자라고 한다.
제네릭 클래스의 타입 매개변수에 타입 인자를 전달하려면 아래와 같이 진행하면 된다.
"교실"에는 "선생님"과 "학생"이 들어갈 수 있으므로 위 코드에서 MyClass를 제네릭 클래스로 선언했고, 아래 코드에서 각각 선생님과 학생을 넣어주고 있다.
new MyClass<Teacher>(new Teacher());
new MyClass<Student>(new Student());
이렇게 되면 제네릭 클래스 내부 T
는 각각 Teacher
, Student
로 대체된다.
JDK7 부터는 타입 인자 생략도 가능하다.
new MyClass<>(new Teacher());
new MyClass<>(new Student());
해당 코드를 통해 코드 재사용성이 늘어났음을 확인할 수 있다! 제네릭의 존재 이유가 슬금슬금 보인다.
클래스명<타입 인자>
로 참조변수 타입을 선언할 수 있다.
MyClass<Teacher> class1 = new MyClass<>(new Teacher());
MyClass<Student> class2 = new MyClass<>(new Student());
예시를 봐도 자유도를 크게 높여주고, 코드 재사용성을 늘려 주고 있음이 파악된다.
자주 언급되는 사용 이유를 살펴보자 :)
제네릭을 사용하지 않고, 위와 같이 Student와 Teacher가 모두 들어가는 MyClass 클래스를 만들고 싶다면 당연하게도 Object를 사용해야할 것이다.
class MyClass<Object> {
private Object person;
public MyClass(Object person) {
this.person= person;
}
public Object getPerson() {
return person;
}
}
이렇게 된다면 getPerson()
을 호출하는 외부 코드에서는 받아온 Object를 Student나 Teacher로 적절히 타입 변경을 해주어야만 할 것이다.
하지만 제네릭을 사용한다면 이러한 과정이 생략된다
MyClass class1 = new MyClass(new Teacher());
MyClass class2 = new MyClass(new Student());
class1 = class2;
위와 같이 제네릭을 사용하지 않는 경우, class1과 class2는 다른 의미임에도 컴파일러는 이를 미리 체크할 수가 없다. (서로 다른 타입이 들어가고 있음을 모르기 때문에)
하지만 아래와 같이 제네릭을 사용한다면, 컴파일러가 이를 알아채고 오류로 해석해준다.
MyClass class1 = new MyClass(new Teacher());
MyClass class2 = new MyClass(new Student());
class1 = class2 // 오류 발생
컴파일러가 엄격한 타입 검사를 하게 되고, 이렇게 되면 컴파일 시점에 문제를 발생할 수 있어 타입 안정성이 높아진다.
타입 매개변수의 다양한 유형에 대해 살펴보자
두 개 이상의 타입 매개변수를 선언할 수 있다!
이땐 콤마를 구분자로 사용한다. 지정된 타입 인자는 순서대로 지정된다.
class MyClass<T, N> {
private T person1;
private N person2;
public MyClass(T person1, N Person2) {
this.person1 = person1;
this.person2 = person2; }
}
위와 같은 클래스를 생성해보자
MyClass<Teacher, Student> myclass = new MyClass<>(new Teacher(), new Student());
이렇게 되면 해당 인스턴스 내의 모든 T
는 Teacher
이 되고, 모든 N
은 Student
가 된다.
위에서는 타입 인자에 어떤 타입을 넣어도 잘 작동됨을 확인할 수 있다. (애초에 제네릭의 목표가 그것이니까)
그런데 타입 인자에 특정한 제한을 두고자하는 상황이 발생할 수 있다. 예를 들어 Teacher, Manager와 같은 성인은 담을 수 있지만, Student와 같은 미성년자는 담을 수 없다고 해보자. (말이 안되긴 하다🤣)
이런 상황에서 타입 매개변수 제한 설정을 하는 것이다.
제한 설정은 <T extends Superclass>
와 같은 형식으로 구현할 수 있는데,
이렇게 되면 T에 들어갈 수 있는 클래스는 슈퍼 클래스나 슈퍼 클래스의 하위 클래스만 가능해진다!
하나의 제한이 아닌 여러 개의 제한을 가질 수도 있다!
이때의 타입 매개변수는 경계로 나열된 모든 타입의 하위 타입이어야한다.
class A {}; // 클래스 A
interface B{}; // 인터페이스B
interface C{}; // 인터페이스C
class D <T extends A & B & C> {};
이때 주의할 점은, 제한 중 클래스가 포함된다면 클래스가 순서상으로 먼저 와야한다는 것이다 :)
일반적으로 사용되는 타입 매개변수명은 아래와 같다 :)
먼저 코드를 통해 확인해보자!
위에서 계속 사용해왔던 MyClass, 즉 교실에 owner가 있다고 생각해보자.
기존 MyClass 클래스에 String owner
라는 필드를 추가하고 이에 관한 getter, setter를 선언했으며, 넘어온 인자에 대해 같은 owner인지 확인하는 isSameOwner()
메소드를 가지고 있다.
class MyClass<T extends Adult> {
private T person;
private String owner;
...
pubilc void setOwner(String owner) {
this.owner = owner;
}
public void getOwner() {
return owner;
}
public booelan isSameOwner(MyClass<T> obj) {
return this.owner.equals(obj.getOwner());
}
}
그런데 해당 코드를 아래와 같이 사용했다고 해보자.
MyClass<Teacher> class1 = new MyClass<>(new Teacher());
MyClass<Manager> class2 = new MyClass<>(new Manager());
class1.setOwner("me");
class2.setOwner("me");
class1.isSameOwner(class2); // -> 예외 발생
당연하게도 class1의 타입 매개변수는 Teacher, class2의 타입 매개변수는 Manager이다. 그래서 class1의 isSameOwner()
메서드에서 사용하는 T인자와 전달된 class2의 T값이 달라서 예외가 발생한다.
하지만 이 예외는 해당 코드의 논리상 적절하지 않다.
class에 누가 들어있든, 해당 교실의 owner가 궁금한것!이기 때문이다.
이런 상황에서 사용하는 것이 바로 와일드 카드, 즉 기호 ? 이다.
와일드 카드는 타입 인자가 무엇이든 나는 모른다~~는 느낌이기 때문에! 이러한 상황에서 적절하다. (나는 모른다~라는 문장이 무슨 의미를 갖는지는 다음 게시물에 설명하고자 한다.)
따라서
public boolean isSameOwner(MyClass<?> obj)
로 선언하면 해결되는 것이다.
와일드 카드에서 마찬가지로 타입을 제한할 수 있다. 종류는 두 가지로 나뉜다.
<? extends 슈퍼클래스>
형식의 제한을 의미한다.
위에서 설명했듯, 슈퍼 클래스 혹은 슈퍼 클래스의 하위 객체만 타입으로 지정할 수 있다.
참조 블로그의 예시로 구체적인 예외 상황을 확인해보자
private static doulbe sum(List<?> lst) {
double total = 0;
for (Number num : lst) {
total += num.doubleValue();
}
return total;
}
위 코드를 보면 당연하게도 lst
의 요소들이 숫자여야함을 알 수 있다. 하지만 와일드 카드를 사용했기 때문에 숫자만 들어올 것이라는 보장은 전혀 없다. 따라서 아래와 같이! 구현하여 상위 제한을 하면 해결된다.
Byte, Short, Integer, Long, Float, Double 모두 Number를 상속받는다.
private static double sum(List<? extends Number> lst) {
<? super 서브클래스>
형식을 갖는다.
서브 클래스 혹은 서브 클래스가 상속하고 있는 상위 객체만 타입으로 지정할 수 있다.
해당 블로그의 예제를 살펴보면
public static void add(List<?> super Integer> lst) {
..
}
해당 메서드는 Integer이거나 상위 타입인 Object, Numbers만 들어갈 수 있음이 확인된다.
위의 클래스와 비슷한 방법으로 형성됩니다!
제네릭 메서드는 제네릭 클래스가 아닌 일반 클래스에서도 정의할 수 있다.
타입 매개변수는 "반환형 앞에" 적어야 한다.
해당 블로그의 예시를 통해 구체적으로 살펴보면 다음과 같다. :)
class Temp {
public static <T> void printAll(T[] arr) {
for(T t : arr) {
System.out.println(t);
}
}
public static void main(String[] args) {
Integer[] nums = {1, 2, 3};
String[] names = {"철수", "영희"};
printAll(nums);
printAll(names);
}
}
잘 보시면 인자로 들어온 타입 매개변수를 반환형에도 사용할 수 있음이 확인된다!
메서드와 동일!
다만 생성자는 반환형이 없으므로 "생성자명" 앞에 적어야 한다 :)
class Temp {
public <T> Temp(T t) {
...