다형성

고동현·2024년 6월 14일
0

JAVA

목록 보기
10/22

다형성

다형성을 사용하면 하나의 객체가 다른 타입으로도 사용될 수 있다.

다형적 참조

public class Parent {
    public void parentMethod(){
        System.out.println("Parent.parentMethod");
    }
}
public class Child extends Parent{
    public void childMethod(){
        System.out.println("Child.childMethod");
    }
}
public class PolyMain {
    public static void main(String[] args) {
        //부모 변수가 자식 인스턴스 참조(다형적 참조)
        System.out.println("Parent -> Child");
        Parent poly = new Child();
        poly.parentMethod();
    }
}

PolyMain코드를 보면 부모 변수가 자식 인스턴스를 참조 할 수 있다.
그러나 자식은 부모를 참조 할 수없고, 부모는 자식 메서드를 호출 할 수 없다.
poly.childMethod()->컴파일 오류

new Child()를 했으므로 당연히 자식,부모 인스턴스 둘다 생성이 된다.
다만, 메서드를 호출할때는 항상 메서드에 해당하는 class type을 보고 먼저 호출된다고 하였다.
그러므로 poly에서 호출할 수 있는 메서드는 Parent class에 있는 메서드밖에 없다.(올라갈수는 있어도 내려갈수는 없다.)

그러면 childMethod를 호출하고 싶으면 어떻게해야할까? 캐스팅을 사용해야한다.

캐스팅


우선 poly의 참조값 x001번이 담겨있는 type이 Parent이다.
메서드 호출시 x001.childMethod()이므로 x001번지로 간다.
어? Parent,Child두개가 있네? type Parent이므로 Parent로 간다. childMethod가 없는데 심지어 부모도 없다.(자식으로는 못감) 고로 컴파일 오류가 발생한다.

public class PolyMain {
    public static void main(String[] args) {
        //부모 변수가 자식 인스턴스 참조(다형적 참조)
        System.out.println("Parent -> Child");
        Parent poly = new Child();
        poly.parentMethod();

        Child child = (Child) poly;
        child.childMethod();
    }
}


일시적 Child type으로 다운캐스팅 하는것이다.
(Child) poly -> (Child) x001 -> 참조값을 읽은 다음에 자식타입으로 지정

고로, 캐스팅을 한다고 Parent poly의 타입이 변하는게 아니다.
해당 참조값을 가지고 복사한 참조값이 Child타입이 되는것이다.
poly는 그대로 Parent 타입이 유지된다.

일시적 다운캐스팅

이렇게 Child변수를 선언하고 또 대입하는 과정이 복잡하다.

        ((Child) poly).childMethod();

이렇게 일시적인 다운캐스팅이 가능하다.

Parent type을 일시적으로 Child로 변경한다.
이것도 마찬가지로 poly에서 참조값을 꺼내고 ((Child) x001).childMethod()이므로, 꺼낸 참조값이 Child가 되는것이다.
고로, poly의 타입이 Child로 변하는것이 아니다. Parent로 유지된다.

업캐스팅
Child child = new Child()
Parent parent = child
근데 타입이 다른데 이걸 넣을 수 있을까?
부모는 자식타입을 참조 할 수 있다고는 했지만,
사실 이건 JAVA가 자동적으로 업캐스팅을 해주는것이다.
원래는 Parent parent = (Parent) child 인것이다.
다만 업캐스팅은 자주 사용되므로 생략하는것이 일반적이다.

다운캐스팅은 위험하다.

Parent parent = new Parent();
Child child = (Child) parent;
child.childMethod();//실행불가

(Child) parent에서 다운캐스팅을 할때 ClassCastException이 터진다.

부모를 생성하면 자식 인스턴스가 생성되지 않는다.

child 인스턴스가 없는데 캐스팅을 하려하니까 문제가 생긴다.
즉, 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하면 오류가 터진다.

