여러 재료가 쓰이는 3D printer를 예시로 들어보자.
public class TreeDPrinterPlastic {
private Plastic material;
public Plastic getMaterial() {
return material;
}
public void setMaterial(Plastic material) {
this.material = material;
}
}
public class TreeDPrinterPowder {
private Powder material;
public Powder getMaterial() {
return material;
}
public void setMaterial(Powder material) {
this.material = material;
}
}
이렇게 plastic, powder를 사용하는 3D printer가 각각 생성되어 있다.
만약 여기서 뼈대는 같고 재료만 바꿀 수 있는 3D printer를 생성해보자.
하지만, 자료형이 Plastic, Powder 2가지이기에 어떻게 만들어야할지 감이 안잡힌다.
모든 참조 자료형을 사용하고 싶을 때 바로 Object를 사용하면 된다. Object 클래스는 모든 클래스의 최상위 클래스이고 모든 클래스는 Object로 형변환이 일어난다. 어떤 타입이 들어오던 Object 타입으로 변환되어 사용이 가능하다.
public class TreeDPrinter {
private Object material;
public Object getMaterial() {
return material;
}
public void setMaterial(Object material) {
this.material = material;
}
}
재료를 Object로 선언함으로써 Plastic, Powder 등 어떤 자료형이든 사용이 가능하다.
public class TreeDPrinterTest {
public static void main(String[] args) {
TreeDPrinter printer = new TreeDPrinter();
printer.setMaterial(new Powder());
// 다운캐스팅
Powder powder = (Powder) printer.getMaterial();
}
}
하지만 이럴 경우 모든 자료형이 Object로 업캐스팅되기 때문에 그 자료형으로 결과를 다시 받을 때는 다운캐스팅을 해야한다.
변수의 선언이나 메서드의 매개변수를 하나의 참조 자료형이 아닌 여러 자료형으로 변환될 수 있도록 프로그래밍하는 방식을 말한다. 실제로 사용되는 참조 자료형으로의 변환은 컴파일러가 검증하므로 안정적이며 대부분의 컬렉션 프레임워크에서 많이 사용되고 있다.
여러 참조형으로 대체될 수 있는 부분을 하나의 문자로 표현한다. 이 문자를 자료형의 매개변수라고 한다.주로 T(type), E, V를 사용한다.
위의 예시를 제너릭 프로그래밍 방식으로 바꿔보자.
public class TreeDPrinter<T> {
private T material;
public T getMaterial() {
return material;
}
public void setMaterial(T material) {
this.material = material;
}
}
이렇게 사용할 타입이 대체될 곳에 문자 하나를 쓰면 된다.
public class TreeDPrinterTest {
public static void main(String[] args) {
TreeDPrinter<Powder> printer = new TreeDPrinter<Powder>();
printer.setMaterial(new Powder());
Powder powder = printer.getMaterial();
}
}
또한 사용할 때는 문자 대신에 이런 식으로 내가 사용할 참조형 타입을 쓰면 된다. 이 T는 내부적으로 Object로 다시 변환이 되지만, 이 작업은 컴파일러가 직접 캐스팅 작업을 해주기 때문에 우리는 따로 캐스팅할 필요가 없다.
public class Plastic {
@Override
public String toString() {
return "재료는 Plastic입니다.";
}
}
public class Powder {
@Override
public String toString() {
return "재료는 Powder입니다.";
}
}
public class TreeDPrinter<T> {
private T material;
public T getMaterial() {
return material;
}
public void setMaterial(T material) {
this.material = material;
}
@Override
public String toString() {
// 재료를 출력하도록 재정의
return material.toString();
}
}
public class TreeDPrinterTest {
public static void main(String[] args) {
// 파우더를 이용한 프린터
TreeDPrinter<Powder> printer1 = new TreeDPrinter<Powder>();
printer1.setMaterial(new Powder());
Powder powder = printer1.getMaterial();
System.out.println(printer1);
// 플라스틱을 이용한 프린터
TreeDPrinter<Plastic> printer2 = new TreeDPrinter<Plastic>();
printer2.setMaterial(new Plastic());
Plastic plastic = printer2.getMaterial();
System.out.println(printer2);
}
}
재료는 Powder입니다.
재료는 Plastic입니다.
이렇게 제너릭 프로그래밍 방식을 사용하면 하나의 클래스를 만들고 여러 자료형을 같이 쓸 수 있다.
public class Water{
@Override
public String toString() {
return "재료는 Water입니다.";
}
}
TreeDPrinter<Water> printer3 = new TreeDPrinter<Water>();
상식적으로 Water는 3D 프린터의 재료로 쓰일 수 없지만 매개변수 타입을 사용할 경우 어떤 타입이든 문법적으로 오류가 나질 않는다.
이럴 경우에 재료에 대한 제한을 두어야하기 때문에 재료에 대한 상위 클래스를 만들어 관리하면 된다.
public class Plastic extends Material{
@Override
public String toString() {
return "재료는 Plastic입니다.";
}
}
public class Powder extends Material{
@Override
public String toString() {
return "재료는 Powder입니다.";
}
}
public class TreeDPrinter<T extends Material> {
private T material;
public T getMaterial() {
return material;
}
public void setMaterial(T material) {
this.material = material;
}
@Override
public String toString() {
return material.toString();
}
}
그리고 매개변수 T를 선언할 때 Material 클래스를 상속받은 클래스만이 사용 가능하다고 명시해주면 된다.
Water 타입을 사용할 경우 아까와 다르게 에러가 나타나는 것을 알 수 있다.
두 개의 점이 주어지고 x, y는 각각 다른 매개변수로 선언된다. 먼저 Point 클래스를 생성하자.
public class Point<T,V> {
private T x;
private V y;
public Point(T x, V y) {
this.x = x;
this.y = y;
}
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public V getY() {
return y;
}
public void setY(V y) {
this.y = y;
}
}
사각형의 너비를 구하는 메서드가 선언된 GenericMethod 클래스를 추가하자.
public class GenericMethod {
// 제너릭 메서드
public static <T, V> double makeRectangle(Point<T,V> p1, Point<T,V> p2){
double left = ((Number)p1.getX()).doubleValue();
double right = ((Number)p2.getX()).doubleValue();
double top = ((Number)p1.getY()).doubleValue();
double bottom = ((Number)p2.getY()).doubleValue();
double width = right - left;
double height = bottom - top;
return width*height;
}
}
public class GenericMethodTest {
public static void main(String[] args) {
Point<Integer,Double> p1 = new Point<Integer,Double>(0,0.0);
Point<Integer,Double> p2 = new Point<Integer,Double>(10,10.0);
double rect = GenericMethod.<Integer,Double>makeRectangle(p1,p2);
System.out.println("두 점으로 만들어진 사각형의 넓이는 "+rect+"입니다.");
}
}
두 점으로 만들어진 사각형의 넓이는 100.0입니다.