JAVA의 오버라이딩(Override)

Polynomeer·2020년 3월 24일
0

JAVA

목록 보기
3/8
post-thumbnail

JAVA의 메소드 오버라이딩(Override)

Override는 '기각하다', '무시하다'의 뜻을 담고있다. 즉, '기존의 것을 무시하고 덮어쓰다.'의 의미를 가진다. 자바에서 메소드 오버라이딩이란, 상속의 관계에 있는 클래스 간에 하위 클래스가 상위 클래스와 '완전 동일한 메소드'를 덮어쓴다는 의미이다. 여기서 '완전 동일한 메소드'라는 말은 이름과 반환형이 같으면서 매개변수의 개수와 타입까지 모두 같은 메소드라는 의미이다. 즉, 오버로딩(overload)되지 않는 (JVM이 단순히 다른 메소드라고 구별을 할 수 없는) 메소드이다.

오버로드란, 상위 클래스에 정의된 메소드의 이름, 반환형, 매개변수 선언까지 완전히 동일한 메소드를 하위 클래스에서 다시 정의한다면, 하위 클래스의 해당 메소드가 상위 클래스의 메소드를 덮어버리는(가려버리는, 지워버리는) 것이다.

1. 오버라이딩된 메소드의 기본동작

class Speaker {
	private int volume;
    	public void showState() {
    		System.out.println("Volume size : " + volume);
    	}
    	public void setVolume(int vol) {
    		volume = vol;
    	}
}
class BaseSpeaker extends Speaker {
	private int baseRate;
    	public void showState() {
        	super.showState(); // 상위 클래스의 오버라이딩 된 메소드 호출
            	System.out.println("Base size : " + baseRate);
        }
        public void setBase(int base) {
        	baseRate = base;
        }
}
class MainClass {
	public static void main(String[] args) {
    		BaseSpeaker bs = new BaseSpeaker();
       		bs.setVolume(10); // BaseSpeaker가 Speaker를 상속했으므로 가능!
       		bs.setBase(20);
       		bs.showState();
    	}
}

여기서 BaseSpeaker 클래스는 Speaker 클래스를 상속하였으므로 Speaker의 메소드인 setVolume(10)을 호출할 수 있다. 위 코드의 출력 결과는 다음과 같다.

Volume size : 10
Base size : 20

실행순서는 먼저, BaseSpeaker 클래스의 showState()가 호출되고, super.showState()가 먼저 호출되어 상위 클래스에 있는 멤버변수인 volume의 값이 출력된다. 그리고 하위 클래스의 showState()가 이어서 실행되므로 baseRate가 출력된다. super.showState()부분이 없다면 당연히 출력결과는 BaseSpeaker의 showState()만 나오게 된다.

Base size : 20

  • 참조변수를 통해 인스턴스의 메소드를 호출하면 하위클래스의 메소드가 호출된다.
  • 하위 클래스에서 오버라이딩 된 상위 클래스의 메소드를 호출하려면 super를 사용한다.

상위 클래스의 참조변수로 하위 클래스의 인스턴스를 참조

위 예제에서는 하위 클래스인 BaseSpeaker 타입의 참조변수인 bs를 통해 BaseSpeaker의 인스턴스를 생성하였다. 하지만 Speaker 타입의 참조변수로 인스턴스를 할 수도 있다. 왜냐하면 BaseSpeaker 은 일종의(IS-A) Speaker이기 때문이다. == BaseSpeaker는 Speaker를 상속하기 때문이다.

class MainClass {
	public static void main(String[] args) {
    		Speaker bs = new BaseSpeaker(); // BaseSpeaker를 Speaker 참조변수로 인스턴스화
       		bs.setVolume(10); // BaseSpeaker가 Speaker를 상속했으므로 가능!
       		bs.setBase(20); // compile error occurred!
       		bs.showState();
    	}
}

