[Java] 오버라이딩 vs 오버로딩

이권민·2025년 11월 30일

목차

  1. 오버라이딩 vs 오버로딩
  2. 메서드 결정시점

오버라이딩 vs 오버로딩

오버라이딩

💡 오버라이딩: 상위 객체가 가진 메서드를 하위 객체가 재정의 하는 것(다형성, 확장성)

오버라이딩의 조건

  • 오버라이딩하고자 하는 함수가 상위 클래스에 있어야한다.
  • 메서드 이름, 매개변수의 수와 자료형이 같아야한다.
  • 메서드 리턴형이 같아야한다.
  • 일반적인 부모 클래스의 기능을 자식 클래스에서 수정하는 것과 추상 메서드를 상속 받아 실질적인 기능을 정의하는 방법 존재.
class Animal {
    void sound() {
        System.out.println("동물이 소리를 냅니다");
    }
}

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

Animal a = new Dog();
a.sound();  // "멍멍!"


// 추상메서드 상속

abstract class Animal {
    abstract void sound();  // 구현 없음
}

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

Animal a = new Cat();
a.sound(); // "야옹!"

@Override 어노테이션

어노테이션 없어도 오버라이딩 가능, 그럼 @Override는 왜 씀?

  • 시스템에서 오버라이딩한 메서드라고 알리는 역할로 오버라이딩이 잘못된 경우 컴파일 오류 발생.

    • 오타, 부모 클래스 이름 변경, 매개변수 수정, 제거 등
  • @Override를 표시함으로써 해당 메서드가 오버라이딩하였다는 것을 쉽게 파악 가능

오버라이딩 접근 제어자

💡 객체지향 설계원칙, SOLID – 리스코프 치환 원칙(LSP)
하위 타입은 상위 타입을 언제나 대체할 수 있어야 한다.
→ 즉, 부모 타입으로 호출하던 기능이 자식 타입으로도 문제없이 동작해야 한다.

규칙

  • 오버라이딩되는 자식 메서드는 부모 메서드보다 접근 제어자를 더 좁게 만들 수 없다.(부모 ≤ 자식)
  • public > protected > default > private의 확장순서에 맞춰서 접근제어자 작성
  • 부모 타입(Parent)으로 호출하던 기능은 자식 타입(Child)으로도 동등하게 접근 가능해야함
class Parent {

    // 부모 메서드는 protected
    protected void doSomething() {
        System.out.println("Parent: 작업 수행");
    }
}

class Child1 extends Parent {

    //기본(default) 접근자로 오버라이딩 → protected보다 좁아짐 → 컴파일 에러
    @Override
    void doSomething() {  // default
        System.out.println("Child1: 기본 접근자는 더 좁아서 override 불가");
    }
}

class Child2 extends Parent {

    // private으로 오버라이딩 → 접근 범위가 부모보다 좁아져서 컴파일 에러
    @Override
    private void doSomething() {
        System.out.println("Child2: private은 더 좁아서 override 불가");
    }
}

class Child3 extends Parent {

    // OK — 부모(protected)와 동일한 접근 제어자
    @Override
    protected void doSomething() {
        System.out.println("Child3: 동일 접근 제어자 사용");
    }
}

class Child4 extends Parent {

    // OK — 부모보다 더 넓은 접근 범위(public)
    @Override
    public void doSomething() {
        System.out.println("Child4: public으로 접근 범위 확장");
    }
}

오버로딩

💡 오버로딩: 같은 이름의 함수에 매개변수를 다르게 주는 것. 사용성, 편의성 확보.

오버로딩의 조건

  • 함수의 이름이 같아야 한다.
  • 매개변수의 수가 달라야 한다.
  • 매개변수의 수가 같으면 자료형이 달라야한다.
  • 리턴값만을 다르게 갖는 오버로딩은 작성할 수 없다.
void add(int num_1, int num_2)
{
	printf("%d", num_1 + num_2);
    return 0;
}
void add(float num_1, float num_2)
{
	printf("%f", num_1 + num_2);
    return 0;
}
  • 메서드의 이름을 절약
    • println() 메서드는 오버로딩 되어있기 때문에 int형 인자, string형 인자, boolean형 인자, char형 인자 모두 받아서 동작할 수 있다.
public class MyPrintStream {

    public void println(int x) {
        print(String.valueOf(x));
    }

    public void println(boolean x) {
        print(String.valueOf(x));
    }

    public void println(char x) {
        print(String.valueOf(x));
    }

    public void println(String x) {
        print(x);
    }

    public void println(double x) {
        print(String.valueOf(x));
    }

    public void println(Object obj) {
        print(String.valueOf(obj));
    }

    // 실제 출력은 여기서 처리 (공통 로직)
    private void print(String s) {
        System.out.println("[MyPrintStream] " + s);
    }
}

public class Main {
    public static void main(String[] args) {
        MyPrintStream out = new MyPrintStream();

        out.println(10);           // int
        out.println("Hello");      // String
        out.println(true);         // boolean
        out.println('A');          // char
        out.println(3.14);         // double
        out.println(new int[]{1,2,3}); // Object
    }
}

메서드 결정시점

컴파일 타임 (Compile-time): 소스 코드가 컴파일될 때 발생하는 시간. 주로 구문 오류 및 타입 오류를 검사.

  • 주요 활동: 구문 분석, 타입 체크, 코드 최적화, 바이트코드 생성

  • 오류 유형: 문법 오류, 타입 오류

런타임 (Run-time): 프로그램이 실제로 실행되는 시간. 실행 중에 발생하는 오류를 처리.

  • 주요 활동: 프로그램 실행, 동적 할당, 입출력 작업, 예외 처리

  • 오류 유형: NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException

필드 vs 메서드 (실행시점)

메서드는 객체의 실제 타입(런타임 타입)을 따르고, 필드는 선언된 타입(컴파일 시점)을 따라감.

class Parent {
    String str = "Parent";

    String getStr() {
        return str;
    }
}

class Child extends Parent {
    String str = "Child";

    String getStr() {
        return str;
    }
}

public class Main {
    public static void main(String[] args) {
    // 선언된 타입, 객체의 실제 타입
        Parent parent = new Child();
        Child child = new Child();

        System.out.println(parent.getStr());
        System.out.println(child.getStr());
        System.out.println(parent.str);
        System.out.println(child.str);
    }
}

/*
출력 결과

Child
Child
Parent
Child
*/

메서드 호출과 필드 접근이 서로 다른 방식으로 동작하는 이유

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

    • 런타임 시점에 실제 객체 타입에 따라 호출, 다형성을 구현.
    • 프로그램의 유연성과 확장성을 증가, 메서드 호출 시 실제 객체가 가진 구체적인 동작을 보장.
  • 필드 숨김(Shadowing)

    • 필드는 다형성을 따르지 않고, 필드 이름이 같을 경우 자식 클래스에서 부모 클래스의 필드를 숨김
    • Java는 필드를 오버라이딩하지 않고 별개의 필드로 취급하여, 컴파일 시점에서 결정된 타입의 필드를 사용.

즉, 메서드는 런타임의 실제 타입을 반영하여 프로그램의 유연성을 제공, 필드는 컴파일 시점에서 정적으로 결정되어 성능과 명확성을 유지.

오버로딩 vs 오버라이딩(바인딩 시점)

  • 오버로딩된 메소드는 컴파일(Compile) 시에 어떤 메소드를 실행할지 결정.
    • 컴파일 시점: 전달된 인자의 정적 타입, 개수, 순서를 기준으로 호출할 메서드를 컴파일러가 확정. 선택된 메서드는 실행 중 변경x. (정적 바인딩)
    • 런타임 시점: 이미 컴파일 때 결정된 메서드를 그대로 실행. (따라서 런타임에는 선택 과정 x)
  • 오버라이딩된 메소드는 런타임(Runtime) 시에 어떤 메소드를 실행할지 결정.
    • 컴파일 시점: 해당 메서드가 정적 타입에 존재하는지 문법적, 타입적 체크만 수행. 어떤 구현(부모/자식)을 호출할지는 결정x.
    • 런타임 시점: 실제 객체의 타입을 기준으로 호출할 메서드 구현을 결정.(동적 바인딩(Dynamic Binding) or 동적 디스패치(Dynamic Dispatch)).
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

public class Overload {
	// 오버로딩.
	public static void get(List<?> list) {
		System.out.println("List");
	}
	
	public static void get(ArrayList<?> list) {
		System.out.println("ArrayList");
	}
	
	public static void get(LinkedList<?> list) {
		System.out.println("LinkedList");
	}
	
	public static void main(String[] args) {
    // 배열 원소의 정적 타입 List<?>
		List<?>[] listArray = { new ArrayList<>(), new LinkedList<>(), new Vector<>() };
		Arrays.stream(listArray).forEach(list -> get(list));
	}
}

/*

모두 List 타입으로 보이기 때문에 결과는 모두 "List" 출력

*/
import java.util.ArrayList;
import java.util.Arrays;

// 오버라이딩
public class MyList<E> extends ArrayList<E> {
	private static final long serialVersionUID = 1L;

	/* (non-Javadoc)
	 * @see java.util.ArrayList#get(int)
	 */
	@Override
	public E get(int index) {
		System.out.print("MyList get method returns = ");
		return super.get(index);
	}
	
	@SuppressWarnings("unchecked")
	public static void main(String[] args) {
		ArrayList<String>[] listArray = new ArrayList[2];
        // 컴파일러는 배열 원소를 ArrayList<String>으로 봄
        // 오버라이딩된 메서드는 실제 타입 기준
		listArray[0] = new ArrayList<String>();
		listArray[1] = new MyList<String>();
		Arrays.stream(listArray).forEach(list -> list.add("first object"));
		Arrays.stream(listArray).forEach(list -> System.out.println(list.get(0)));
	}
}

/*

ArrayList → "first object"
MyList   → "MyList get method returns = first object"

*/

오버로딩·오버라이딩의 결정 시점 차이

  • 컴파일 시점에는 listArray의 각 요소가 ArrayList인지 LinkedList인지 알 수 없고, 오직 정적 타입(List) 만 알고 있다. 즉, 오버로딩에서는 컴파일러가 List 타입 파라미터를 받는 메서드를 선택.

  • 런타임 시점에는 new ArrayList(), new MyList() 로 생성된 실제 객체 타입을 알 수 있음. 이때 오버라이딩된 메서드는 동적 바인딩을 통해 실제 객체의 메서드가 실행된다

참고

  1. 오버로딩(Overloading) 오버라이딩(Overriding)

  2. 오버로딩(Overloading),오버라이딩(Overriding)이란? + 차이점

  3. [Java] 메서드와 필드 접근: 런타임과 컴파일 타임의 차이

  4. 오버로딩 & 오버라이딩의 결정 시점 차이 (Overloading & Overriding)

  5. 오버로딩, 오버라이딩 결정 시점차이

  6. [Java] 부모 클래스의 메소드 오버라이딩이 더 큰 범위의 접근 제어자만 가능한 이유 or 더 좁은 범위로 변경할 수 없는 이유

  7. 메서드 오버라이딩에서 접근 제어자의 범위

profile
이것저것이것 개발자

0개의 댓글