그러면 해당 인스턴스가 다운캐스팅이 되는지 안되는지 궁금할 수 있다.
instance of를 사용하면 된다.

인스턴스 instance of parent(type)
오른쪽에 있는 type이 왼쪽에 있는 인스턴스를 담을 수 있는지 확인한다.

public class CastingMain {
    public static void main(String[] args) {
        Parent parent = new Parent();
        System.out.println("parent 호출");
        call(parent);

        Parent parent1 = new Child();
        System.out.println("parent 1 호출");
        call(parent1);
    }

    private static void call(Parent parent) {
        parent.parentMethod();
        if(parent instanceof Child){
            System.out.println("Child 인스턴스 맞음");
            ((Child) parent).childMethod();
        }

        if(parent instanceof Child child){
            child.childMethod();
        }
    }
}

call메서드에서 캐스팅이 불가능하여도 항상 paerentMethod는 호출된다.
if문에서 확인을 하는데 parent instance of Child이다.
parent는 child인스턴스이다. 고로 type Child가 담을 수 있다. if문이 호출된다.

자바 16버전에서는 instanceof와 동시에 true라면 child로 인스턴스 참조까지 해준다.

다형성과 메서드 오버라이딩

  • 멤버 변수는 오버라이딩 되지 않는다.
  • 메서드는 오버라이딩이 된다.
 Parent poly = new Child();
        System.out.println("Parent -> Child");
        System.out.println("value =  "+poly.value);
        poly.method();

poly.value를 하면 Parent의 value가 나온다.
poly.method를 호출하면 자식의 method가 호출된다.

당연한게,

poly.value를 하면 Parent Type이므로 x001의 Parent부터 먼저간다. 해당 Parent 인스턴스가 있으므로 parent인스턴스의 value를 가져온다.

메서드는 다르다. 오버라이딩 메서드가 항상 우선권을 가진다.
고로, 오버라이딩 된 메서드를 호출하여 Child의 method가 호출된다.

다형성 활용

Animal

public class Animal {
    public void sound(){}
}

오버라이드를 위해서 sound에는 아무것도 적지 않았다.
Dog

public class Dog extends Animal{
    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

Cat

public class Cat extends Animal{
    @Override
    public void sound() {
        System.out.println("냐옹");
    }
}

Cow

public class Caw extends Animal{
    @Override
    public void sound() {
        System.out.println("음메");
    }
}

Main

public class AnimalPolyMain1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Caw caw = new Caw();

        sound(dog);
        sound(cat);
        sound(caw);
    }

    private static void sound(Animal animal) {
        animal.sound();
    }
}

sound의 파라미터를 Animal 부모로 받아 자식의 오버라이드된 sound메서드를 호출하게 하였다.

animal의 타입이 Animal이므로 부모타입으로 가지만 -> 오버라이드된 자식메서드 호출됨

그런데 문제가 있다.

public class Pig extends Animal{
 //todo sound()
 ...
}

만약 pig가 Animal을 상속받음에도 sound메서드 오버라이딩 하는걸 까먹었다고 치자.
main에서

public class AnimalPolyMain1 {
    public static void main(String[] args) {
        Animal [] animals = {new Dog(),new Cat(), new Caw(),new Pig()};
        for (Animal animal : animals) {
            animal.sound();
        }
    }
}

해도, Pig에는 sound()메서드가 없으므로 Animal의 sound가 호출될것이다.

좋은 프로그램은 제약이 있는 프로그램이다. 추상클래스와 추상 메서드를 사용하면 이런 문제를 해결 할 수 있다.

추상클래스

  • 추상클래스
    부모클래스는 제공하지만, 실제 생성이 불가능한 클래스
    abstract키워드를 사용한다.