MainClass가 이와같이 변경된다면 참조변수 bs가 참조하는 인스턴스는 BaseSpeaker가 된다. 그런데 여기서 컴파일러도 이처럼 인식해야할까? 항상 문법을 공부할때는 '왜 그렇게 만들었지?'라는 생각보다 '그렇지 않다면 어떤 일이 벌어질까?'를 생각해본다면 이해가 빠르다.

만약 컴파일러가 bs를 BaseSpeaker로 바라본다면, bs.setBase(20)을 호출하기 위해서 먼저 Speaker클래스 내부의 메소드를 탐색할것이다. 그리고 setBase()메소드를 찾을 수 없으니 Speaker를 상속하는 클래스 중에서 setBase()를 가진 하위 클래스를 모두 탐색할 것이다. 그중에서 또 '완전 동일한 메소드'가 있다면? 단순히 생각해보아도 이 뿐만아니라 문제가 많이 발생한다는 것을 알 수 있다. 따라서 자바에서는 이 경우에 컴파일러가 bs를 Speaker로 바라보도록 디자인 되어있다.

결론적으로 Speaker의 참조변수로 인스턴스를 참조하면, 실제로 참조하는 인스턴스의 종류에 상관없이 Speaker클래스에 정의된 메소드만 호출 가능하다. 다시 말하면, 참조변수 bs를 통해서 Speaker에 정의되어 있는 메소드만 호출 가능하도록 제한하는 방식으로 컴파일러가 디자인 되어있다.

상속관계에서 상위 클래스의 참조변수로 인스턴스를 참조하면, 실제로 참조하는 인스턴스에 관계없이 상위 클래스에 정의된 메소드만 호출 가능하다.

2. 상속의 참조관계

상속에 있어서의 참조관계로 다음과 같이 일반적인 상황이 있을 수 있다.

class First {...}
class Second extends First {...}
class Third extends Second {...}

위 코드는 문제없이 컴파일이 된다. 상위 클래스의 참조변수로도 하위 클래스의 인스턴스 참조가 가능하기 때문이다. 전체적으로 Third는 Second를 상속하고 연쇄적으로 Second는 First를 상속한다. 따라서 Third도 First를 당연히 상속한다.

Third ref1 = ... // compile success
Second ref2 = ref1; // Third is a Second
First ref3 = ref2; //  Third is a First

여기서 Third ref1이 무엇이 되었든간에 컴파일에 성공했다면, 그 다음행은 정상적으로 실행된다. 왜냐하면 Third는 Second이기도 하며, Third는 First이기도 하므로 참조가 가능하기 때문이다.

어떤 클래스의 참조변수는 그 클래스의 인스턴스, 또는 그것을 상속하는 하위 클래스의 인스턴스를 참조할 수 있다.

First ref1 = new Third(); // 컴파일러가 ref1을 First 타입으로 판단
Second ref2 = ref1; // First is not a Second, compile error!!
Third ref3 = ref1; // Fisrt is not a Third, compile error!!
First ref1 = new Third(); // 컴파일러가 ref1을 First 타입으로 판단
Second ref2 = new First(); // First is not a Second, compile error!!
Third ref3 = new First(); // Fisrt is not a Third, compile error!!

반면에 위 코드는 컴파일 에러가 발생한다. First타입의 참조변수 ref1에 Third의 인스턴스를 담지만 컴파일러는 ref1을 First타입으로 판단하므로, 나머지 행에서 하위 클래스인 Second와 Third의 참조변수에 상위 클래스인 First타입의 참조값을 담으려고 하자 컴파일 에러가 발생한다.

사실상 위의 두 코드는 같은 이유로 컴파일 에러가 발생하는 것이다. 더 큰 그릇에 작은 그릇을 담을 수는 없는 법이다.

First ref1 = new Third(); 
Second ref2 = (Third)ref1; // 참조변수 ref1의 형 변환
Third ref3 = (Third)ref1; // 참조변수 ref1의 형 변환

물론 이와같이 참조변수의 형 변환을 통해 정상적인 컴파일이 가능하다. 하지만 이것은 매우 드물게 발생하는 상황이며, 이때 올바른 코드의 구성인가를 다시 살펴볼 필요가 있다.

