Light Blue Pointer
본문 바로가기
Job Interview Prep

Java의 다형성(Polymorphism)

by 개발바닥곰발바닥!!! 2024. 5. 31.

다형성(Polymorphism)

OOP의 특성중 하나이다.

다형성은 동일한 인터페이스나 부모 클래스를 공유하는 객체들이 다양한 방식으로 동작할 수 있게 하는 특성을 의미한다

하나의 메서드가 여러 클래스에서 다른 방식으로 구현될 수 있다

Java에서는 오버로딩(Overloading)과 오버라이딩(Overriding) 두 가지 형태로 주로 구현된다.

 

참조 변수의 다형성

자바에서는 다형성을 위해 부모 클래스 타입의 참조 변수로 자식 클래스 타입의 인스턴스를 참조할 수 있도록 하고 있다.

(이때 참조 변수가 사용할 수 있는 멤버의 개수가 실제 인스턴스의 멤버 개수보다 같거나 적어야 참조할 수 있다.)

class Parent { ... }

class Child extends Parent { ... }

...

Parent pa = new Parent(); // 허용

Child ch = new Child();   // 허용

Parent pc = new Child();  // 허용

Child cp = new Parent();  // 오류 발생.

참조 변수의 타입 변환

  1. 서로 상속 관계에 있는 클래스끼리 타입 변환을 할 수 있다
  2. 부모 클래스 = 자식 클래스 → 암시적
  3. 자식 클래스 = (자식 클래스)부모 클래스 → 명시적
class Parent { ... }

class Child extends Parent { ... }

class Brother extends Parent { ... }

...

Parent pa01 = null;

Child ch = new Child();

Parent pa02 = new Parent();

Brother br = null;

 

pa01 = ch;          // pa01 = (Parent)ch; 와 같으며, 타입 변환을 생략할 수 있음.

br = (Brother)pa02; // 타입 변환을 생략할 수 없음.

br = (Brother)ch;   // 직접적인 상속 관계가 아니므로, 오류 발생.

instanceof 연산자

다형성으로 인해 런타임에 참조 변수가 실제로 참조하고 있는 인스턴스의 타입을 확인해야 한다.

Java에서는 instanceof 연산자로 참조 변수가 참조하고 있는 인스턴스의 실제 타입을 확인할 수 있다.

Parent p = new Parent();

System.out.println(p instanceof Object); // true

System.out.println(p instanceof Parent); // true

System.out.println(p instanceof Child);  // false

 

Overloading

하나의 클래스에서 같은 이름의 메서드가 여러 개 있는 것

컴파일러가 메서드 호출시 어느 메서드인지 헷갈리지 않기 위해 메서드 인자의 갯수나 타입은 달라야 한다 (컴파일 시점에 결정)

public class Calculator {
    // 두 정수를 더하는 메서드
    public int add(int a, int b) {
        return a + b;
    }

    // 세 정수를 더하는 메서드
    public int add(int a, int b, int c) {
        return a + b + c;
    }

    // 두 실수를 더하는 메서드
    public double add(double a, double b) {
        return a + b;
    }
}

Overloading의 장점

코드의 가독성이 높아지고 유지보수가 용이해진다

오버로딩을 통해 다양한 입력을 처리할 수 있는 메서드를 정의할 수 있다.

메서드 오버로딩은 메서드 이름을 인자에 따라 다르게 외울 필요 없이 다양한 입력에 대해 동일한 메서드 이름을 사용할 수 있게 한다.

캡슐화의 원칙을 강화

오버로딩은 메서드의 내부 구현을 감추고 동일한 이름의 메서드를 다양한 방식으로 사용할 수 있게 한다

 

 

Overriding

부모클래스를 상속받은 자식클래스에서 부모클래스의 (추상) 메서드를 같은 이름, 같은 반환 값, 같은 인자로 메서드 내의 로직만 새롭게 정의하는 것

부모 클래스의 메서드를 자식 클래스에서 재정의함으로써, 동일한 인터페이스를 사용하면서 각 자식 클래스가 자신만의 방식으로 동작하도록 할 수 있게 한다. (런타임에 결정)

class Animal {
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

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

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

public class Test {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        Animal myCat = new Cat();
        
        myDog.sound(); // Dog barks
        myCat.sound(); // Cat meows
    }
}

 

Overriding의 장점

 

확장성이 높아진다

새로운 기능을 추가할 때 기존 코드를 변경하지 않고도 확장할 수 있게 된다

class Animal {
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

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

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

class Bird extends Animal {
    @Override
    void sound() {
        System.out.println("Bird sings");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        Animal myCat = new Cat();
        Animal myBird = new Bird();
        
        myDog.sound(); // Dog barks
        myCat.sound(); // Cat meows
        myBird.sound(); // Bird sings
    }
}

Bird가 노래하는 기능이 추가되었지만 기존의 Animal 클래스나 Dog, Cat 클래스를 변경할 필요가 없다.

 

유연성이 높아진다

실행 시점에 메서드 호출이 결정되어 객체의 실제 타입에 따라 다른 동작을 수행할 수 있게 한다.

class Animal {
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

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

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

public class Test {
    public static void main(String[] args) {
        Animal myAnimal = getAnimal("dog");
        myAnimal.sound(); // Dog barks
    }
    
    static Animal getAnimal(String type) {
        if (type.equals("dog")) {
            return new Dog();
        } else if (type.equals("cat")) {
            return new Cat();
        } else {
            return new Animal();
        }
    }
}

실행시점에 Dog 객체나 Cat객체를 받아서 그 객체의 타입에 따라 다르게 동작한다

 

코드의 중복을 줄일 수 있다

부모 클래스는 공통 인터페이스를 정의하고, 자식 클래스는 이를 구현한다.

이는 인터페이스와 구현을 분리하는 객체지향 원칙을 따르며, 코드의 재사용성을 높인다.

abstract class Animal {
    abstract void sound();
    
    void sleep() {
        System.out.println("Animal sleeps");
    }
}

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

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

public class Test {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        Animal myCat = new Cat();
        
        myDog.sound(); // Dog barks
        myDog.sleep(); // Animal sleeps
        myCat.sound(); // Cat meows
        myCat.sleep(); // Animal sleeps
    }
}

sleep 메서드는 Animal 클래스에 한 번만 정의되기 때문에 코드의 중복을 줄이고 재사용성을 높일 수 있다.