Backend/Java

[Java] 객체지향 프로그래밍 2(Object-oriented Programming 2) - TIL 221221

짜잉이 2022. 12. 21. 19:51

📖 자바의 정석 Chapter 07 참고


5. 다형성(polymorphism)

5.1 다형성이란?

  • 객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.
  • 두 클래스가 서로 상속관계에 있을 경우, 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하도록 하는 것도 가능하다.
  • 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다. 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스 멤버 개수보다 같거나 적어야 한다.
// 생성된 인스턴스를 다루기 위해서 인스턴스의 타입과 일치하는 타입의 참조변수를 사용했다.
// 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이다.
Tv t = new Tv();
CaptionTv c = new CaptionTv();


// Tv클래스와 CaptionTv클래스가 서로 상속 관계에 있을 경우,
// 조상 타입의 참조변수로 자손 인스턴스를 참조하도록 하는 것도 가능하다.
Tv t = new CaptionTv();


// CaptionTv인스턴스 2개를 생성하고 참조변수 c와 t가 생성된 인스턴스를 하나씩 참조하고 있다.
// Tv타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv클래스의 멤버들(상속받은 멤버포함)만 사용할 수 있다.
CaptionTv c = new CaptionTv();
Tv t = new CaptionTv();
조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있다.
반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.

 

5.2 참조변수의 형변환

자손타입 -> 조상타입(Up-casting)     : 형변환 생략가능 (기본형 변수의 형변환. 작은 -> 큰 형변환 생략가능)
자손타입 <- 조상타입(Down-casting): 형변환 생략불가

다운캐스팅(down-casting): 조상타입의 참조변수를 자손타입의 참조변수로 변환하는 것
업캐스팅(up-casting): 자손타입의 참조변수를 조상타입의 참조변수로 변환하는 것
  • 서로 상속관계에 있는 클래스사이에서만 참조변수의 형변환이 가능하다.
  • 형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 젼환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다.
  • 참조변수의 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것이다.
  • 형변환을 수행하기 전에 instanceof 연산자를 사용해서 참조변수가 참조하고 있는 실제 인스턴스의 타입을 확인하는 것이 안전하다.
// Car클래스는 FireEngine클래스의 조상. 두 클래스는 서로 상속관계에 있다.
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;

car = fe;              // car = (Car)fe;에서 형변환 생략됨. 업캐스팅
fe2 = (FireEngine)car; // 형변환 생략불가(명시적 형변환). 다운캐스팅
서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다.
그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.

 

5.3 instanceof연산자

  • 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 주로 조건문에서 instanceof연산자를 사용한다.
  • instaceof의 왼쪽에는 참조변수를, 오른쪽에는 타입(클래스명)이 피연산자로 위치한다.
  • instanceof를 이용한 연산결과로 boolean값을 반환하는데, true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻한다. 값이 null인 참조변수에 대해 instanceof연산을 수행하면 false를 결과로 얻는다.
  • 실제 인스턴스와 같은 타입의 instanceof연산 이외에 조상타입의 instanceof연산에도 true를 결과로 얻는다.
// 매서드가 호출될 때, 매개변수로 Car클래스 또는 그 자손 클래스의 인스턴스를 넘겨받겠지만
// 메서드 내에서는 정확히 어떤 인스턴스인지 알 수 없다.
void doWork(Car c) {
    // 그래서 instanceof 연산자를 이용해 참조변수 c가 가리키고 있는 인스턴스의 타입을 체크하고
    // 적절히 형변환한 해야한다.
    if(c instanceof FireEngine) {
        FireEngine fe = (FireEngine)c;
        fe.water();
        ...
    } else if(c instanceof Ambulance) {
        Ambulance a = (Ambulance)c;
        a.siren();
        ...
    }
}

 