3. 오버라이딩(Override)과 오버로딩(Overload)

class GrandParents {
    public void rideMethod() { System.out.println("GrandParants' rideMethod"); }
    public void loadMethod() { System.out.println("GrandParants' loadMethod"); }
}
class Parents extends GrandParents {
    public void rideMethod() { System.out.println("Parants' rideMethod"); }
    public void loadMethod(int n) { System.out.println("Parants' loadMethod"); }
}
class Child extends Parents {
    public void rideMethod() { System.out.println("Child's rideMethod"); }
    public void loadMethod(double n) { System.out.println("Child's loadMethod"); }
}
class RideLoad {
	public static void main(String[] args) {
    		GrandParents ref1 = new Child();
        	Parents ref2 = new Child();
        	Child ref3 = new Child();

    		ref1.rideMethod();	// Child.rideMethod() 호출
        	ref2.rideMethod();	// Child.rideMethod() 호출
        	ref3.rideMethod();	// Child.rideMethod() 호출
            
        	ref1.loadMethod(); 	// GrandParents.loadMethod() 호출
    	  	ref2.loadMethod(1); 	// Parents.loadMethod(int n) 호출
	        ref3.loadMethod(1.2); 	// Child.loadMethod(double n) 호출     
 	}
}

GrandParents타입의 참조변수 ref1은 Child타입의 인스턴스를 참조한다. ref2와 ref3도 마찬가지로 Child인스턴스를 각각 참조한다. 즉, 참조변수의 타입은 모두 다르지만, 최하위 클래스인 Child의 인스턴스를 참조한다.

Child's rideMethod
Child's rideMethod
Child's rideMethod
GrandParants' loadMethod
Parants' loadMethod
Child's loadMethod

출력결과를 보면 참조변수의 자료형에 관계없이 최하위 클래스인 Child의 rideMethod만 호출되었다는 사실을 알 수 있다. 이에 비해 loadMethod는 각각 호출부분도 다르고 결과도 각각 다르다. 오버라이딩에 비해 오버로딩된 메소드는 사실상 이름만 같을 뿐 서로 다른 메소드라고 볼 수 있다.

  • 상위 클래스의 참조변수는 하위 클래스의 인스턴스를 참조할 수 있다.
  • 오버라이딩된 상위 클래스의 메소드는 오버라이딩을 한 하위 클래스의 메소드에 의해 가려진다. 즉, 외부에서는 참조변수를 통해서 오버라이딩된 상위 클래스의 메소드를 호출할 수 없다. (내부에서는 super키워드로 호출가능!)
profile
어려운 문제를 어렵지 않게.

1개의 댓글

comment-user-thumbnail
2021년 5월 6일

오버라이딩 검색해서 들어왔는데 어거스트 벨로그가 딱!ㅋㅋㅋㅋㅋㅋㅋㅋㅋ

다름이 아니라 조금 애매한 부분이 있어서 수정사항 건의합니다

첫 문단에

'기존의 것을 무시하고 덮어쓰다.'의 의미를 가진다. 자바에서 메소드 오버라이딩이란, 상속의 관계에 있는 클래스 간에 하위 클래스가 상위 클래스와 '완전 동일한 메소드'를 덮어쓴다는 의미이다. 여기서 '완전 동일한 메소드'라는 말은 이름과 반환형이 같으면서 매개변수의 개수와 타입까지 모두 같은 메소드라는 의미이다.

라고 말씀하셨는데,

geeksforgeeks를 참고하면, 아래처럼 나와서요.

Before JDK 5.0, it was not possible to override a method by changing the return type. When we override a parent class method, the name, argument types and return type of the overriding method in child class has to be exactly same as that of parent class method. Overriding method was said to be invariant with respect to return type.

Covariant return types

Java 5.0 onwards it is possible to have different return type for a overriding method in child class, but child’s return type should be sub-type of parent’s return type.

결론은 오버라이딩으로 상속관계의 리턴타입으로 변경할 수 있다! 입니다.
예시 -> Iterable<String> findAll(); -> @Override List<String> findAll();

답글 달기