다형성

2025. 2. 25. 00:02Java/객체지향

1. 다형성 (Polymorphism)

1-1. 다형성이란?

  • 다형성이란 여러가지 형태를 가질수 있는 능력을 의미한다
  • 자바에서는 한 타입의 참조변수로서 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 구현하였다.
  • 즉, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.

1-2. 다형성의 장점

  • 여러 타입의 객체를 하나의 타입으로 관리할 수 있기 때문에 유지보수성과 생산성이 증가된다.
  • 상속을 기반으로 한 기술이기 때문에 상속관계에 있는 모든 객체는 동일한 메세지를 수신할 수 있다. 이런 동일한 메세지를 수신받아 처리하는 내용을 객체별로 다르게 할 수 있다는 장점을 가지고 있다. (다양한 기능을 사용하는데 있어서 관리해야 하는 메세지 종류가 줄어들게 된다.)
  • 확장성이 좋은 코드를 작성할 수 있다.
  • 결합도를 낮춰서 유지보수성을 증가시킬 수 있다.

 

2. 예시를 통한 다형성의 이해 

1) Rabbit이라는 클래스가 Animal이라는 클래스를 상속받아 기능을 확장 (extends)

//부모클래스
public class Animal {
    public void eat(){ System.out.println("Animal eat"); }
    public void run(){ System.out.println("Animal run"); }
    public void cry(){ System.out.println("Animal cry"); }
}

//자식 클래스
public class Rabbit extends Animal {
    @Override
    public void eat() { System.out.println("Rabbit eat"); }
    @Override
    public void run() { System.out.println("Rabbit run"); }
    @Override
    public void cry() { System.out.println("Rabbit cry"); }

    public void jump() { System.out.println("Rabbit jump"); }
}

 

2) 보통은 인스턴스의 타입과 참조변수의 타입이 일치하도록 선언 

Animal animal = new Animal();
Rabbit rabbit = new Rabbit();


3) '부모 타입의 레퍼런스 변수'로 '자식 인스턴스의 주소 값 참조'가 가능하다.

단, 부모 레퍼런스 변수는 자식 인스턴스만의 멤버를 가리킬 수 없음

Animal animal = new Rabbit();

animal.jump(); //불가능, 에러

클래스가 서로 상속관계에 있을 경우, 

부모 클래스 타입의 참조변수(Animal)로 

자식 클래스 인스턴스(Rabbit)를 참조하도록 하는 것이 가능

         
4) 자식 타입의 레퍼런스 변수로 부모 인스턴스의 주소 값을 참조할 수 없다.

Rabbit rabbit = new Animal(); //불가능

실제 인스턴스인 Animal의 멤버 개수보다

참조변수 rabbit이 사용할 수 있는 멤버 개수가 많아지면
rabbit은 이상한 곳을 가리키게 되기 때문에 실행 시 ClassCastException이 발생 (컴파일 에러는 발생 안함)
그러므로 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수와 같거나 그보다 적어야한다.

 

5) 동적 바인딩

Animal animal = new Rabbit();
animal.cry(); // Rabbit cry

부모 타입의 레퍼런스 변수로 자식 인스턴스의 주소 값을 참조한 경우 

인스턴스 animal이

컴파일 당시에는 레퍼런스 변수 타입(Animal)의 메소드와 연결되어 있다가 - 정적 바인딩
런타임 시에 실제 객체(Rabbit)가 가진 오버라이딩 된 메소드로 동작 - 동적 바인딩

 

3. 참조변수의 형변환

참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조정하는 것

 

업캐스팅 (Upcasting) 다운캐스팅(Downcasting)
자식 -> 부모 부모 -> 자식
다룰 수 있는 멤버를 잘라내는 거니까 안전 
다룰 수 있는 멤버를 늘리는 거니까 위험
묵시적/자동 형변환 명시적/강제 형변환

자식 클래스의 고유 메서드는 직접 호출 불가 
 
부모 클래스의 메서드만 호출할 수 있는데
메서드가 override된 경우
오버라이딩된 자식 클래스의 메서드가 실행됨 (동적 바인딩) 


자식 클래스 고유 메서드 직접 호출 가능
(자식 클래스를 참조하고 있으므로 ) 

  부모 타입이 실제로 자식 객체를 참조하는 경우에만 가능.
(즉, 업캐스팅된 참조변수를 대상으로만 다운캐스팅이 가능)

 

//1. 업 캐스팅: 자식->부모
//자식 클래스 객체를 부모 타입으로 변환하는 것.
Animal animal = (Animal) new Rabbit(); //업캐스팅 
✅ Animal animal2 = new Rabbit(); //업캐스팅 자동으로 진행됨

✅ animal.run();  //Animal 클래스의 메서드 호출 가능
❌ animal.jump(); //Rabbit 클래스 고유의 메서드는 호출 불가 

//2. 다운 캐스팅: 부모->자식 
//부모 참조변수가 자식 객체를 참조하는 경우, 부모타입 참조변수를 자식타입 참조변수로 변환 
Animal animal3 = new Rabbit(); // 업캐스팅, 부모 참조변수가 자식 객체를 참조 
✅ Rabbit rabbit = (Rabbit) animal3; // 다운캐스팅 (강제 형변환 필요)  
❌ Rabbit rabbit2 = animal3; // 컴파일 오류 (다운캐스팅은 자동 변환 불가능)