5.4 참조변수와 인스턴스의 연결

  • 멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가  사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다. (멤버변수의 경우 참조변수의 타입에 따라 달라진다)
  • 메서드의 경우 조상 클래스의 메서드를 자손 클래스에서 오버라이딩한 경우에도 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩 된 메서드)가 호출된다.
  • 참조변수의 타입에 따라 결과가 달라지는 경우는 조상 클래스의 멤버변수와 같은 이름의 멤버변수를 자손 클래스에 중복해서 정의한 경우 뿐이다.

 

5.5 매개변수의 다형성

class Product {
    int price;      // 제품의 가격
    int bonusPoint; // 제품구매 시 제공하는 보너스점수
    
    Product(int price) {
        this.price = price;
        bonusPoint = (int)(price/10.0); // 보너스점수는 제품가격의 10%
    }
}

class Tv extends Product {
    Tv() {
        // 조상클래스의 생성자 Product(int price)를 호출한다.
        super(100); // Tv의 가격을 100만원으로 한다. 
    }    
        // Object클래스의 toString()을 오버라이딩한다.
        public String toString() { return "Tv"; }
}

class Computer extends Product {
    Computer() { super(200); }
    public String toString() { return "Computer"; }
}

class Buyer { // 고객, 물건을 사는 사람
    int money = 1000;   // 소유금액
    int bonusPoint = 0; // 보너스점수
    
    // 물건을 구입하는 기능의 메서드
    // 메서드의 매개변수에 다형성을 적용해 매개변수를 Product타입의 참조변수 p 하나로 처리했다.
    // 메서드 매개변수로 Product클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다.
    // 다른 제품 클래스를 추가할 때 Product클래스를 상속받기만 하면, buy(Product p)메서드의 매개변수로 받아들여질 수 있다.
    void buy(Product p) {
        if(money < p.price) {
            System.out.println("잔액이 부족하여 물건을 구매할 수 없습니다.");
            return;
        }
        
        money -= p.price;           // 가진 돈에서 구입한 제품의 가격을 뺀다.
        bonusPoint += p.bonusPoint; // 제품의 보너스 점수를 추가한다.
        System.out.println(p + "을/를 구입하셨습니다.");
    }
}

class PolyArgumentTest {
    public static void main(String args[]) {
        Buyer b = new Buyer();
        
        // 고객(Buyer)이 buy(Product p)메서드를 이용해서 Tv와 Computer를 구입하고, 
        // 고객의 잔고와 보너스점수를 출력하는 예제
        b.buy(new Tv());
        b.buy(new Computer());
        
        System.out.println("현재 남은 돈은 " + b.money + "만원입니다.");
        System.out.println("현재 보너스점수는 " + b.bonusPoint + "점입니다.");
    }
}
  • 실행결과
Tv을/를 구입하셨습니다.
Computer을/를 구입하셨습니다.
현재 남은 돈은 700만원입니다.
현재 보너스점수는 30점입니다.

 

5.6 여러 종류의 객체를 배열로 다루기

import java.util.*;

class Product {
    int price;      // 제품의 가격
    int bonusPoint; // 제품구매 시 제공하는 보너스점수

    Product(int price) {
        this.price = price;
        bonusPoint = (int)(price/10.0); // 보너스점수는 제품가격의 10%
    }

    Product() {
        price = 0;
        bonusPoint = 0;
    }
}

class Tv extends Product {
    Tv() {
        // 조상클래스의 생성자 Product(int price)를 호출한다.
        super(100);
    }
    // Object클래스의 toString()을 오버라이딩한다.
    public String toString() { return "Tv"; }
}

class Computer extends Product {
    Computer() { super(200); }
    public String toString() { return "Computer"; }
}

class Audio extends Product {
    Audio() { super(50); }
    public String toString() { return "Audio"; }
}


class Buyer { // 고객, 물건을 사는 사람
    int money = 1000;   // 소유금액
    int bonusPoint = 0; // 보너스점수
    Vector item = new Vector(); // 구입한 제품을 저장하는데 사용될 Vector객체


