JAVA - 다형성

흑이·2022년 5월 18일
0

다형성

다형성이란 하나의 객체가 여러 가지 형태를 가질 수 있는 것을 의미한다.

영어에서 ‘여러 개'를 의미하는 poly 와 어떤 ‘형태' 또는 ‘실체’를 의미하는 morphism 를 결합해서 ‘여러 가지의 형태'를 의미한다고 할 수 있습니다.

자바에서 다형성은 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미한다.

  • 상위 클래스 타입의 참조변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용한 것

주의할 점

참조변수가 사용할 수 있는 멤버의 개수는 실제 객체의 멤버 개수보다 같거나 적어야한다는 점


예시

//참조변수의 다형성 예시

class Friend {
    public void friendInfo() {
        System.out.println("나는 당신의 친구입니다.");
    }
}

class BoyFriend extends Friend {
   
    public void friendInfo() {
        System.out.println("나는 당신의 남자친구입니다.");
    }
}

class GirlFriend extends Friend {
    
    public void friendInfo() {
        System.out.println("나는 당신의 여자친구입니다.");
    }
}

public class FriendTest {

    public static void main(String[] args) {
        Friend friend = new Friend(); // 객체 타입과 참조변수 타입의 일치
        BoyFriend boyfriend = new BoyFriend();
        Friend girlfriend = new GirlFriend(); // 객체 타입과 참조변수 타입의 불일치

        friend.friendInfo();
        boyfriend.friendInfo();
        girlfriend.friendInfo();
    }
}

//Output
나는 당신의 친구입니다.
나는 당신의 남자친구입니다.
나는 당신의 여자친구입니다.

GirlFriend 클래스의 인스턴스를 생성하고 그것을 Friend 타입의 참조변수 girlfriend에 할당하고 있습니다.

원래라면 타입을 일치시키기 위해 GirlFriend를 참조변수의 타입으로 지정해주어야 하지만, 그러지 않고 상위 클래스 Friend를 타입으로 지정해주고 있습니다.

이것이 ‘상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조하는 것’이자 자바 다형성의 핵심적인 부분이라 할 수 있습니다.


상위 클래스의 타입으로 하위 클래스 타입의 객체를 참조하는 것은 가능하지만, 그 반대는 성립되지 않는다


public class FriendTest {

    public static void main(String[] args) {
        Friend friend = new Friend(); // 객체 타입과 참조변수 타입의 일치 -> 가능
        BoyFriend boyfriend = new BoyFriend();
        Friend girlfriend = new GirlFriend(); // 객체 타입과 참조변수 타입의 불일치 -> 가능
	//  GirlFriend friend1 = new Friend(); -> 하위클래스 타입으로 상위클래스 객체 참조 -> 불가능

        friend.friendInfo();
        boyfriend.friendInfo();
        girlfriend.friendInfo();
    }
}

그 반대로 하위 클래스 GirlFriend의 타입으로 상위 클래스 객체 Friend를 참조하는 것은 불가능하다.

그 이유는 실제 객체인 Friend의 멤버 개수보다 참조변수 friend1이 사용할 수 있는 멤버 개수가 더 많기 때문입니다.


다형성의 장점은?

코드의 중복을 줄여 보다 편리하고 효과적으로 프로그래밍을 수행할 수 있다.

  • 메서드 오버라이딩과 메서드 오버로딩 또한 다형성의 한 예시

참조변수의 타입 변환

참조 변수의 타입 변환은 사용할 수 있는 멤버의 개수를 조절하는 것을 의미

  1. 서로 상속관계에 있는 상위 클래스 - 하위 클래스 사이에만 타입 변환이 가능합니다.
  2. 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형변환 연산자(괄호)를 생략할 수 있습니다.
  3. 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형변환 연산자(괄호)를 반드시 명시해야합니다.

예시
public class VehicleTest {
    public static void main(String[] args) {
        Car car = new Car();
        Vehicle vehicle = (Vehicle) car; // 상위 클래스 Vehicle 타입으로 변환(생략 가능)
        Car car2 = (Car) vehicle; // 하위 클래스 Car타입으로 변환(생략 불가능)
        MotorBike motorBike = (MotorBike) car; // 상속관계가 아니므로 타입 변환 불가 -> 에러발생
    }
}

class Vehicle {
    String model;
    String color;
    int wheels;

    void startEngine() {
        System.out.println("시동 걸기");
    }

    void accelerate() {
        System.out.println("속도 올리기");
    }

    void brake() {
        System.out.println("브레이크!");
    }
}

class Car extends Vehicle {
    void giveRide() {
        System.out.println("다른 사람 태우기");
    }
}

class MotorBike extends Vehicle {
    void performance() {
        System.out.println("묘기 부리기");
    }
}

Vehicle 클래스가 있고, 이로부터 각각 상속을 받아 만들어진 CarMotorBike 클래스가 있습니다.

먼저 Car 클래스의 인스턴스 객체 car를 생성하고, 그 객체를 가리키는 참조변수 vehicle의 타입을 Vehicle로 지정하여 참조변수의 타입변환을 실시하였습니다.

그 후 반대로 vehicle를 하위 클래스 타입인 Car로 타입 변환하여 참조변수 car2에 할당하였습니다.

Car 클래스와 MotorBike 클래스는 상속관계가 아니므로 타입 변환이 불가하여 에러가 발생



instanceof 연산자