❌ Rabbit rabbit = (Rabbit) new Animal(); //잘못된 다운캐스팅시 런타임 오류!

//3. 자식 클래스의 고유 메서드 직접 호출 비교 
Animal animal4 = (Animal) new Rabbit();
//1) 자식을 부모로 업캐스팅한 animal4 참조변수는 Rabbit클래스의 고유 메서드 호출이 불가능하지만
❌ animal4.jump(); //불가능 
//2) 부모를 자식으로 다운캐스팅한 rabbit 참조변수는 실제로 Rabbit 객체를 참조하고 있기 때문에 호출 가능해짐
✅ ((Rabbit)animal4).jump(); //가능

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

 

4. instanceof 연산자

  • 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위한 연산자
  • (참조변수) instanceof (타입명) -> 연산결과로 true, false값
  • 실제 인스턴스와 같은 타입뿐만 아니라 부모타입의 연산에도 true값을 결과로 얻는다.
Object obj = new Rabbit();  // Object 타입 변수에 Rabbit 객체 저장
Animal animal = new Rabbit(); // Animal 타입 변수에 Rabbit 객체 저장
Rabbit rabbit = new Rabbit(); // Rabbit 타입 변수에 Rabbit 객체 저장

// ✅ obj는 Object, Animal, Rabbit 중 어떤 타입일까?
System.out.println(obj instanceof Object); // true (모든 클래스는 Object를 상속)
System.out.println(obj instanceof Animal); // true (Rabbit → Animal)
System.out.println(obj instanceof Rabbit); // true (Rabbit 자체)

// ✅ animal은?
System.out.println(animal instanceof Object); // true (Animal → Object)
System.out.println(animal instanceof Animal); // true (animal은 Animal)
System.out.println(animal instanceof Rabbit); // true (animal이 Rabbit 객체를 가리킴)

// ✅ rabbit은?
System.out.println(rabbit instanceof Object); // true (Rabbit → Object)
System.out.println(rabbit instanceof Animal); // true (Rabbit → Animal)
System.out.println(rabbit instanceof Rabbit); // true (Rabbit 자체)
  • 안전한 다운캐스팅(부모->자식)을 위해 instanceof 연산자를 사용한다.
    • 다운캐스팅은 부모 타입이 자식 객체를 참조하는 경우에만 가능.
    • 즉, 자식 -(업캐스팅)-> 부모 -(다운캐스팅)-> 자식 
Animal animal = new Rabbit(); 
// ✅ 잘못된 다운캐스팅 방지
if (animal instanceof Rabbit) {
    Rabbit r = (Rabbit) animal;  // 안전한 다운캐스팅
    r.jump();
}

// ❌ 잘못된 경우
Animal a2 = new Animal(); //업캐스팅된 부모객체가 아님!
System.out.println(a2 instanceof Rabbit); // false (Animal은 Rabbit이 아님!)

if (a2 instanceof Rabbit) {
    Rabbit r2 = (Rabbit) a2; // 🚨 실행되지 않음 (안전한 코드)
} else {
    System.out.println("a2는 Rabbit이 아니므로 다운캐스팅 불가능!");
}

        

 

5. 다형성의 활용

5-1. 매개변수의 다형성

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

class Rabbit extends Animal {
    void makeSound() {
        System.out.println("Rabbit squeaks");
    }
}

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

public class Main {
    // 부모 타입(Animal)으로 매개변수를 받음 → 다형성 활용
    static void printSound(Animal animal) {
        animal.makeSound(); // 재정의된 메서드가 실행됨 (오버라이딩)
    }

    public static void main(String[] args) {
        Animal rabbit = new Rabbit();
        Animal dog = new Dog();

        printSound(rabbit); // Rabbit squeaks
        printSound(dog); // Dog barks
    }
}
  • 부모 타입을 매개변수로 사용하면 다양한 자식 객체를 전달할 수 있다.
  • 유지보수성과 확장성이 향상됨.
    • printSound(Animal animal) 하나만 만들면 Rabbit이든 Dog이든 다 받을 수 있음
    • 새로운 동물이 추가되더라도 printSound 메서드는 변경할 필요 없음 

5-2. 여러 종류의 객체를 배열로 다루기

public class Main {
    public static void main(String[] args) {
        // Animal 타입 배열 선언
        Animal[] animals = new Animal[3];

        // 다양한 자식 객체 저장 (업캐스팅)
        animals[0] = new Animal();
        animals[1] = new Rabbit();
        animals[2] = new Dog();

        // 반복문을 이용해 모든 객체의 메서드 실행 (다형성 활용)
        for (Animal animal : animals) {
            animal.makeSound(); // 각각의 오버라이딩된 메서드가 실행됨
        }
    }
}
  • 부모 타입 배열을 사용하면 다양한 자식 객체를 하나의 배열에 저장할 수 있다. (Rabbit, Dog 모두 Animal[] 배열에 저장 가능)
  • 이를 통해 객체를 일괄 처리하는 것이 가능해짐. (for 문을 사용해 모든 객체의 메서드를 실행할 수 있음)
  • 새로운 동물 클래스가 추가되더라도, 배열을 수정할 필요 없이 사용 가능