[JAVA] 객체지향 프로그래밍 - 다형성의 사용처와 벡터

2023. 12. 6. 13:55프로그래밍 언어/Java

1. 다형성

다형성이란 여러 가지 형태를 가질 수 있는 능력으로, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 하는 기술이다. 구체적으로 말하면, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하는 것이다. 

예를 들어 Tv 클래스를 조상으로 하는 CaptionTv 클래스가 있다고 생각해 보자. 다형성을 통해 Tv 타입의 참조변수로 자손 인스턴스인 new CaptionTv를 참조할 수 있다.

CaptionTv c = new CaptionTv();
Tv t = new CaptionTv();

c와 t는 어떤 차이가 있을까? c와 t 모두 CaptionTv 인스턴스를 담고 있지만 t로는 CaptionTv 인스턴스의 모든 멤버를 사용할 수 없다. Tv타입의 참조변수로는 Tv클래스에게 상속받은 멤버만 사용할 수 있다. 즉, 둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.

 

그렇다면 아래처럼 자손타입의 참조변수로 조상 타입의 인스턴스를 참조할 수 있을까?

CaptionTv c = new Tv();

불가능하다. 그 이유는 실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버의 개수가 더 많기 때문이다. 

 

다형성을 정리하자면 조상 타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있지만 그 반대는 불가능하다.


2. 참조변수의 형변환

참조변수도 기본형 변수와 같이 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스 사이에서만 가능하다. 

기본형 변수의 형변환에서 작은 자료형에서 큰 자료형의 형변환은 생략이 가능하듯이, 참조형 변수의 형변환에서는 자손타입을 조상타입으로 변환하는 업캐스팅은 형변환을 생략할 수 있고, 조상타입을 자손타입으로 변경하는 다운캐스팅은 형변환을 생략할 수 없다. 조상클래스 Car과 자손클래스 FireEngine 간의 형변환을 보자.

Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;

car = fe;
fe2 = (FireEngine)car;

 

형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 인스턴스에 아무런 영향을 미치지 않는다. 단지 참조변수의 형변환을 통해서 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 조절하는 것뿐이다.

따라서 위 예시에서 참조변수 car로는 FireEngine에만 존재하는 메서드를 호출할 수 없다.

 

아직 헷갈린다면 car = fe를 분석해 보자. fe는 FireEngine()의 인스턴스를 저장한 참조변수이다. 이  참조변수를 Car 타입으로 업캐스팅하여 car에 저장했으니 당연히 fe와 car은 같은 인스턴스를 참조하고 있지만, car로는 인스턴스의 모든 멤버에 접근할 수 없는 것이다. 다른 예시도 보자.

Car car = new Car();
FireEngine fe = null;

fe = (FireEngine)car;

이렇게는 가능할까? 불가능하다. car 참조변수에는 Car형 인스턴스가 들어있고, 이 car 참조변수를 다운캐스팅해서 fe에 저장하는 것까지는 문제가 없다. 하지만 car 참조변수에 Car형 인스턴스가 들어있다는 부분이 문제가 된다. 자식 클래스 타입의 참조변수 fe가 부모 클래스 타입의 인스턴스를 담게 되는 것이기 때문이다. 따라서 `Car car = new FireEngine();`과 같이 변경해서 사용해야 한다.

 

서로 상속관계에 있는 타입의 참조변수끼리는 형변환이 양방향 자유롭게 수행될 수 있으나, 인스턴스를 자손타입으로 형변환하는 것은 허용되지 않는다. 그래서 참조변수가 가리키는 인스턴스의 타입을 아는 것이 중요하다.

 

이때 사용할 수 있는 연산자가 instacneof 연산자다. c instacneof FireEngine과 같은 방식으로 사용하는데, c가 FireEngine으로 형변환이 가능한지 검사하는 연산이다. instacneof 연산자로 조건문을 걸어두고 안전하게 형변환을 할 수 있다.

if (car instanceof FireEngine) {
    fe = (FireEngine) car;
} else {
    // 처리할 내용 또는 예외처리 코드 작성
}

이런 식으로 사용하면 아까의 오류를 방지할 수 있다.


3. 참조변수와 인스턴스의 연결

조상의 멤버변수와 같은 이름의 멤버변수를 자손클래스에 중복으로 정의하면, 어떤 타입의 참조변수로 자손 인스턴스를  참조하는지에 따라 어떤 변수가 호출되는지 달라진다. 

메서드의 경우 조상 클래스의 메서드를 오버라이딩한 경우에도 참조변수의 타입에 관계없이 항상 오버라이딩된 메서드가 호출된다. 하지만 멤버변수의 경우에는 조상타입의 참조변수를 사용하면 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용하면 자손 클래스에 선언된 멤버변수가 사용된다. 

 

이제 다형성의 여러 속성들을 알았으니 어떻게 다형성을 이용할 수 있을지 보자.


4. 매개변수의 다형성과 여러 타입의 배열 만들기

