우선 제네릭(Generic)
을 이해하기 위해 제네릭이 왜 필요한지부터 알아보자
아래 코드를 보면 Integer와 String 값을 담는 Box 클래스들을 생성해주었다.
public class IntegerBox {
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
}
public class StringBox {
private String value;
public void set(String value) {
this.value = value;
}
public String get() {
return value;
}
}
public class BoxMain1 {
public static void main(String[] args) {
IntegerBox integerBox = new IntegerBox();
integerBox.set(10); //오토 박싱
Integer integer = integerBox.get();
System.out.println("integer = " + integer);
StringBox stringBox = new StringBox();
stringBox.set("hello");
String str = stringBox.get();
System.out.println("str = " + str);
}
}
IntegerBox에 값을 넣어주고 다시 꺼내어 출력해주는 코드다.
StringBox도 마찬가지로 동작한다.
이렇게만 보면 문제가 없어보이지만, 만약에 Double 타입이나 Boolean 타입을 담는 클래스가 필요하다고 한다면 그 때마다 새로운 클래스를 추가해서 정의해줘야할 것이다.
타입이 달라져도 코드의 구조는 계속 반복되는데 새로 생성해야하니 재사용성이 상당히 떨어진다.
그럼 재사용성을 늘리려면 어떻게 해야될까?
public class ObjectBox {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
위와 같이 모든 타입을 담을 수 있는 ObjectBox를 만들어주었다.
public class BoxMain2 {
public static void main(String[] args) {
ObjectBox integerBox = new ObjectBox();
integerBox.set(10);
Integer integer = (Integer) integerBox.get(); //Object -> Integer 캐스팅
System.out.println("integer = " + integer);
ObjectBox stringBox = new ObjectBox();
stringBox.set("hello");
String str = (String) stringBox.get(); //Object -> String 캐스팅
System.out.println("str = " + str);
//잘못된 타입의 인수 전달 시
integerBox.set("문자100");
Integer result = (Integer) integerBox.get(); //String -> Integer 캐스팅 예외
System.out.println("result = " + result);
}
}
ObjectBox는 Integer, String 그 외 모든 타입을 담을 수 있다.
대신에 반환값 또한 Object 타입이기 때문에 다운캐스팅을 통해서 사용해야된다.
만약 String 값을 담았는데 꺼낼 때 Integer 값으로 받아오면?
위 코드를 실행하면 캐스팅 예외가 발생한다.
재사용성은 높였으나 타입 안정성은 떨어지는 코드로 바뀌었다.
둘 다 동시에 챙길 수는 없을까?
이럴 때 사용하는 게 바로 제네릭
이다.
public class GenericBox<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
위 코드에서 <>
라는 다이아몬드 연산자를 사용했다.
<>
안에 T
라는 타입 매개변수를 넣어주었다.
필드와 매개변수, 그리고 메서드의 타입에도 T
라는 타입이 선언되어있다.
이 T
에 타입 인자를 넣어주면 T
로 선언한 타입들을 해당 타입 인자의 타입으로 바꾸어 사용할 수 있게 된다.
public class BoxMain3 {
public static void main(String[] args) {
GenericBox<Integer> integerBox = new GenericBox<Integer>(); //생성 시점에 T의 타입 결정
integerBox.set(10);
// integerBox.set("문자100"); //Integer 타입만 허용, 컴파일 오류
Integer integer = integerBox.get(); //Integer 타입 반환(캐스팅 X)
System.out.println("integer = " + integer);
GenericBox<String> stringBox = new GenericBox<String>();
stringBox.set("hello");
String str = stringBox.get();
System.out.println("str = " + str);
//원하는 모든 타입 사용 가능
GenericBox<Double> doubleBox = new GenericBox<>();
doubleBox.set(10.5);
Double doubleValue = doubleBox.get();
System.out.println("doubleValue = " + doubleValue);
//타입 추론: 생성하는 제네릭 타입 생략 가능
GenericBox<Integer> integerBox2 = new GenericBox<>();
}
}
main 메서드의 첫줄부터 살펴보면 GenericBox에 Integer라는 타입 인자를 넣어주어 Generic 타입 객체를 생성해주었다.
10이라는 값을 넣어주고 꺼내서 사용하면 다운 캐스팅 없이 Integer 값을 반환받을 수 있다.
타입 인자로 넣어준 Integer를 보고 추론해서 반환 타입을 결정해주기 때문이다.
여기서 만약에 "문자100" 이라는 String 값을 넣어주면 컴파일 오류가 발생한다.
마찬가지로 Integer 아래의 String과 Double을 타입 인자를 넣어줘도 각각의 타입에 맞는 값을 설정하고 반환받을 수 있다.
참고로 Generic 타입 객체를 생성할 때는 타입 매개변수를 생략 가능하다.
GenericBox<Integer> integerBox = new GenericBox<Integer>();
GenericBox<Integer> integerBox = new GenericBox<>();
변수에만 제네릭 타입의 타입 인자를 적어주면 제네릭 객체를 생성할 때는 컴파일러가 타입을 추론하여 동작한다.
Reference