JAVA

[자바] 오버로딩, 오버라이딩

감자b 2024. 12. 24. 18:42

오버로딩 : 이름은 같지만 매개변수가 다른 메서드를 여러 개 정의하는 것. 단 반환 타입이 다를 때에는 인정 x

메서드 시그니처 = 메서드 이름 + 매개변수 타입(순서)

즉 오버로딩은 메서드 시그니처를 가지고 판단한다.

// 단순히 입력받은 숫자를 출력하는 메서드
public class TestMain {
    public static void main(String[] args) {
        Test test = new Test();
        test.call(1, 3);
        test.call(1, 3, 4);
        test.call(1);
    }

    static class Test {
        public void call(int x) {
            System.out.println("첫 번째");
            System.out.println(x);
        }

        public void call(int x, int y) {
            System.out.println("두 번째");
            System.out.println(x + ", " + y);
        }

        public void call(int x, int y, int z) {
            System.out.println("세 번째");
            System.out.println(x + ", " + y + ", " + z);
        }

//        public int call(int x) { // 에러
//            return x;
//        }
    }
}

결과 :

두 번째 1, 3

세 번째 1, 3, 4

첫 번째 1


오버라이딩 : 부모 클래스에게서 상속받은 기능을 자식 클래스에서 재정의하는 것

조건

1. 자식 클래스에서 재정의하려는 메서드 이름이 부모 클래스 메서드명과 같아야 하며, 파라미터 타입과 순서 역시 같아야 함. 즉 부모와 자식의 메서드 시그니처가 동일해야 함

2. 반환타입이 같거나 부모의 하위 클래스 타입이어야 함

static class Parent {
    public String name = "parent";

    public Object call() {
        System.out.println("Parent.call()");
        return name;  
    }
}

static class Child extends Parent {
    public String name = "child";

    @Override
    public String call() { //Object의 하위 타입인 String으로 반환해도 오버라이딩 가능
        System.out.println("Child.call()");
        return name;
    }
}

 

3. 접근제어자가 상위 클래스의 메서드보다 제한적이면 X

static class Parent {
    public String name = "parent";

    public Object call() {
        System.out.println("Parent.call()");
        return name;
    }
}

static class Child extends Parent {
    public String name = "child";

//    @Override
//    protected String call() { 에러 발생
//        System.out.println("Child.call()");
//        return name;
//    }
}

 

4. 상위 클래스의 메서드보다 더 많은 체크 예외를 throws 선언할 수 없지만 더 적거나 같은 수의 예외, 하위 타입의 예외는 선언할 수 있음

static class Parent {
    public String name = "parent";

    public Object call() throws NullPointerException {
        System.out.println("Parent.call()");
        return name;
    }
}

static class Child extends Parent {
    public String name = "child";

//    @Override
//    public String call() throws Exception { 상위 타입 예외 선언 시 에러
//        System.out.println("Child.call()");
//        return name;
//    }
//}

 

5. static(오버라이딩은 인스턴스 레벨에서 작동하므로), final, private가 붙으면 오버라이딩 X

6. 생성자 역시 오버라이딩 X


다형성과 오버라이딩 관계

다형성이란? 한 객체가 여러 객체 타입으로 취급될 수 있는 기능을 뜻한다.

public class Parent {
	public void parentCall() {
		System.out.println("Parent.call");
	}
}
public class Child extends Parent {
    public void childCall() {
        System.out.println("Child.call");
    }
}
public class Test {
	public static void main(String[] args) {
		Parent parent1 = new Parent(); //(1)
		Parent parent2 = new Child(); //(2)
		Child child1 = new Child(); //(3)
//        Child child2 = new Parent(); (4) 불가능

		parent1.parentCall(); // Parent.call 호출
		parent2.parentCall(); // Child.call 호출
//        parent2.childCall(); 불가능
		child1.parentCall();
		child1.childCall(); // Child.call 호출
	}
}

부모는 자식 클래스를 담는 것이 허용된다.

 

그렇다면 위와 같이 다형적 참조가 일어날 때 메모리 구조는 어떻게 될까?