예시로 먼저 보자. 아래와 같이 Product, Tv, Computer, Auido, Buyer 클래스가 정의되어 있다고 가정하자. Buyer 클래스는 제품을 구입하는 사람을 표현했다.

// Product 클래스 (조상 클래스)
class Product {
    String brand;
    int price;

    public Product(String brand, int price) {
        this.brand = brand;
        this.price = price;
    }
}

// Tv 클래스 (자손 클래스)
class Tv extends Product {
    private String resolution;

    public Tv(String brand, int price, String resolution) {
        super(brand, price);
        this.resolution = resolution;
    }
}

// Computer 클래스 (자손 클래스)
class Computer extends Product {
    private String processor;

    public Computer(String brand, int price, String processor) {
        super(brand, price);
        this.processor = processor;
    }
}

// Audio 클래스 (자손 클래스)
class Audio extends Product {
    private String model;

    public Audio(String brand, int price, String model) {
        super(brand, price);
        this.model = model;
	}
}

// Buyer 클래스
class Buyer {
    private int money;
    private String preferBrand;
    
    public Buyer(int money, String preferBrand){
    	this.money = money;
        this.preferBrand = preferBrand;
    }
}

Buyer클래스에 제품을 구입하는 기능의 메서드를 추가하고 싶다. 그렇다면 매개변수로 Tv, Computer, Audio를 받는 함수를 각각 따로 작성해야 할 것이다. 각 물건의 가격과 브랜드만 알고 싶은데, 모두 다른 클래스라 어쩔 수 없이 메서드를 각각 구현해야 할 것 같다.

 

이때 다형성을 사용할 수 있다! 매개변수를 Product로 사용해서 간편하게 하나로 구현하는 것이다.

void buy(Product p) {
    if(preferBrand == p.brand)
    	money -= p.price;
}

 


다형성을 또 다르게 응용하면 여러 종류의 객체를 배열로 다룰 수 있다. 아까 위의 예시에서 Product타입의 참조변수 배열로 만들어 보자.

Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Auido();

이처럼 조상타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 객체를 배열로 묶어서 다룰 수 있다. 다만 이 방법도 단점이 있는데, p 배열의 크기가 고정되어 있다는 것이다. 이런 경우, Vector 클래스를 사용하면 된다. Vector 클래스는 C++의 벡터와 유사한 기능으로, 내부적으로 Obejct타입의 배열을 가지고 있다. 따라서 모든 타입을 삽입할 수 있지만 벡터에서 어떤 값을 꺼내도 그 값이 Obejct타입이므로 형변환해서 사용해야 한다.

import java.util.Vector;

public class MixedTypeVectorExample {
    public static void main(String[] args) {
        // 벡터 생성
        Vector<Object> mixedVector = new Vector<>();

        // Tv 객체 추가
        Tv tv = new Tv("Samsung", 1000, "4K");
        mixedVector.add(tv);

        // Computer 객체 추가
        Computer computer = new Computer("Dell", 1200, "Intel i7");
        mixedVector.add(computer);

        // Audio 객체 추가
        Audio audio = new Audio("Sony", 500, "Soundbar");
        mixedVector.add(audio);

        // 객체 출력
        System.out.println("Vector elements:");

        for (Object obj : mixedVector) {
            if (obj instanceof Tv) {
                System.out.println("TV: " + ((Tv) obj).getDisplayInfo());
            } else if (obj instanceof Computer) {
                System.out.println("Computer: " + ((Computer) obj).getDisplayInfo());
            } else if (obj instanceof Audio) {
                System.out.println("Audio: " + ((Audio) obj).getDisplayInfo());
            }
        }
    }
}

이런 식으로 사용할 수 있는데 눈여겨볼 점은, Obejct obj로 벡터를 순회한다는 점과 obj를 사용할 때 instanceof 연산자를 통해 타입을 판단한 후 형변환해서 사용한다는 점이다. 벡터에서 꼭 알아야 하는 메서드 몇 가지만 알아두자.

  • Vector()
    10개의 객체를 저장할 수 있는 벡터 인스턴스를 생성한다. 10개 이상의 인스턴스가 저장되면 자동적으로 크기가 늘어난다.
  • boolean add(Obejct obj)
    벡터의 가장 끝에 obj를 추가한다. 
  • boolean remove(Object obj)
    obj를 찾아서 삭제한다. remove(Integer.valueOf(42)); 와 같이 사용해야 42를 찾아서 삭제한다.
  • boolean remove(int index)
    index번에 있는 데이터를 삭제한다. remove(42); 는 42번째 데이터를 삭제한다.
  • boolean isEmpty()
    벡터가 비어있다면 true를 반환한다.
  • Object get(int index)
    index에 있는 데이터를 반환한다. Object 형태로 반환하므로 사용할 때는 형변환이 필요하다.
  • int size()
    벡터의 사이즈를 반환한다.