Post

[JAVA] 다형성_1

[JAVA] 다형성_1

다형성이란?

  • 다형성(Polymorphism)은 다양한 형태를 의미.
  • 프로그래밍에서 다형성이란, 하나의 객체가 여러 타입으로 참조될 수 있는 능력을 말한다.

보통 하나의 객체는 하나의 타입으로만 사용된다.

하지만 다형성을 사용하면 하나의 객체가 여러 타입으로 사용될 수 있다.

다형성을 이해하려면 두 가지 개념을 알아야 한다:

  1. 다형적 참조 (Polymorphic Reference)
  2. 메서드 오버라이딩 (Method Overriding)

다형적 참조 (Polymorphic Reference)

다형성을 이해하려면 먼저 다형적 참조 개념을 알아야 한다. 다형적 참조는 부모 타입의 변수로 자식 인스턴스를 참조하는 것 을 의미한다.

1
2
3
4
5
6
7
8
// Parent.java
package poly.basic;

public class Parent {
    public void parentMethod() {
        System.out.println("Parent.parentMethod");
    }
}
1
2
3
4
5
6
7
8
// Child.java
package poly.basic;

public class Child extends Parent {
    public void childMethod() {
        System.out.println("Child.childMethod");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// PolyMain.java
package poly.basic;

public class PolyMain {
    public static void main(String[] args) {
        System.out.println("Parent -> Parent");
        Parent parent = new Parent();
        parent.parentMethod();

        System.out.println("Child -> Child");
        Child child = new Child();
        child.parentMethod();   // 상속받은 메서드 호출 가능
        child.childMethod();    // 자식 고유 메서드 호출 가능

        System.out.println("Parent -> Child");
        Parent poly = new Child();
        poly.parentMethod();    // 부모 타입에서 선언된 메서드만 호출 가능

        // poly.childMethod(); // 컴파일 오류 - Parent 타입에는 존재하지 않는 메서드
    }
}

// 실행결
Parent -> Parent
Parent.parentMethod
Child -> Child
Parent.parentMethod
Child.childMethod
Parent -> Child
Parent.parentMethod

부모 타입 변수 → 부모 타입 인스턴스

1
Parent parent = new Parent();

image.png

  • Parent 클래스만 메모리에 생성된다.
  • parent.parentMethod() 호출 시 Parent 클래스의 메서드가 실행된다.

자식 타입 변수 → 자식 타입 인스턴스

1
Child child = new Child();

image

  • ChildParent 클래스 모두 메모리에 생성된다.
  • 자식 변수는 부모 메서드와 자식 메서드 모두 호출할 수 있다.

부모 타입 변수 → 자식 타입 인스턴스 (다형적 참조)

1
Parent poly = new Child(); //업캐스팅

image

  • ChildParent 클래스 모두 메모리에 생성된다.
  • 변수의 타입이 Parent이므로, Parent에 정의된 메서드만 호출할 수 있다.
  • 자식 고유 메서드인 childMethod()는 호출할 수 없다. (컴파일 오류)
1
poly.childMethod(); // 컴파일 오류 발생
  • polyParent 타입이므로 Parent에 정의되지 않은 메서드는 호출할 수 없다.
  • 부모 타입은 자식의 기능을 알지 못하므로 접근할 수 없다.
  • 이럴 경우 형변환(캐스팅)을 통해 접근해야 한다.

캐스팅

1
2
Child casted = (Child) poly; // 형변환 캐스팅
casted.childMethod(); // 가능

형변환을 사용하면 자식 타입에만 있는 기능도 사용할 수 있다.

이처럼 부모 타입의 참조 변수를 자식 타입으로 변환하는 것을 다운캐스팅(downcasting) 이라고 한다.

image

Child child = (Child) poly; 이 코드는 poly가 가리키는 인스턴스(실제 객체)가 Child 타입이라는 것을 가정하고, 그 참조값을 Child 타입 변수에 대입하는 것이다.

참고: poly의 참조값만 복사되는 것이고,

poly 자체의 타입은 여전히 Parent이다. 변하는 것은 아니다.

일시적 다운캐스팅

다운캐스팅 결과를 변수에 담지 않고, 필요한 순간에만 일시적으로 타입을 변환해서 메서드를 호출할 수도 있다.

1
((Child) poly).childMethod();
  • polyParent 타입이지만, 괄호를 두 번 감싸 (Child)일시적으로 타입을 변환한 뒤, 곧바로 childMethod()를 호출한다.
  • 이 방식은 변수를 따로 만들 필요가 없어서 간편하다.
  • 물론 실제 인스턴스가 자식 타입이어야만 가능하다.

image

이 캐스팅은 단지 poly가 가리키는 참조값을 Child로 해석해서 호출하는 것 뿐이다. 실제로 poly의 타입 자체가 바뀌는 것은 아니다. 즉, poly는 여전히 Parent 타입 이다.

업캐스팅

다운캐스팅과 반대로, 자식 타입을 부모 타입으로 변환하는 것을 업캐스팅 이라고 한다.

1
2
3
4
5
6
7
Child child = new Child();
Parent parent1 = (Parent) child; // 명시적 업캐스팅 (사실 생략 가능)
Parent parent2 = child;          // 암묵적 업캐스팅 (추천 방식)

// 실행 결과
Parent.parentMethod
Parent.parentMethod
  • Child 인스턴스를 Parent 타입으로 참조하는 것은 업캐스팅이다.
  • 이 경우 형변환 코드를 생략해도 자동으로 변환된다.
  • 자바에서는 업캐스팅을 매우 자주 사용하므로 생략하는 것이 일반적이다.
1
Parent parent = new Child(); // 업캐스팅 생략한 형태

업캐스팅은 자바 컴파일러가 자동으로 처리해준다. 반면, 다운캐스팅은 명시적인 형변환이 없으면 컴파일 오류가 발생 한다.

다운캐스팅과 주의점

다운캐스팅은 실제 인스턴스가 자식 타입일 때만 안전 하게 사용할 수 있다. 잘못된 다운캐스팅은 ClassCastException이라는 런타임 오류를 발생시킨다.

1
2
3
4
5
6
7
Parent parent1 = new Child();
Child child1 = (Child) parent1;
child1.childMethod(); // OK

Parent parent2 = new Parent();
Child child2 = (Child) parent2; // 런타임 오류 발생
child2.childMethod(); // 실행되지 않음

왜 오류가 생길까

1
2
Parent parent2 = new Parent(); // 실제 인스턴스는 Parent
Child child2 = (Child) parent2; // 존재하지 않는 기능에 접근하려고 함
  • parent2Parent 타입 인스턴스이기 때문에, 메모리에 Child 관련 정보가 없음
  • 그런데 Child로 변환하려 하니까 자바가 런타임 예외를 발생시킴
  • 이 예외가 바로 ClassCastException
1
2
Child.childMethod
Exception in thread "main" java.lang.ClassCastException: class poly.basic.Parent cannot be cast to class poly.basic.Child
다운캐스팅은 부모가 자식을 알고 있을 때만 가능하다 즉, 실제 인스턴스가 자식이어야 한다는 뜻이다.

안전하게 캐스팅하려면?

자바에서는 다운캐스팅 전에 instanceof 키워드를 사용해서 실제 인스턴스 타입을 검사할 수 있다.

1
2
3
4
5
6
if (parent2 instanceof Child) {
    Child safeChild = (Child) parent2;
    safeChild.childMethod();
} else {
    System.out.println("Child 타입이 아닙니다.");
}

→ 이렇게 검사하면 런타임 오류 없이 안전하게 다운캐스팅할 수 있다.

업캐스팅은 안전하고, 다운캐스팅은 왜 위험할까?

업캐스팅은 항상 안전하다

1
A a = new C(); // 업캐스팅
  • 클래스 A → B → C 순으로 상속 관계가 있다고 가정하자.
  • new C()를 하면 메모리에는 C, B, A 모두 함께 생성된다.
  • 따라서 상위 타입인 A, B, C 어느 타입으로 참조해도 문제 없다.
  • 이처럼 위로 올라가는 업캐스팅은 인스턴스 내부에 모든 타입 정보가 포함되어 있어 안전하다.

그래서 업캐스팅은 형변환 생략도 가능하고, 자바에서 매우 자주 쓰인다

다운캐스팅은 조심해야 한다

1
C c = (C) new B(); // 런타임 오류 (ClassCastException)
  • new B()로 생성하면 인스턴스에는 B, A만 존재한다.
  • 그런데 C는 존재하지 않기 때문에 C로 캐스팅하면 없는 기능에 접근하려는 시도다.
  • 자바는 이 상황에서 ClassCastException이라는 런타임 오류를 발생시킨다.

하위 타입은 객체 생성 시 포함되지 않기 때문에, 존재하지 않는 부분을 참조하게 될 위험이 있다.

컴파일 오류 vs 런타임 오류

구분 설명 예시 확인 시점
컴파일 오류 문법적 오류, 즉시 확인 가능 변수 오타, 클래스 없음 코드 작성 시
런타임 오류 실행 중 발생하는 오류 잘못된 다운캐스팅 프로그램 실행 중
  • 컴파일 오류는 IDE가 바로 알려주기 때문에 안전하다.
  • 런타임 오류는 실제 사용자 앞에서 발생할 수 있기 때문에 위험하다.
  • 그래서 다운캐스팅은 항상 조심해서 사용해야 한다.

instanceof

다형성에서는 부모 타입 변수가 다양한 자식 인스턴스를 참조할 수 있기 때문에,

현재 참조하고 있는 실제 인스턴스의 타입을 확인할 필요가 있다.

이때 사용하는 것이 바로 instanceof 연산자이다.

1
2
Parent parent1 = new Parent();
Parent parent2 = new Child();
  • parent1Parent 인스턴스를 참조한다.
  • parent2Child 인스턴스를 참조한다.

이처럼 부모 타입 변수는 다양한 자식 인스턴스를 참조할 수 있으므로,

다운캐스팅 전에 instanceof로 검사하는 것이 안전한 코드 작성에 도움이 된다.

instanceof 결과 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void call(Parent parent) {
    parent.parentMethod();

    if (parent instanceof Child) {
        System.out.println("Child 인스턴스 맞음");
        Child child = (Child) parent; // 안전한 다운캐스팅
        child.childMethod();
    }
}

new Parent() instanceof Parent  // true
new Child() instanceof Parent   // true (부모 타입도 포함됨)
new Parent() instanceof Child   // false
new Child() instanceof Child    // true
  • 왼쪽 인스턴스를 오른쪽 타입에 대입할 수 있는지 판단하는 구조
  • 대입 가능하면 true, 불가능하면 false를 반환한다

Java 16부터: 패턴 매칭 기반 instanceof

Java 16부터는 instanceof를 사용할 때 타입 검사 + 캐스팅 + 변수 선언을 한 번에 처리할 수 있다.

1
2
3
if (parent instanceof Child child) {
    child.childMethod();
}
  • 위 코드는 parentChild 타입인 경우만 실행된다.
  • 동시에 Child child = (Child) parent를 자동으로 수행한 효과를 가진다.
  • 불필요한 캐스팅 없이 더 간결한 코드 작성이 가능하다.

정리

  • instanceof다운캐스팅의 안전성을 확보하기 위한 필수 도구이다.
  • 실제 인스턴스 타입을 확신할 수 없을 때는 instanceof로 검사한 뒤 캐스팅해야 한다.
  • Java 16 이상에서는 패턴 매칭을 활용해 더 깔끔하게 작성할 수 있다.

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

다형성을 이루는 또 하나의 핵심 이론은 바로 메서드 오버라이딩(Overriding) 이다. 오버라이딩은 부모 클래스에 정의된 메서드를 자식 클래스에서 재정의하는 것이다.이때 중요한 점은, 오버라이딩 된 메서드는 항상 우선권을 가진다.

다형성과 함께 사용할 때, 메서드 오버라이딩의 진짜 힘이 발휘된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Parent.java
public class Parent {
    public String value = "parent";
    public void method() {
        System.out.println("Parent.method");
    }
}

// Child.java
public class Child extends Parent {
    public String value = "child";

    @Override
    public void method() {
        System.out.println("Child.method");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class OverridingMain {
    public static void main(String[] args) {
        Child child = new Child();
        System.out.println("Child -> Child");
        System.out.println("value = " + child.value);
        child.method();

        Parent parent = new Parent();
        System.out.println("Parent -> Parent");
        System.out.println("value = " + parent.value);
        parent.method();

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

//실행결과
Child -> Child
value = child
Child.method
Parent -> Parent
value = parent
Parent.method
Parent -> Child
value = parent
Child.method
  • 변수는 타입에 따라 결정된다 → 정적 바인딩
  • 메서드는 실제 인스턴스에 따라 결정된다 → 동적 바인딩

오버라이딩의 우선순위

오버라이딩된 메서드는 항상 하위 클래스 쪽 메서드가 우선순위를 가진다.

예를 들어,

  • 부모 → 자식 → 손자 클래스가 있고,
  • 셋 다 method()를 오버라이딩했다면,
  • 손자의 메서드가 실행된다. 즉, 더 하위 클래스일수록 우선순위가 높다.

정리

  • 다형적 참조: 부모 타입 변수로 자식 인스턴스를 참조할 수 있다.
  • 메서드 오버라이딩: 자식이 부모의 메서드를 재정의하여 새로운 동작을 제공한다.
  • 다형성과 오버라이딩을 함께 사용하면,

    부모 타입 변수를 통해 자식의 오버라이딩된 메서드를 실행할 수 있다.

이 조합이 바로 다형성의 진짜 위력이며, 유연하고 확장 가능한 객체지향 설계핵심이다.


출처: 김영한의 실전 자바 - 기본편

This post is licensed under CC BY 4.0 by the author.

Trending Tags