  • 1번의 경우 (3번의 경우도 동일)
    • Parent 타입의 변수가 Parent 인스턴스 참조.

 

  1. main 메소드 호출 → 메인 스택 프레임 생성 → parent1 변수 생성
  2. 힙 영역에 Parent 객체 생성. parent1이 힙 영역의 Parent 객체를 참조
  3. parent1.parentCall() 호출 → 메서드 영역에서 Parent 클래스의 call() 메서드 실행

 

  • 2번의 경우
    • Parent 타입의 변수가 자식인 Child 인스턴스 참조 (다형적 참조)

  1. main 메소드 호출 → 메인 스택 프레임 생성 → parent2 변수 생성. 타입은 Parent
  2. 힙 영역에 Child 객체 생성. parent2가 힙 영역의 Child 객체를 참조
  3. parent2.parentCall() 호출 → 메서드 영역에서 Parent 클래스의 parentCall() 메서드 실행

메모리에 child와 parent가 모두 생성

힙 영역에 있는 메서드는 메서드 영역에 정의된 메서드에 대한 참조일 뿐 실제 메서드는 메서드 영역에 존재

(parent2 변수는 Parent 타입이므로 Parent 클래스에 정의된 parentCall() 메서드를 찾고 실행)

 

다형적 참조의 한계

위 코드에서 Parent 타입의 parent2 변수가 childCall() 메서드를 호출하는 것은 불가능했다. 상속 관계에서 부모 쪽으로 메서드를 호출하는 것은 가능하지만 자식 방향으로는 내려갈 수는 없다.

즉 Parent 는 부모 타입이고 상위에 부모가 없다. 따라서 childCall() 를 찾을 수 없으므로 컴파일 오류가 발생한다. 하위 타입의 메서드를 호출하고 싶다면 캐스팅해야함!


다형적 참조에서 오버라이딩이 일어날 때 동작

아래와 같은 상황이 있다고 하자.

public class Parent {
    public void same() {
        System.out.println("Parent.same");
    }
}

public class Child extends Parent {
    @Override
    public void same() {
        System.out.println("Child.same");
    }
}

public class Test {
	public static void main(String[] args) {
		Parent parent1 = new Parent();
		Parent parent2 = new Child();
		Child child1 = new Child();

		parent1.same();
		parent2.same();
		child1.same();
	}
}

출력 결과

Parent.same

Child.same

Child.same

 

오버라이딩을 했을 때는 parent2가 Parent 타입의 변수임에도 불구하고 child의 메소드가 호출되었다.

위에서 부모에서 자식 방향으로 메서드를 호출하는 것은 안된다고 했는데 오버라이딩 했을 때는 되는 이유가 무엇일까?

이는 바로 메서드 호출 시점의 차이가 있다. 컴파일 시점에는 parent2가 Parent 타입이므로 Parent 클래스에 정의된 메서드만 확인한다. 오버라이딩 된 메서드는 런타임 시점(메서드가 호출되는 시점)에 객체의 실제 타입을 확인한다. parent2 객체는 Child의 인스턴스이고 이를 참조하고 있으므로 JVM은 Child 클래스에 오버라이딩된 same() 메서드를 찾아 실행하게 된다.

즉 상속 관계에서는 오버라이딩 된 메서드가 우선으로 실행됨 (자바 동적바인딩)


instanceof

  • 변수가 참조하는 인스턴스 타입을 확인할 때 사용
public void call() {
        Child parent = new Child();
        if (parent instanceof Child) {
            Child child = parent;
            child.call();
        }
}

instanceof → 우측 타입에 좌측 타입이 들어갈 수 있는가?

new Parent() instanceof Parent //true

new Child() instanceof Parent //true

new Parent() instanceof Child //false

new Child() instanceof Child //true

자바 16에서의 instanceof → instanceof와 동시에 변수 선언이 가능하여 다운 캐스팅 코드 생략 가능

public void call() {
	Child parent = new Child();
	if (parent instanceof Child child) {
		child.call();
	}
}