instanceof 키워드는 캐스팅이 가능한 지 여부를 boolean 타입으로 확인할 수 있는 자바의 문법요소 이다.

  • 캐스팅 가능 여부를 판단하기 위해서는
  1. ‘객체를 어떤 생성자로 만들었는가’
  2. ‘클래스 사이에 상속관계가 존재하는가’를 판단해야 합니다.

프로젝트의 규모가 커지고, 클래스가 많아지면 매번 이러한 정보를 확인하는 것이 어려워집니다.

이를 해결하기 위해 자바는 instanceof라는 연산자를 제공

만약 참조_변수 instanceof 타입을 입력했을 때 리턴 값이 true가 나오면 참조 변수가 검사한 타입으로 타입 변환이 가능하며

반대로 false가 나오는 경우에는 타입 변환이 불가능합니다. 만약에 참조 변수가 null인 경우에는 false를 반환합니다.


예시

public class InstanceOfExample {
    public static void main(String[] args) {
        Animal animal = new Animal();
        System.out.println(animal instanceof Object); //true
        System.out.println(animal instanceof Animal); //true
        System.out.println(animal instanceof Bat); //false

        Animal cat = new Cat();
        System.out.println(cat instanceof Object); //true
        System.out.println(cat instanceof Animal); //true
        System.out.println(cat instanceof Cat); //true
        System.out.println(cat instanceof Bat); //false
    }
}

class Animal {};
class Bat extends Animal{};
class Cat extends Animal{};

Animal 클래스가 있고, BatCat 클래스가 각각 Animal 클래스를 상속받고 있습니다.

각각 객체를 생성하여 Animal 타입의 참조변수에 넣고 instanceof 키워드를 사용하여 형변환 여부를 확인

Cat 객체를 예로 들어보면, 생성된 객체는 Animal타입으로 선언되어있지만 다형적 표현 방법에 따라 ObjectAnimal 타입으로도 선언될 수 있다는 점을 확인

생성 객체의 타입을 확인하기가 어려운 상황에서 instanceof는 매우 유용한 수단이 될 수 있습니다.



다형성의 활용 예제

손님이 카페에 방문하여 커피를 소비하는 시나리오

class Coffee {
    int price;

    public Coffee(int price) {
        this.price = price;
    }
}

class Americano extends Coffee {};
class CaffeLatte extends Coffee {};

class Customer {
    int money = 50000;
}

커피의 가격 정보를 담고있는 Coffee 클래스가 있고, 이를 상속받는 Americano 클래스와 CaffeLatte 클래스가 아직 구현부가 작성되지 않은 상태로 존재합니다.

마지막으로 Customer 클래스는 커피를 구매하는 손님을 의미하며, 기본적으로 5만원의 돈을 가지고 있다고 가정


다음으로 이 5만원의 돈을 가지고 아메리카노 한 잔과 카페라떼 한 잔을 구입하는 경우를 생각해보면, 어떻게 할 수 있을까

void buyCoffee(Americano americano) { // 아메리카노 구입
	money = money - americano.price;
}

void buyCoffee(CaffeLatte caffeLatte) { // 카페라테 구입
  money = money - caffeLatte.price;
}

사야하는 커피가 무엇인지 구분하기 위해서는 매개변수로 커피에 대한 정보를 전달 받아야하기 때문에 매개변수로 각각 Americano 타입과 CaffeLatte 타입의 객체를 전달

만약에 손님이 구입해야하는 커피의 종류가 한 두개가 아니라 수 십 수 백 가지가 된다면

그 경우에는 매번 새로운 타입을 매개변수로 전달해주는 buyCoffee 메서드를 계속 추가해주어야 할 것이다..


객체의 다형성을 활용하여 해결

void buyCoffee(Coffee coffee) { // 매개변수의 다형성
        money = money - coffee.price;
    }

상위클래스인 Coffee의 타입을 매개변수로 전달받으면, 그 하위클래스 타입의 참조변수면 어느 것이나 매개변수로 전달될 수 있고 이에 따라 매번 다른 타입의 참조변수를 매개변수로 전달해주어야하는 번거로움을 훨씬 줄일 수 있습니다.


전체적인 코드

package package2;

public class PolymorphismEx {
  public static void main(String[] args) {
    Customer customer = new Customer();
    customer.buyCoffee(new Americano());
    customer.buyCoffee(new CaffeLatte());

    System.out.println("현재 잔액은 " + customer.money + "원 입니다.");
  }
}

class Coffee {
  int price;

  public Coffee(int price) {
    this.price = price;
  }
}

class Americano extends Coffee {
  public Americano() {
    super(4000); // 상위 클래스 Coffee의 생성자를 호출
  }

  public String toString() {return "아메리카노";}; //Object클래스 toString()메서드 오버라이딩
};

class CaffeLatte extends Coffee {
  public CaffeLatte() {
    super(5000);
  }

  public String toString() {return "카페라떼";};
};

class Customer {
  int money = 50000;

  void buyCoffee(Coffee coffee) {
    if (money < coffee.price) { // 물건 가격보다 돈이 없는 경우
      System.out.println("잔액이 부족합니다.");
      return;
    }
    money = money - coffee.price; // 가진 돈 - 커피 가격
    System.out.println(coffee + "를 구입했습니다.");
  }
}

//Output
아메리카노를 구입했습니다.
카페라떼를 구입했습니다.
현재 잔액은 41000원 입니다.

buyCoffee() 메서드의 매개변수로 Coffee 타입을 전달해주었습니다. 이제 객체를 생성하고 참조변수를 사용할 때 Coffee 클래스를 상속받기만 하면 buyCoffee(Coffee coffee) 메서드의 매개변수로 전달할 수 있습니다.

0개의 댓글