public abstract class AbstractAnimal {
    public abstract void sound();
    public void move(){
        System.out.println("동물이 움직입니다.");
    }
}
  • 추상메서드
    자식 클래스가 반드시 오버라이딩 해야하는 메서드를 부모 클래스에 정의하는 메서드.
    실체가 존재하지 않고, 메서드 바디 없다.
    추상메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야한다.
    move메서드는 일반 메서드 인데, 상속받은 자식클래스에서 사용할 수 있도록 넣은 메서드이다. 오버라이딩 하지 않아도 된다.
public class Dog extends AbstractAnimal{
    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}
public class Cat extends AbstractAnimal{
    @Override
    public void sound() {
        System.out.println("냐옹");
    }
}
public class Caw extends AbstractAnimal{
    @Override
    public void sound() {
        System.out.println("음메");
    }
}
public class AbstractMain {
    public static void main(String[] args) {
        AbstractAnimal []animal = {new Dog(),new Cat(),new Caw()};
        for (AbstractAnimal abstractAnimal : animal) {
            abstractAnimal.sound();
            abstractAnimal.move();
        }
    }
}

sound메서드 호출시 자식 class에서 오버라이드 한 메서드가 호출되고,
move는 그냥 상속받은 자식 클래스에서 부모클래스의 move를 호출하는것이다.(자식 클래스에서 move메서드는 오버라이드 하지 않았으므로)

순수 추상클래스

모든 메서드가 추상 메서드인 추상클래스
현재 AbstractAnimal의 move메서드는 추상이 아님, 자식에서 상속받아서 사용하고 있다.

public abstract class AbstractAnimal {
    public abstract void sound();
    public abstract void move();
}

모든 메서드가 추상메서드인 순수 추상 클래스

자식에서 move또한 override해줘야함

public class Cat extends AbstractAnimal {
    @Override
    public void sound() {
        System.out.println("냐옹");
    }

    @Override
    public void move(){
        System.out.println("고양이 이동");
    }
}

순수 추상클래스를 통해서 인터페이스와 같이, 규격을 지켜서 구현해야하는 틀을 마련한 것과 같다.

자바에서는 순수 추상 클래스를 더 편리하게 사용하게 인터페이스라는 개념을 제공한다.

인터페이스

인터페이스의 메서드는 모두 public,abstract이다.
public은 자식이 구현해야하므로 접근해야하니까 public, abstract도 자식이 구현해야하므로
메서드에 public abstract가 생략된다.

public abstract interface AbstractAnimal {
     void sound();
     void move();
}

public abstract void sound()인데 pubic abstract 생략

public class Cat implements AbstractAnimal {
    @Override
    public void sound() {
        System.out.println("냐옹");
    }

    @Override
    public void move(){
        System.out.println("고양이 이동");
    }
}

인터페이스는 extends가 아닌 implements
해당 클래스는 반드시 인터페이스의 메서드를 구현해야함

참고. 인터페이스에 멤버 변수를 넣을 수 있다.

 public interface InterfaceAnimal {
 public static final double MY_PI = 3.14;
 }
 public interface InterfaceAnimal {
 double MY_PI = 3.14;
 }

인터페이스에서 멤버 변수는 public static final이 모두 포함 되었다고 간주한다.

public class AbstractMain {
    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();

        soundAnimal(cat);
        soundAnimal(dog);
    }

    private static void soundAnimal(AbstractAnimal abstractAnimal) {
        abstractAnimal.sound();
        abstractAnimal.move();
    }
}

soundAnimal 메서드의 파라미터를 보면
인터페이스로 구현 클래스를 받아서 호출한다. 그러면 오버라이드 된 구현클래스의 메서드를 다시 호출한다.

인터페이스를 사용해야하는 이유

  • 제약: 인터페이스를 구현하는 곳에서 반드시 인터페이스의 메서드를 구현하라는 규약을 주는것이다.
  • 다중구현: 자바에서 클래스 상속은 부모를 하나마 지정할 수 있다. 반면에 인터페이스는 부모를 여러명 두는 다중 구현(상속)이 가능하다.

인터페이스 - 다중구현

자바가 다중상속을 지원하지 않는이유.