    // 물건을 구입하는 기능의 메서드
    // 메서드의 매개변수에 다형성을 적용해 매개변수를 Product타입의 참조변수 p 하나로 처리했다.
    // 메서드 매개변수로 Product클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다.
    // 다른 제품 클래스를 추가할 때 Product클래스를 상속받기만 하면, buy(Product p)메서드의 매개변수로 받아들여질 수 있다.
    void buy(Product p) {
        if(money < p.price) {
            System.out.println("잔액이 부족하여 물건을 구매할 수 없습니다.");
            return;
        }
        money -= p.price;           // 가진 돈에서 구입한 제품의 가격을 뺀다.
        bonusPoint += p.bonusPoint; // 제품의 보너스 점수를 추가한다.
        item.add(p);                // 구입한 제품을 Vector에 저장된다.
        System.out.println(p + "을/를 구입하셨습니다.");
    }

    void refund(Product p) { // 구입한 제품을 환불한다.
        if(item.remove(p)) { // 제품을 Vector에서 제거한다.
            money += p.price;
            bonusPoint -= p.bonusPoint;
            System.out.println(p + "을/를 반품하셨습니다.");
        } else { // 제거에 실패한 경우
            System.out.println("구입하신 제품 중 해당 제품이 없습니다.");
        }
    }

    void summary() { // 구매한 물품에 대한 정보를 요약해서 보여준다.
        int sum = 0;          // 구입한 물품의 가격합계
        String itemList = ""; // 구입한 물품목록

        if(item.isEmpty()) { // Vector가 비어있는지 확인한다.
            System.out.println("구입하신 제품이 없습니다.");
            return;
        }

        // 반복문을 이용해서 구입한 물품 총 가격과 목록을 만든다.
        for(int i = 0; i < item.size(); i++) {
            Product p = (Product) item.get(i); // Vector의 i번째에 있는 객체를 얻어온다.
            sum += p.price;
            itemList += (i==0) ? "" + p : ", " + p;
        }
        System.out.println("구입하신 물품의 총 금액은 " + sum + "만원입니다.");
        System.out.println("구입하신 제품은 " + itemList + "입니다.");
    }
}

class PolyArgumentTest {
    public static void main(String args[]) {
        Buyer b = new Buyer();
        Tv tv = new Tv();
        Computer com = new Computer();
        Audio audio = new Audio();

        // 고객(Buyer)이 buy(Product p)메서드를 이용해서 Tv와 Computer를 구입하고,
        // 고객의 잔고와 보너스점수를 출력하는 예제
        b.buy(tv);
        b.buy(com);
        b.buy(audio);
        b.summary();
        System.out.println();
        b.refund(com);
        b.summary();
    }
}
  • 배열은 한 번 설정한 크기를 바꾸기 어렵기 때문에 동적으로 크기를 관리하기 위해서 Vector클래스를 사용한다. Vector클래스는 내부적으로 Object타입 배열을 가지고 있어서, 이 배열에 객체를 추가하거나 제거할 수 있게 작성되어 있다. 

Vector클래스의 주요 메서드

메서드 / 생성자 설명
Vector() 10개의 객체를 저장할 수 있는 Vector인스턴스를 생성한다.
10개 이상의 인스턴스가 저장되면, 자동적으로 크기가 증가된다.
boolean add(Object o) Vector에 객체를 추가한다. 추가에 성공하면 결과값으로 true, 실패하면 false를 반환한다.
boolean remove(Object o) Vector에 저장되어 있는 객체를 제거한다. 제거에 성공하면 true, 실패하면 false를 반환한다.
boolean isEmpty() Vector가 비어있는지 검사한다. 비어있으면 true, 비어있지 않으면 false를 반환한다.
Object get(int index) 지정된 위치(index)의 객체를 반환한다. 반환타입이 Object타입이므로 적절한 타입으로의 형변환이 필요하다.
int size() Vector에 저장된 객체의 개수를 반환한다.