JAVA - 제너릭 프로그래밍

ubiies·2023년 5월 13일
0

JAVA

목록 보기
3/4

Object를 이용한 자료형 변환

여러 재료가 쓰이는 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로 업캐스팅되기 때문에 그 자료형으로 결과를 다시 받을 때는 다운캐스팅을 해야한다.

Generic

변수의 선언이나 메서드의 매개변수를 하나의 참조 자료형이 아닌 여러 자료형으로 변환될 수 있도록 프로그래밍하는 방식을 말한다. 실제로 사용되는 참조 자료형으로의 변환은 컴파일러가 검증하므로 안정적이며 대부분의 컬렉션 프레임워크에서 많이 사용되고 있다.

제너릭 클래스 정의하기

여러 참조형으로 대체될 수 있는 부분을 하나의 문자로 표현한다. 이 문자를 자료형의 매개변수라고 한다.주로 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로 다시 변환이 되지만, 이 작업은 컴파일러가 직접 캐스팅 작업을 해주기 때문에 우리는 따로 캐스팅할 필요가 없다.

이번에는 toString() 메서드를 추가하여 확인해보자.

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입니다.

이렇게 제너릭 프로그래밍 방식을 사용하면 하나의 클래스를 만들고 여러 자료형을 같이 쓸 수 있다.

이번에는 Powder, Plastic이 아닌 Water 클래스를 추가해보자.

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 타입을 사용할 경우 아까와 다르게 에러가 나타나는 것을 알 수 있다.

이번에는 2개의 매개변수 사용하고, 제너릭 메소드를 생성하여 사각형의 너비를 구하는 문제를 풀어보자.

  1. 두 개의 점이 주어지고 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;
      }
    }
  2. 사각형의 너비를 구하는 메서드가 선언된 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;
    }
}
  1. Test 클래스 생성
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입니다.

0개의 댓글