만약에 AirplanCar가 Airplane과 Car를 상속받았다 치자, AirplanCar ac = new AirplanCar()를 하고, ac.move를 호출하면 Airplane의 move를 호출할지 Car의 move를 호출할지 모호한 상황이 된다. -> 다이아몬드 문제
고로, 다중상속을 지원하지 않는다.

그러나 인터페이스는 다르다. 구현 자체를 부모 인터페이스에 하지 않았으므로, 다중 구현이 가능하다.

public interface InterfaceA {
    void methodA();
    void methodCommon();
}
public interface InterfaceB {
    void methodB();
    void methodCommon();
}

자식 구현체

public class Child implements InterfaceA,InterfaceB{

    @Override
    public void methodA() {
        System.out.println("this is interFaceA");
    }

    @Override
    public void methodB() {
        System.out.println("this is interFaceB");
    }

    @Override
    public void methodCommon() {
        System.out.println("this is common");
    }
}

implements를 두개이상한것을 볼 수 있다.

public class InterfaceMain {
    public static void main(String[] args) {
        InterfaceA interfaceA = new Child();
        interfaceA.methodA();
        interfaceA.methodCommon();
        
        InterfaceB interfaceB = new Child();
        interfaceB.methodB();
        interfaceB.methodCommon();
    }
}

InterfaceA는 부모 Child는 자식이므로 부모는 자식을 받을 수 있다.
interfaceA의 methodA를 호출하더라도 오버라이드 된 자식의 메서드가 호출된다.


InterfaceA이던 B이던 관계없이 동일한 methodCommon을 호출하더라도, 오버라이딩 된 Child의 인스턴스 methodCommon()가 호출되므로 다중 구현이 가능하다.

클래스와 다중구현 활용

public abstract class AbstractAnimal {
    public abstract void sound();
    public void move(){
        System.out.println("동물이 이동합니다.");
    }
}

sound는 추상메서드, move는 상속을 위한 메서드로 사용했다.

public interface Fly {
    void fly();
}
public class Dog extends AbstractAnimal{
    @Override
    public void sound() {
        System.out.println("멍멍");
    }

}

dong는 AbstractAnimal을 상속받는다. 반드시 추상메서드 sound를 오버라이드 해줘야한다.

public class Bird extends AbstractAnimal implements Fly {
    @Override
    public void sound() {
        System.out.println("짹짹");
    }

    @Override
    public void fly() {
        System.out.println("새가난다");
    }
}

bird는 AbstractAnimal을 상속받는다. sound를 오버라이딩해준다.
Fly를 구현하고있다. fly메서드도 오버라이드 해줘야한다.

Main

public class SoundFlyMain {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Bird bird = new Bird();

        soundAnimal(dog);
        soundAnimal(bird);

        flyAnimal(bird);
    }

    private static void flyAnimal(Fly fly) {
        fly.fly();
    }

    private static void soundAnimal(AbstractAnimal animal) {
        animal.sound();
    }
}

다형성과 설계

Driver가 차를 운전해야한다고 치자. K3부터 운전해보자


public class K3Car {
    public void startEngine(){
        System.out.println("K3Car.startEngine");
    }
    public void offEngine(){
        System.out.println("K3Car.offEngine");
    }
    public void pressAccelerator(){
        System.out.println("K3Car.pressAccelerator");
    }
}
public class Driver {
    private K3Car k3Car;
    public void setK3Car(K3Car k3Car){
        this.k3Car = k3Car;
    }
    public void drive(){
        System.out.println("자동차를 운전합니다.");
        k3Car.startEngine();
        k3Car.pressAccelerator();
        k3Car.offEngine();
    }
}

중요한점은 Driver는 K3를 알고있다는점이고, 그 다음 해당 k3로 drive메서드를 호출하는것이다.

public class CarMain0 {
    public static void main(String[] args) {
        Driver driver = new Driver();
        K3Car k3Car = new K3Car();
        driver.setK3Car(k3Car);
        driver.drive();
    }
}

