Dot Programming/Java

[Java] 메서드 오버라이딩과 하이딩에 대해 (Overriding and Hiding Methods)

루지 2022. 1. 31. 19:45

    인스턴스 메서드의 Overriding

    동일한 시그니처(이름, 매개변수 타입과 개수) 및 같은 반환 타입을 가진 Super의 인스턴스 메서드를 갖는 Sub의 인스턴스 메서드는 Super의 메서드를 overriding(재정의)한다.

    • @Override 애노테이션은 재정의할 때 오류를 컴파일 타임에 잡아줄 수 있게 도움을 준다. 그래서 무조건 명시해주는 것이 좋다.
    class Super{
    	void print(){
    		System.out.println("super");
    	}
    }
    
    class Sub extends Super{
    	@Override void print(){
    		System.out.println("sub");
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		Super sp = new Super();
    		Super spSub = new Sub();
    		Sub sub  = new Sub();
    		
    		sp.print(); // super
    		spSub.print(); // sub // override
     		sub.print(); // sub 
    	}
    }

     

    공변 반환 유형 (covariant return type)

    클래스는 Sub의 오버라이딩 기능으로 가장 가까운 Super에서 상속한 다음 필요에 따라 동작을 수정할 수 있다. 오버라이딩한 메서드는 오버라이딩한 메서드 선언 이름, 매개변수 유형, 개수, 리턴 타입이 모두 동일하다. 여기서 반환 형식을 리턴 타입(Super)의 하위 타입(Sub)으로 반환할 수 있는데, 이를 공변 반환 유형이라고 한다.

    class Super{
    	Super of(int type){
    		return new Super();
    	}
    }
    
    class Sub extends Super{
    	// covariant return type
    	@Override Super of(int type){
    		if(type == 1) return new Super();
    		else return new Sub();
    	}
    }

     

    Static 메서드는 Overriding이 아닌 Hiding을 한다

    만약 sub클래스가 super클래스의 static 메서드와 동일한 시그니처를 가진 정적 메서드를 정의하는 경우 sub클래스 메서드는 super클래스의 메서드를 hiding(숨기기)한다.

     

    static 메서드의 hiding과 인스턴스 메서드의 overriding의 차이는 중요하다.

    • 오버라이딩된 인스턴스 메서드는 sub 클래스 버전이다.
    • 하이딩된 정적 메서드는 super에서 호출되는지 sub에서 호출되는지에 따라 버전이 다르다.

     

    다음의 예를 살펴보자. Animal 클래스는 하나의 인스턴스 메서드와 하나는 정적 메서드를 갖고 있다. 그리고 Animal을 상속한 Cat 클래스는 Animal 인스턴스 메서드와 static 메서드를 똑같이 포함하고 있다.

    • static 메서드에 @Override를 달면 컴파일 에러가 발생한다
    class Animal{
       public static void staticMethod(){
          System.out.println("Animal static method");
       }
       public void instanceMethod(){
          System.out.println("Animal instance method");
       }
    }
    
    class Cat extends Animal{
       // @Override 컴파일 에러 
       public static void staticMethod(){
          System.out.println("Cat static method");
       }
       @Override public void instanceMethod(){
          System.out.println("Cat instance method");
       }
    }

     

    인스턴스 메서드와 같은 경우 위에서 다뤘던 것처럼 오버라이딩이 적용된다. 

    public class Main {
       public static void main(String[] args) {
          Animal myAnimal = new Animal();
          Animal myAnimalCat = new Cat();
          Cat myCat = new Cat();
    
          myAnimal.instanceMethod(); // Animal static method
          myAnimalCat.instanceMethod(); // Cat static method // overriding
          myCat.instanceMethod(); // Cat static method
       }
    }

     

    그러나 static 메서드와 같은 경우는 다른 결과를 보인다.

    • Animal myAnimalCat = new Cat(); 같은 경우 Cat 클래스로 선언했음에도 Animal static 메서드를 호출한다.
    • 이를 바로 hidding이라고 한다.
    public class Main{
       public static void main(String[] args) {
          Animal myAnimal = new Animal();
          Animal myAnimalCat = new Cat();
          Cat myCat = new Cat();
    
          myAnimal.staticMethod(); // Animal static method
          myAnimalCat.staticMethod(); // Animal static method // hiding
          myCat.staticMethod(); // Cat static method
       }
    }

     

    인터페이스 메서드 (default, abstract)

    인터페이스의 default와 abstract 메서드는 인스턴스 메서드처럼 상속이 된다. 만약 상속한 super클래스와 구현한 인터페이스에 완전 똑같은 시그니처가 있다면 무엇을 호출하게 될까?

    • 자바 컴파일러는 이러한 충돌을 피하기위해 inheritance rules를 따른다.
    • 한마디로, 결합력이 더 높은 메서드를 추종한다고 보면 된다. (당연히 추상클래스 혹은 클래스보다 인터페이스의 결합력이 더 낮다. 참고)

     

    1. 인스턴스 메서드가 인터페이스 default 메서드보다 더 선호된다.

    class Horse{
       public String identityMySelf(){
          return "I am a horse";
       }
    }
    
    interface Flyer {
       default public String identityMySelf(){
          return "I am able to fly";
       }
    }
    
    interface Mythical {
       default public String identityMySelf(){
          return "I am a mythical creature";
       }
    }
    
    public class Pegasus extends Horse implements Flyer, Mythical {
       public static void main(String[] args) {
          Pegasus myApp = new Pegasus();
          System.out.println(myApp.identityMySelf()); // "I am a horse"
       }
    }

     

    Pegasus가 한 클래스를 상속하고 두 인터페이스를 구현했고 세 곳에서 모두 똑같은 시그니처 메서드(identityMySelf)를 갖고 있다고 했을 때 가장 결합력이 높은 Horse 클래스의 인스턴스 메서드를 호출한다.

     

    Pegasus 관계도

    만약 인터페이스에서 default메서드가 아닌 abstract 메서드로 정의되어있다면 Pegasus에서 직접 구현하지 않는 이상 Horse 클래스의 메서드를 따른다. 당연히 Pegasus에서 abstract 메서드를 구현한다면 가장 결합력이 높은 자신 클래스에 정의된 메서드를 따르게 된다. 

     

    2. 이미 다른 후보에 의해 오버라이딩된 메서드는 무시된다. 이 상황은 슈퍼타입들이 공통 조상을 갖고 있을 때 발생한다.

    interface Animal{
       default public String identityMyself(){
          return "I am a animal";
       }
    }
    
    interface EggLayer extends Animal{
       default public String identityMyself(){
          return "I am able to lay eggs";
       }
    }
    
    interface FireBreather extends Animal {}
    
    public class Dragon implements EggLayer, FireBreather{
       public static void main(String[] args) {
          Dragon dragon = new Dragon();
          System.out.println(dragon.identityMyself()); // "I am able to lay eggs"
       }
    }

     

    Dragon이 두 인터페이스를 상속했고 EggLayer에 정의된 identityMySelf()를 사용했다. 결합의 관점에서 살펴보면 당연히 EggLayer의 메서드를 호출하는 게 맞다. 그리고 실제로도 그렇게 동작한다. Animal의 identityMySelf()는 이미 EggLayer 인터페이스에서 오버라이딩되었기 때문에 호출되지 않고 오버라이딩된 EggLayer 메서드가 호출된다

     

    Dragon 관계도

     

    3. 만약 2개 이상의 default 메서드끼리 충돌하거나 혹은 abstrac와 default가 충돌하는 경우 자바 컴파일러는 에러를 발생한다.

    이는 상위에 명시된 메서드를 구현체에서 직접 명시적으로 대체해야 한다.

    interface OperateCar{
       default public String startEngine(){
          return "OperateCar startEngine";
       }
    }
    
    interface FlyCar{
       default public String startEngine(){
          return "FlyCar startEngine";
       }
    }
    
    
    public class FlyingCar implements OperateCar, FlyCar{
       @Override public String startEngine() {
          return OperateCar.super.startEngine() + " and " + FlyCar.super.startEngine();
       }
    }

     

     OperateCar, FlyCar 둘 다 구현 하고 메서드를 오버라이딩해야 한다. 둘의 결합력은 같기 때문에 컴파일러가 선택을 하지 못하는 것이다. super키워드를 사용하여 super타입에서 정의된 default 구현을 호출할 수 있다.

    • super 앞에는 구현하는 인터페이스를 직접 명시해줘야 한다. 이러한 메서드 호출 방식은 인터페이스 다중 구현에서 중복되는 메서드를 구분해줄 뿐만 아니라 super 클래스와 인터페이스 모두에서 default 메서드를 호출 할 수 있다.

     

    FlyingCar 관계도

     

    4. 클래스에 상속된 인스턴스 메서드는 인터페이스의 abstract 메서드를 오버라이딩 할 수 있다.

    1번에서 살짝 언급한 부분이다. 상속받은 클래스에 abstract메서드와 동일한 인스턴스 메서드가 있다면 abstract 메서드는 굳이 구현하지 않아도 된다. 

    interface Mammal{
       String identityMyself();
    }
    
    class Horse1{
       public String identityMyself(){
          return "I am a horse";
       }
    }
    
    public class Mustang extends Horse1 implements Mammal{
       public static void main(String[] args) {
          Mustang mustang = new Mustang();
          System.out.println(mustang.identityMyself()); // "I am a horse"
       }
    }

     

    mustang.identityMySelf는 "I am a horse"를 반환한다. Horse로 부터 상속받은 인스턴스 메서드가 Mammal에 있는 같은 이름을 가진 abstract 메서드를 오버라이딩한 것이다. 

     

    Mustang 관계도

     

    마지막. 인터페이스의 static 메서드는 상속되지 않는다

    인터페이스 static 메서드는 클래스의 static 멤버처럼 직접 자신의 인터페이스 객체를 호출(ex. Interface.staticMethod())해서 사용해야 한다.

     

    오버라이딩은 다형성의 범주 안에 들어가있는 개념으로 런타임에 바인딩되면서 다형성이 충족되어진다. static은 컴파일시점에 JVM의 공유 메모리(Method Area)에 등록이 된다. 그러므로 런타임시점에  다형성과는 거리가 멀기 때문에 오버라이딩이 충족되지않고 hiding이라는 개념을 적용한다고 볼 수 있다.

    interface Mammal{
       static String identityMyself(){
          return "I am a Mammal";
       }
    }
    
    public class Mustang implements Mammal{
       public static void main(String[] args) {
          Mammal mustang = new Mustang();
          // System.out.println(mustang.identityMyself()); // 컴파일 에러
       }
    }

     

    부모클래스와 같은 시그니처를 가진 메서드가 있을 때 정의하는 방법

    마지막으로 다시 한번 정리하면 부모-조상의 같은 시그니처를 가진 메서드를 만났을 때 인스턴스 메서드는 오버라이딩을 하고 static 메서드는 hiding을 한다.

     

      부모클래스 인스턴스 메서드 부모클래스 Static 메서드
    자식클래스 인스턴스 메서드 Overrides X (Generates a compile-time error)
    자식클래스 Static 메서드 X (Generates a compile-time error) Hides

     


    참고

    https://docs.oracle.com/javase/tutorial/java/IandI/override.html

    https://stackoverflow.com/questions/2475259/can-i-override-and-overload-static-methods-in-java