제네릭 (1)
제네릭이 필요한 이유
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 String get() {
return value;
}
public void set(String value) {
this.value = value;
}
}
숫자, 문자열을 보관하고 꺼낼 수 있는 단순한 클래스이다.
package mid2.generic.ex1;
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);
}
}
set으로 숫자 또는 문자열을 보관하고, 꺼내서 출력하는 코드이다.
여기서 추가로 Boolean, Double 등등등 다양한 타입을 담는 박스가 필요하다면, 수많은 클래스를 다 추가해야할 것이다.
그렇다면 최상위 클래스인 Object를 사용해서 코드 중복을 줄일 수 있지 않을까?
다형성을 통한 중복 해결 시도
package mid2.generic.ex1;
public class ObjectBox {
private Object value;
public void set(Object object) {
this.value = object;
}
public Object get() {
return value;
}
}
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();
System.out.println("result = " + result);
}
}
Object는 모든 타입의 부모이다. 다형성을 사용해 코드 중복을 줄일 수 있었다. 하지만 반환 타입이 맞지 않아 다운캐스팅을 해야한다.
개발자의 의도는 integerBox에 숫자 타입이 입력되기를 기대했으나.. ObjectBox의 set은 Object 타입이므로 어떤 데이터도 입력
받을 수 있게 된다. 자바의 입장에선 아무 문제가 되지 않는 코드이다.
결국 integerBox에 문자열이 입력됐다.
예외가 발생해 프로그램이 종료되었다.
다형성을 활용한 덕분에 코드의 중복을 제거하고 코드를 재사용할 수 있었다. 하지만 원하지 않는 타입이 들어갈 위험이 생겼다.
다운캐스팅은 항상 어떤 값이 튀어나올지 모른다. 결과적으로 이 방식은 타입 안정성이 떨어지는 것이다.
BoxMain1은 코드 재사용성은 매우 떨어지지만, 타입 안정성은 매우 높아 실수할 확률이 매우 적고
BoxMain2는 코드 재사용성은 매우 높지만, 타입 안정성이 매우 낮다.
이럴 때 제네릭을 적용하는 것이다.
제네릭 적용
제네릭을 사용하면 코드 재사용성과 타입 안정성 두가지를 한번에 해결할 수 있다.
public class GenericBox<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
<>를 사용한 클래스를 제네릭 클래스라고 한다. (<> 를 다이아몬드라고 함)
제네릭을 사용할 땐 미리 타입을 지정하지 않는다. 위 코드에서 T는 타입 매개변수라고 한다.
타입 매개변수는 이후에 Integer, String 등 여러 타입으로 변할 수 있다.
public class BoxMain3 {
public static void main(String[] args) {
GenericBox<Integer> integerBox = new GenericBox<Integer>(); // 생성 시점에 T의 타입 결정
integerBox.set(10);
// integerBox.set("문자"); // 컴파일 오류
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);
}
}
생성 시점에 원하는 타입을 지정하면 된다. Integer라고 저장 했을 시 T (타입 매개변수)는 Integer 타입으로 변한다.
String, Boolean 등 모두 마찬가지다.
Object 타입을 사용한 코드는 Integer 대신 문자열이 들어갔을 때 에러가 발생했지만, 제네릭을 사용하면 컴파일러가 막아준다.
생성 시점에 타입이 변했으므로 다운 캐스팅이 필요가 없어진 것이다.
타입 추론
// 타입 추론 : 생성하는 제네릭 타입 생략 가능
GenericBox<Integer> integerBox2 = new GenericBox<>();
객체를 생성할 때 굳이 Integer를 사용하지 않아도 왼쪽에 있는 변수 타입을 보고 타입 정보를 추론해 정보를 얻을 수 있다.
항상 타입 추론을 하는것이 아니고 읽을 수 있는 타입정보가 주변에 있어야만 추론할 수 있다.
제네릭 용어와 관례
제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다. 즉 실제 사용하는 시점에서 타입을 결정한다.
쉽게 비유하자면 메서드의 매개변수(parameter)와 인자(argument)의 관계와 비슷하다.
메서드에 매개변수를 지정하고, 메서드를 사용할 때 원하는 값을 인자로 전달하면, 사용하는 시점에서 값을 결정하는 것이다.
제네릭도 마찬가지로 타입 변수를 지정하고, 해당 클래스를 실제 사용하는 시점에서 타입을 결정한다.
즉 둘의 차이는 나중에 결정하는게 값이냐 타입이냐의 차이이다.
raw type
package mid2.generic.ex1;
public class RawTypeMain {
public static void main(String[] args) {
GenericBox integerBox = new GenericBox();
// GenericBox<Object> integerBox = new GenericBox<>(); // 권장
integerBox.set(10); // Object 타입으로 간주
}
}
제네릭에 타입 인자를 지정하지 않아도 컴파일러가 에러를 내지 않는다. 왜냐면 예전 제네릭은 원래 있던 기능이 아니고
나중에 추가된 기능인데, 지원하지 않는 버전과 지원 후의 버전의 호환때문에 어쩔 수 없이 에러를 내지 않는 것이다.
raw type을 사용하면, 타입을 Object로 간주하게 된다. 따라서 에러가 발생할 수 있으니 사용하지 말자
제네릭 활용 예제
package generic.animal;
public class Animal {
private String name;
private int size;
public Animal(String name, int size) {
this.name = name;
this.size = size;
}
public int getSize() {
return this.size;
}
public String getName() {
return this.name;
}
public void sound() {
System.out.println("동물 울음소리");
}
@Override
public String toString() {
return "Animal{name='" + this.name + "', size=" + this.size + "}";
}
}
동물의 name, size의 정보를 가지는 부모 클래스 작성, toString을 오버라이딩 했다.
package generic.animal;
public class Dog extends Animal {
public Dog(String name, int size) {
super(name, size);
}
public void sound() {
System.out.println("멍멍");
}
}
Animal을 상속받는 자식 객체 Dog 생성 (Cat 은 생략)
부모 클래스에 정의된 생성자에 맞춰 super()를 호출한다.
package generic.ex2;
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return this.value;
}
}
제네릭 클래스 생성
package generic.ex2;
import generic.animal.Animal;
import generic.animal.Cat;
import generic.animal.Dog;
public class AnimalMain1 {
public static void main(String[] args) {
Animal animal = new Animal("동물", 0);
Dog dog = new Dog("멍멍이", 100);
Cat cat = new Cat("야옹이", 50);
Box<Dog> dogBox = new Box<>();
dogBox.set(dog);
Dog findDog = (Dog) dogBox.get();
System.out.println("findDog = " + findDog);
Box<Cat> catBox = new Box<>();
catBox.set(cat);
Cat findCat = (Cat) catBox.get();
System.out.println("findCat = " + findCat);
Box<Animal> animalBox = new Box<>();
animalBox.set(animal);
Animal findAnimal = animalBox.get();
System.out.println("findAnimal = " + findAnimal);
}
}
Box 제네릭 클래스에 각각 타입에 맞는 동물을 보관하고 꺼낸다.
위에서 언급했던것 처럼 타입 매개변수에 맞게 Box의 <T>가 해당 타입으로 변한다.
(그렇다고 클래스가 생성되는게 아님)
Box<Animal> animalBox = new Box<>();
animalBox.set(animal);
animalBox.set(dog);
animalBox.set(cat);
Animal 타입으로 클래스 생성 시, 부모는 자식을 담을 수 있으므로 dog와 cat도 인자로 사용 가능하다.