근데 여기서 Driver가 Model3차를 또 운전해야한다고 치자, 그러면 많은부분의 코드가 Driver에서 고쳐져야한다.


public class Model3Car {
    public void startEngine(){
        System.out.println("Model3Car.startEngine");
    }
    public void offEngine(){
        System.out.println("Model3Car.offEngine");
    }
    public void pressAccelerator(){
        System.out.println("Model3Car.pressAccelerator");
    }
}

public class Driver {
    private K3Car k3Car;
    private Model3Car model3Car;
    public void setK3Car(K3Car k3Car){
        this.k3Car = k3Car;
    }
    public void setModel3Car(Model3Car model3Car){
        this.model3Car = model3Car;
    }
    public void drive(){
        System.out.println("자동차를 운전합니다.");
        if(k3Car != null) {
            k3Car.startEngine();
            k3Car.pressAccelerator();
            k3Car.offEngine();
        }else if(model3Car !=null){
            model3Car.startEngine();
            model3Car.pressAccelerator();
            model3Car.offEngine();
        }
    }
}
public class CarMain0 {
    public static void main(String[] args) {
        Driver driver = new Driver();
        K3Car k3Car = new K3Car();
        driver.setK3Car(k3Car);
        driver.drive();

        Model3Car model3Car = new Model3Car();
        driver.setK3Car(null);
        driver.setModel3Car(model3Car);
        driver.drive();
    }
}

인터페이스를 도입해보자.

public interface Car {
    void startEngine();
    void offEngine();
    void pressAccelerator();
}
public class K3Car implements Car{
    @Override
    public void startEngine() {
        System.out.println("K3Car.startEngine");
    }

    @Override
    public void offEngine() {
        System.out.println("K3Car.offEngine");
    }

    @Override
    public void pressAccelerator() {
        System.out.println("K3Car.pressAccelerator");
    }
}
public class Model3 implements Car{
    @Override
    public void startEngine() {
        System.out.println("Model3.startEngine");
    }

    @Override
    public void offEngine() {
        System.out.println("Model3.offEngine");
    }

    @Override
    public void pressAccelerator() {
        System.out.println("Model3.pressAccelerator");
    }
}
public class Driver {
    private Car car;
    public void setCar(Car car){
        System.out.println("자동차를 설정합니다:"+car);
        this.car = car;
    }
    public void drive(){
        System.out.println("자동차를 운전합니다.");
        car.startEngine();
        car.pressAccelerator();
        car.offEngine();
    }
}

Driver가 핵심이다. Driver의 필드에는 구현체에 해당하는 K3,Model3가 없다.
결국 Car라는 인터페이스만 알 뿐이다.

public class CarMain2 {
    public static void main(String[] args) {
        Driver driver = new Driver();

        Car k3Car = new K3Car();
        driver.setCar(k3Car);
        driver.drive();

        Car model3Car = new Model3();
        driver.setCar(model3Car);
        driver.drive();
    }
}

Main에서처럼 구현체 k3,model3를 갈아 끼우기만 하면된다. 부모는 자식을 받을 수 있으니까, Car = newK3Car(), new Model3가 가능하다.

OCP원칙

OCP: Open-Closed Principle
확장에는 열려있고, 변경에는 닫혀있어야한다.

Car라는 인터페이스를 보면, k3, model3, 투싼 등등 여러개의 구현체 인스턴스들이 추가 될수 있다. -> 확장에는 열려있다.

차가 추가되더라도 Driver의 메서드 구현부분은 변경할 필요가 없다. -> 변경에는 닫혀있다.

정리

  • Car를 사용하는 클라이언트 코드인 Dirver코드의 변경없이 새로운 자동차를 확장할 수 있다.
  • 전략패턴: 알고리즘을 클라이언트 코드의 변경없이 쉽게 교체가 가능한 패턴
profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글