Java/Java

[Java] 오버로딩 / 오버라이딩 정리

웅지니어링 2023. 2. 8. 18:15

* 오버로딩(Overloading)의 정의

C언어에서는 함수명이 고유하게 존재해야 한다. 즉 하나의 함수가 하나의 기능만을 구현해야 한다는 것이다. 그러나 자바에서는 하나의 메소드 이름으로 여러 기능을 구현할 수 있다. 자바에서는 한 클래스 내에 이미 사용하려는 이름과 같은 이름을 가진 메서드가 있더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 메서드를 정의할 수 있다. 이처럼 한 클래스 내에 같은 이름의 메서드를 여러 개 정의하는 것을 '메서드 오버로딩' 또는 '오버로딩'이라고 한다. 주의해야 할 것은 같은 이름의 메서드를 정의한다고 해서 무조건 오버로딩인 것은 아니다. 오버로딩에는 조건이 존재한다.

 

* 오버로딩의 조건

1. 메서드 명이 같아야 한다.

2. 매개변수의 개수 또는 타입이 달라야 한다.

3. 반환 타입은 관계없다.

 

* 오버로딩 메서드 예시 - println

오버로딩의 예로 가장 대표적인 것은 println 메서드이다. 실제로 println 메서드가 실행될 때, 매개변수로 지정하는 값의 타입에 따라서 호출되는 println 메서드가 달라진다. println 메서드가 어떻게 구성되어 있는지 확인해보면 다음 이미지와 같다.

이렇듯 println 메서드를 호출할 때 매개변수로 넘겨주는 값의 타입에 따라서 위의 오버로딩된 메서드들 중 하나가 선택되어 실행되는 것이다.

 

* 오버로딩의 시점 - 예시 코드

그렇다면 오버 로딩은 어느 시점에서 이루어지는가? 해당 코드를 통해 확인해보자.

import java.util.*;

public class OverloadingTest {
    public static String print(Set<?> set) {
        return "Set";
    }

    public static String print(List<?> list) {
        return "List";
    }

    public static String print(Collection<?> collection) {
        return "Etc";
    }

    public static void main(String[] args) {
        Collection<?>[] collections = {new HashSet<String>(), new ArrayList<Integer>()};

        for (Collection<?> collection : collections) {
            System.out.println(print(collection));
        }
    }
}

print 메서드를 각 매개변수의 타입을 다르게 하여 오버로딩하였다. 만약 print 메서드가 컴파일 시점에서 오버로딩 된다면 "Etc" "Etc"가 출력될 것이고, 런타임 시점에서 오버로딩 된다면 "Set" "List"가 출력될 것이다. 왜냐하면 컴파일 시점에는 타입이 Collection이고, 런타임 시점에는 각각 Set, List로 타입이 변환된다. 출력 결과는 다음과 같다.

 

출력 결과를 통해 오버로딩은 컴파일 시점에서 이루어지는 것을 확인할 수 있다.

 

* 오버로딩 - JVM과의 관련성

오버로딩은 컴파일 시점에서 이루어지므로, 컴파일 타임 다형성(Compiletime Polymorphism), 정적 바인딩(Static Binding)에 해당하며 이미 JVM에 로드될 적에는 컴파일러에 의해 메서드 시그니처 기준으로 전부 구분되므로 JVM에서 별도로 메서드 식별 작업을 수행하지 않는다. 컴파일 수행하여 ".class" 파일 생성 시 오버로딩된 메서드는 이름이 동일하지만 메서드 이름 + 매개 변수를 바탕으로 메서드 시그니처를 생성하여 저장한다. 따라서 클래스 메타 정보에서 메서드들이 구분된 상태로 저장되기 때문에 컴파일이 완료되면 별개의 메서드로 취급한다.

 

* 오버라이딩(Overriding)의 정의

오버라이딩은 부모 클래스를 상속받은 자식 클래스에서 부모 클래스의 메서드와 메서드 이름, 매개변수의 개수와 순서, 리턴 타입이 동일하지만 메서드의 동작을 다르게 선언하는 방식이다. 오버라이딩을 수행하면 다른 매개변수를 갖지만 동작이 비슷한 메서드의 불필요한 호출을 줄일 수 있고, 조건문을 쓰지 않고 메서드를 구분할 수 있다는 이점이 존재한다.

 

* 오버라이딩의 조건

1. 메서드 명이 동일해야 한다.

2. static 메서드(클래스 메서드)는 오버라이딩이 불가하다.

3. 매개변수의 개수, 타입, 순서가 같아야 한다.

4. 접근 제어자는 자식 클래스의 메서드가 부모 클래스와 같거나 보다 더 공개되는 방향으로 변경되어야 한다.

 

* 오버라이딩의 시점 - 예시 코드 

오버라이딩은 어느 시점에서 이루어질까? 다음 코드를 통해 확인해보자.

class ParentTest {
    public void printInt(int value) {
        System.out.println("ParentTest value = " + value);
    }

}

class ChildTest extends ParentTest {
    public void printInt(int value) {
        System.out.println("ChildTest value = " + value);
    }
}

public class OverridingTest {
    public static void main(String[] args) {
        ParentTest parentTest = new ChildTest();
        parentTest.printInt(1);

        ChildTest childTest = new ChildTest();
        childTest.printInt(1);
    }
}

ChildTest는 자식 클래스, ParentTest는 부모 클래스에 해당한다. main을 확인해보면, parentTest의 객체 인스턴스 변수에 ChildTest() 클래스 객체를 할당했다. 오버라이딩이 컴파일 시점에서 이루어졌다면 ParentTest value = 1이 출력될 것이고, 런타임 시점에서 이루어졌다면 ChildTest value = 1이 출력될 것이다. 출력 결과는 다음과 같다.

출력 결과를 통해 오버라이딩은 런타임 시점에서 이루어지는 것을 확인할 수 있다.

 

* 오버라이딩 - JVM과의 관련성

오버라이딩은 런타임 다형성(Runtime Polymorphism), 동적 바인딩(Dynamic Binding)에 해당하며 컴파일 시 동일한 시그니처를 가지고 있다. 그러므로 컴파일러는 메서드를 호출한 객체가 오버라이딩한 메서드를 가지고 있는지 알 수 없다. 따라서 메서드 호출 시 변수에 할당된 객체가 오버라이딩 메서드를 정의하지 않았다면 부모 클래스의 메서드를 호출하고, 정의했다면 할당된 객체 클래스(자식 클래스)의 오버라이딩 메서드를 호출하는 등 JVM에게 별도의 동작을 요구한다. 그렇다면 JVM은 어떤 기준으로 오버라이딩된 함수를 선택하는 것일까? JVM은 invokeinterface 또는 invokevirtual 이라는 바이트 코드와 특정 메서드를 연결할 때, 스택 최상단에 올라와 있는 객체의 타입에 따라 메서드를 결정한다. 즉 Java의 동적 바인딩은 런타임 시점에 클래스의 런타임 상수 풀에 있는 Symbolic Reference를 고정된 주소 값으로 바꾸는 것이며,  이 때 고정된 주소 값을 선택하는 기준은 스택 위에 올라와 있는 객체의 타입이다. 위의 예시 코드를 보면

ParentTest parentTest = new ChildTest();

 

ChildTest() 클래스 의 객체 타입이 주소 값을 선택하는 기준이 된다고 볼 수 있다. 따라서 Java의 동적 바인딩은 런타임 시점에 클래스의 런타임 상수 풀에 있는 Symbolic Reference를 고정된 주소 값으로 바꾸는 것이다.