3. Java Tutorials

[Java Tutorials] #11 Lesson: Interfaces and Inheritance[Inheritance 1]

Dorothy. 2024. 8. 27. 17:30

Inheritance

[공식 튜토리얼]

 

이전 강의에서 상속이 여러 번 언급되는 것을 보았습니다. Java 언어에서 클래스는 다른 클래스에서 파생될 수 있으므로 해당 클래스의 필드와 메서드를 상속받을 수 있습니다.


정의: 다른 클래스에서 파생된 클래스를 하위 클래스(파생 클래스, 확장 클래스 또는 하위 클래스라고도 함)라고 합니다. 하위 클래스[자식 클래스]로 파생되는 클래스[부모 클래스]를 슈퍼클래스(기본 클래스 또는 부모 클래스라고도 함)라고 합니다.

슈퍼클래스가 없는 Object를 제외하고 모든 클래스는 오직 단 하나의 Direct Super Class(단일 상속)를 갖습니다. 다른 명시적인 슈퍼클래스가 없으면 모든 클래스는 암시적으로 Object의 하위 클래스입니다.

※ Direct Super Class란? 상속 계층 구조에서 바로 위 super class를 의미를 의미합니다. direct super class 이외의 super class에 대한 정식 명칭을 자바에서는 정의하지 않았지만, 일부 ancestor class 또는 상위 클래스 그리고 단지 super class라고 부릅니다.

클래스는 클래스 등에서 파생된 클래스에서 파생될 수 있으며 궁극적으로 최상위 클래스인 Object에서 파생될 수 있습니다. 이러한 클래스는 Object로 다시 확장되는 상속 체인의 모든 클래스의 자손이라고 합니다.


상속의 개념은 간단하지만 강력합니다. 새 클래스를 만들고 싶은데 원하는 코드 일부가 포함된 클래스가 이미 있는 경우 기존 클래스에서 새 클래스를 파생시킬 수 있습니다. 이렇게 하면 기존 클래스의 필드와 메서드를 직접 작성(및 디버그)하지 않고도 재사용할 수 있습니다.

하위 클래스는 상위 클래스에서 모든 멤버(필드, 메서드 및 중첩 클래스)를 상속합니다. 생성자는 멤버가 아니므로 하위 클래스에서 상속되지 않지만 상위 클래스의 생성자는 하위 클래스에서 호출될 수 있습니다.

 

The Java Platform Class Hierarchy

java.lang 패키지에 정의된 Object 클래스는 사용자가 작성한 클래스를 포함하여 모든 클래스에 공통적인 동작을 정의하고 구현합니다. Java 플랫폼에서는 많은 클래스가 Object에서 직접 파생되고, 다른 클래스는 해당 클래스 중 일부에서 파생되는 식으로 클래스 계층 구조를 형성합니다.

All Classes in the Java Platform are Descendants of Object

 

계층 구조의 최상위에 있는 Object는 모든 클래스 중에서 가장 일반적인 클래스입니다. 계층 구조의 맨 아래에 있는 클래스는 보다 특수한 동작을 제공합니다.

 

An Example of Inheritance

다음은 클래스 및 객체 단원에서 제시된 Bicycle 클래스의 가능한 구현을 위한 샘플 코드입니다.

public class Bicycle {
        
    // the Bicycle class has three fields
    public int cadence;
    public int gear;
    public int speed;
        
    // the Bicycle class has one constructor
    public Bicycle(int startCadence, int startSpeed, int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;
    }
        
    // the Bicycle class has four methods
    public void setCadence(int newValue) {
        cadence = newValue;
    }
        
    public void setGear(int newValue) {
        gear = newValue;
    }
        
    public void applyBrake(int decrement) {
        speed -= decrement;
    }
        
    public void speedUp(int increment) {
        speed += increment;
    }
        
}

 

Bicycle의 하위 클래스인 MountainBike 클래스에 대한 클래스 선언은 다음과 같습니다.

public class MountainBike extends Bicycle {
        
    // the MountainBike subclass adds one field
    public int seatHeight;

    // the MountainBike subclass has one constructor
    public MountainBike(int startHeight,
                        int startCadence,
                        int startSpeed,
                        int startGear) {
        super(startCadence, startSpeed, startGear);
        seatHeight = startHeight;
    }   
        
    // the MountainBike subclass adds one method
    public void setHeight(int newValue) {
        seatHeight = newValue;
    }   
}

 

MountainBike는 Bicycle의 모든 필드와 메소드를 상속하고 SeatHeight 필드와 이를 설정하는 메소드를 추가합니다. 생성자를 제외하면 4개의 필드와 5개의 메서드를 사용하여 처음부터 완전히 새로운 MountainBike 클래스를 작성한 것과 같습니다. 그러나 모든 작업을 수행할 필요는 없었습니다. Bicycle 클래스의 메서드가 복잡하고 디버깅하는 데 상당한 시간이 걸렸다면 이는 특히 유용합니다.

 

What You Can Do in a Subclass

하위 클래스는 하위 클래스가 어떤 패키지에 있는지에 관계없이 상위 클래스의 모든 public 및 protected 멤버를 상속합니다. 하위 클래스가 상위 클래스와 동일한 패키지에 있는 경우 상위 클래스의 package-private 멤버도 상속합니다. 상속된 멤버를 그대로 사용하거나, 바꾸거나, 숨기거나, 새 멤버로 보완할 수 있습니다.

⦁ 상속된 필드는 다른 필드와 마찬가지로 직접 사용할 수 있습니다.
⦁ 서브클래스의 필드를 슈퍼클래스의 필드와 동일한 이름으로 선언하여 숨길 수 있습니다(권장하지 않음).
⦁ 슈퍼클래스에는 없는 새로운 필드를 서브클래스에서 선언할 수 있습니다.
⦁ 상속받은 메소드는 그대로 사용이 가능합니다.
⦁ 슈퍼클래스와 동일한 시그니처를 갖는 새로운 인스턴스 메서드를 서브클래스에 작성하여 Override[재정의]할 수 있습니다.
⦁ 슈퍼클래스와 동일한 시그니처를 갖는 새로운 static 메소드를 서브클래스에 작성하여 슈퍼클래스의 이 static 메서드를 숨길 수 있습니다.
⦁ 슈퍼클래스에는 없는 새로운 메소드를 서브클래스에서 선언할 수 있습니다.
⦁암시적으로 또는 키워드 super를 사용하여 슈퍼클래스의 생성자를 호출하는 하위 클래스 생성자를 작성할 수 있습니다.

이 강의의 다음 섹션에서는 이러한 주제에 대해 자세히 설명합니다.

 

Private Members in a Superclass

하위 클래스는 상위 클래스의 private 멤버를 상속하지 않습니다그러나 슈퍼클래스의 private 필드에 액세스하기 위한 public 또는 protected 메소드가 있는 경우 하위 클래스에서도 이러한 메소드를 사용할 수 있습니다.

중첩 클래스는 자신을 둘러싸는 외부 클래스의 모든 private 멤버(필드와 메서드 모두)에 액세스할 수 있습니다. 따라서 하위 클래스에서 상속된 public 또는 protected 중첩 클래스는 슈퍼클래스의 모든 private 멤버에 간접적으로 액세스할 수 있습니다.


하위 클래스 코드[인스턴스 메서드]에서 수퍼 클래스의 non-static nested 클래스의 멤버를 사용할 수 있는 방법은 없습니다   

예를 들어, 다음과 같은 클래스 상속을 생각해볼 수 있습니다:

package com.intheeast.interfaces;

public class SuperClass {
	
	private int privateField;
    NestedClass nc;
	
	public SuperClass() {
		this.privateField = 1;
		System.out.println("SuperClass Constructor");
	}
    
    public SuperClass(NestedClass nc) {
		this.privateField = 1;
        this.nc = nc
		System.out.println("SuperClass Constructor");
	}
	
	public int getPrivateField() {
    	this.nc.accessPrivateField();
		return privateField;
        
	}

	public void setPrivateField(int privateField) {
        //accessPrivateField();
		this.privateField = privateField;
	}

	class NestedClass {
		
		public NestedClass() {
			System.out.println("NestedClass Constructor");
		}
		
		void accessPrivateField() {
			System.out.printf("accessPrivateField:%d \n", privateField);
		}
	}
}

 

하위 클래스 코드에서 수퍼 클래스의 non-static nested 클래스의 메서드를 호출할 수 있는 방법은 아래와 같습니다.

package com.intheeast.interfaces;

public class SubClass extends SuperClass{

	public SubClass() {
		System.out.println("SubClass");
	}
	
	public int getSuperPrivateField() {
		return super.getPrivateField();		
	}
	
	public void setSuperPrivateField(int field) {
		super.setPrivateField(field);
		return ;		
	}
	
	class SubNestedClass extends SuperClass.NestedClass {
		
		public SubNestedClass() {
			System.out.println("SubNestedClass constructor");
		}
		
        void accessSuperPrivateField() {
            // SubNestedClass는 SuperClass의 NestedClass를 상속받았으므로, 
            // SuperClass의 privateField에 접근할 수 있습니다.
            // 하지만 직접적으로 접근하는 것이 아니라, 
            // SuperClass의 인스턴스를 통해 간접적으로 접근하게 됩니다.
            accessPrivateField(); // 이것은 super.accessPrivateField()와 동일합니다.
        }
    }
}


위 코드에서 SubNestedClass는 SuperClass.NestedClass를 상속받았으므로 SuperClass의 privateField에 접근할 수 있습니다. 하지만 이 접근은 SubNestedClass가 SuperClass의 인스턴스를 통해 accessPrivateField() 메서드를 호출함으로써 이루어집니다.

public static void main(String[] args) {
		
	    SubClass sc = new SubClass();
		System.out.printf("getPrivateField:%d \n", sc.getPrivateField());
		System.out.printf("getSuperPrivateField:%d \n", sc.getSuperPrivateField());

		NestedClass nc = sc.new NestedClass();
		nc.accessPrivateField();
		
		SubNestedClass snc = sc.new SubNestedClass();
		snc.accessSuperPrivateField();
}

 

 

Casting Objects

우리가 본 것처럼, 객체는 인스턴스화된 클래스의 데이터 타입을 가집니다. 예를 들어,

public MountainBike myBike = new MountainBike();


위와 같이 작성하면, myBike는 MountainBike 타입입니다.

MountainBike는 Bicycle과 Object로부터 상속받았습니다. 따라서 MountainBike는 Bicycle이며, 또한 Object입니다. 그러므로 Bicycle이나 Object 객체가 필요한 곳에서 MountainBike를 사용할 수 있습니다.

반대는 반드시 참이 아닙니다. Bicycle이 MountainBike일 수도 있지만, 반드시 그렇지는 않습니다. 마찬가지로 Object가 Bicycle이나 MountainBike일 수도 있지만, 반드시 그렇지는 않습니다.

캐스팅은 상속 및 구현에 의해 허용된 객체들 사이에서 한 타입의 객체를 다른 타입의 객체로 사용하는 것을 보여줍니다. 예를 들어,

Object obj = new MountainBike();
현재 위 예제 코드에 MountainBike 클래스의 디폴트 생성자가 정의되지 않았습니다. MountainBike 클래스에 디폴트 생성자를 정의해야 합니다. 그러나 Bicycle 클래스 또한 디폴트 생성자가 정의되지 않았기 때문에 아래와 같은 컴파일 에러가 발생합니다. 그러므로 Bicycle 클래스에 디폴트 생성자를 정의해야 합니다.
Implicit super constructor Bicycle() is undefined. Must explicitly invoke another constructor

 

위와 같이 작성하면, obj는 Object이면서 MountainBike입니다 (나중에 obj가 MountainBike가 아닌 다른 객체에 할당되기 전까지). 이것을 암시적[implicit] 캐스팅이라고 합니다.
반면에,

MountainBike myBike = obj;

 

위와 같이 작성하면 컴파일 타임 오류가 발생합니다. 왜냐하면 컴파일러는 obj가 MountainBike인지 알 수 없기 때문입니다. 그러나 우리는 obj에 MountainBike를 할당할 것을 컴파일러에게 명시적[explicit]으로 캐스팅하여 약속할 수 있습니다:

MountainBike myBike = (MountainBike)obj;


이 캐스팅은 런타임에 obj가 MountainBike로 할당되었는지 확인하는 체크를 삽입합니다. 따라서 컴파일러는 안전하게 obj가 MountainBike라고 가정할 수 있습니다. 만약 런타임에 obj가 MountainBike가 아니라면, 예외가 발생합니다.

 

참고: instanceof 연산자를 사용하여 특정 객체의 타입에 대한 논리적 테스트를 수행할 수 있습니다. 이렇게 하면 부적절한 캐스트로 인한 런타임 오류를 방지할 수 있습니다. 예를 들어:

if (obj instanceof MountainBike) {
    MountainBike myBike = (MountainBike)obj;
}

 

여기에서 instanceof 연산자는 obj가 MountainBike를 참조하는지 확인하므로 런타임 예외[java.lang.ClassCastException]가 발생하지 않는다는 사실을 알고 캐스팅을 수행할 수 있습니다.

 

Mutiple Inheritance of State, Implementation, and Type

클래스와 인터페이스의 한 가지 중요한 차이점은 클래스는 필드를 가질 수 있지만 인터페이스는 가질 수 없다[단, enum 타입과 final 상수는 제외]는 것입니다. 또한 클래스를 인스턴스화하여 인터페이스로는 수행할 수 없는 객체를 생성할 수 있습니다. What Is an Object? 섹션에 설명된 대로 객체는 클래스에 정의된 필드에 state를 저장합니다. Java 프로그래밍 언어에서 둘 이상의 직접[Direct] 클래스 확장[상속]을 허용하지 않는 이유 중 하나는 state의 다중 상속 문제, 즉 여러 클래스에서 필드를 상속하는 기능을 방지하기 위한 것입니다. 예를 들어, 여러 클래스를 확장[상속]하는 새 클래스를 정의할 수 있다고 가정해 보겠습니다. 해당 클래스를 인스턴스화하여 객체를 생성하면 해당 객체는 모든 슈퍼클래스에서 필드를 상속받습니다. 서로 다른 슈퍼클래스의 메서드나 생성자가 동일한 필드를 인스턴스화하면 어떻게 되나요? 어떤 메서드 또는 생성자가 우선적으로 적용되나요? 인터페이스에는 필드가 포함되어 있지 않으므로 상태의 다중 상속으로 인해 발생하는 문제에 대해 걱정할 필요가 없습니다.

// 두 개의 슈퍼클래스를 정의합니다.
class SuperClassA {
    int state = 1;

    SuperClassA() {
        state = 2; // 생성자에서 state 값을 2로 설정
    }

    void printState() {
        System.out.println("State from SuperClassA: " + state);
    }
}

class SuperClassB {
    int state = 3;

    SuperClassB() {
        state = 4; // 생성자에서 state 값을 4로 설정
    }

    void printState() {
        System.out.println("State from SuperClassB: " + state);
    }
}

// 다중 상속이 가능하다고 가정하고 두 슈퍼클래스를 상속받는 서브클래스를 정의합니다.
class SubClass extends SuperClassA /*, SuperClassB*/ {
    // 다중 상속이 허용되지 않기 때문에 두 번째 슈퍼클래스를 주석 처리했습니다.

    // 메서드를 오버라이드하여 상태를 출력합니다.
    @Override
    void printState() {
        // 만약 다중 상속이 가능하다면, 어떤 state를 출력해야 할지 애매해집니다.
        super.printState(); // SuperClassA의 state 출력
        // SuperClassB의 state도 출력해야 한다면?
        // super.printState(); // SuperClassB의 state 출력 (애매해짐)
    }
}

public class MultipleInheritanceDemo {
    public static void main(String[] args) {
        SubClass sub = new SubClass();
        sub.printState();
    }
}

위 예제에서 SubClass가 SuperClassA와 SuperClassB를 동시에 상속받는 경우를 가정해보면, 
SubClass의 인스턴스는 두 개의 state 필드를 가지게 됩니다. 
이로 인해 어떤 state를 참조해야 하는지 혼란이 발생합니다.

SubClass의 생성자가 호출될 때 SuperClassA와 SuperClassB의 생성자가 호출되는데, 
어느 생성자가 우선시 되는가?
SubClass의 printState 메서드가 호출될 때 어느 슈퍼클래스의 state를 출력해야 하는가?
이러한 문제를 피하기 위해 Java는 다중 상속을 허용하지 않습니다. 
대신, 인터페이스를 사용하여 다중 상속의 장점을 활용하면서도 상태 충돌 문제를 피할 수 있습니다. 
인터페이스는 필드를 가질 수 없기 때문에 상태의 다중 상속 문제를 유발하지 않습니다.

 

Multiple inheritance of implementation[구현의 다중 상속:자바는 지원하지 않음]은 여러 클래스에서 메서드 정의를 상속하는 기능입니다. 이러한 유형의 다중 상속에서는 이름 충돌 및 모호함과 같은 문제가 발생합니다. 이러한 유형의 다중 상속을 지원하는 프로그래밍 언어의 컴파일러는 동일한 이름의 메서드가 포함된 슈퍼클래스를 발견할 때 액세스하거나 호출할 멤버나 메서드를 결정할 수 없는 경우가 있습니다. 또한 프로그래머는 슈퍼클래스에 새 메서드를 추가하여 자신도 모르게 이름 충돌을 일으킬 수 있습니다. 자바 인터페이스의 디폴트 메소드[Default Method]는 구현의 다중 상속의 한 형태를 도입합니다. 자바 클래스는 동일한 이름을 가진 default method를 선언할 수 있는 둘 이상의 인터페이스를 구현할 수 있습니다. Java 컴파일러는 특정 클래스가 사용하는 default method를 결정하는 몇 가지 규칙을 제공합니다.

interface InterfaceA {
    default void printMessage() {
        System.out.println("InterfaceA's message");
    }
}

interface InterfaceB {
    default void printMessage() {
        System.out.println("InterfaceB's message");
    }
}

class MyClass implements InterfaceA, InterfaceB {
    // 명시적으로 디폴트 메소드를 오버라이드하여 어떤 메소드를 사용할지 결정해야 함
    @Override
    public default void printMessage() {
        // InterfaceA의 디폴트 메소드를 호출하려면 super를 통해 명시적으로 호출할 수 있습니다.
        InterfaceA.super.printMessage();
        // 또는 InterfaceB의 디폴트 메소드를 호출할 수도 있습니다.
        InterfaceB.super.printMessage();
        // 커스텀 구현을 추가할 수도 있습니다.
        System.out.println("MyClass's custom message");
    }
}

public class TestDefaultMethod {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        myClass.printMessage();
    }
}

 

Java 프로그래밍 언어는 multiple inheritance of type[타입의 다중 상속]을 지원합니다. 이는 클래스가 둘 이상의 인터페이스를 구현하는 기능입니다. 객체는 자체 클래스의 타입과 클래스가 구현하는 모든 인터페이스의 타입 등 여러 타입들을 가질 수 있습니다. 즉, 변수[ex:Flyable flyable]가 인터페이스 타입으로 선언되면 해당 값은 인터페이스를 구현하는 모든 클래스에서 인스턴스화되는 모든 객체를 참조할 수 있습니다. 이에 대해서는 Using an Interface as a Type 섹션에서 설명합니다.

// 인터페이스를 정의합니다.
interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

// 클래스를 정의하고 인터페이스들을 구현합니다.
class Duck implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("Duck is flying.");
    }

    @Override
    public void swim() {
        System.out.println("Duck is swimming.");
    }

    void quack() {
        System.out.println("Duck is quacking.");
    }
}

public class MultipleInheritanceDemo {
    public static void main(String[] args) {
        // Duck 객체를 생성합니다.
        Duck duck = new Duck();

        // Flyable 타입 변수에 Duck 객체를 할당합니다.
        Flyable flyable = duck;
        flyable.fly();
        // flyable.swim(); // 컴파일 오류: Flyable 타입에서는 swim() 메서드에 접근할 수 없습니다.

        // Swimmable 타입 변수에 Duck 객체를 할당합니다.
        Swimmable swimmable = duck;
        swimmable.swim();
        // swimmable.fly(); // 컴파일 오류: Swimmable 타입에서는 fly() 메서드에 접근할 수 없습니다.

        // Duck 타입 변수에 Duck 객체를 할당합니다.
        Duck sameDuck = (Duck) flyable;
        sameDuck.fly();
        sameDuck.swim();
        sameDuck.quack();
    }
}

 

구현의 다중 상속과 마찬가지로 클래스는 확장하는 인터페이스에서 정의된 (default 또는 static)메서드의 다양한 구현을 상속할 수 있습니다. 이 경우 컴파일러나 사용자는 어느 것을 사용할지 결정해야 합니다.

As with multiple inheritance of implementation, a class can inherit different implementations of a method defined (as default or static) in the interfaces that it extends. In this case, the compiler or the user must decide which one to use.

 

두 개의 인터페이스가 동일한 이름과 시그니처를 가진 추상 메서드를 가지고 있는 경우

두 개의 인터페이스가 동일한 이름과 시그니처를 가진 추상 메서드를 가지고 있는 경우, 이 두 인터페이스를 구현하려는 클래스는 그 메서드를 한 번만 구현하면 됩니다. 왜냐하면 자바에서는 다중 상속을 지원하지 않지만, 인터페이스 다중 상속을 통해 동일한 이름과 시그니처를 가진 메서드를 여러 인터페이스로부터 상속받을 수 있기 때문입니다.

다음은 그 예시입니다:

interface InterfaceA {
    void commonMethod();
}

interface InterfaceB {
    void commonMethod();
}

class ConcreteClass implements InterfaceA, InterfaceB {
    @Override
    public void commonMethod() {
        System.out.println("Implementation of commonMethod");
    }
}

public class TestAbstractMethod {
    public static void main(String[] args) {
        ConcreteClass obj = new ConcreteClass();
        obj.commonMethod();
        
        // InterfaceA 타입의 참조 변수로 호출
        InterfaceA objA = obj;
        objA.commonMethod();
        
        // InterfaceB 타입의 참조 변수로 호출
        InterfaceB objB = obj;
        objB.commonMethod();
    }
}


위 예시에서 InterfaceA와 InterfaceB는 동일한 이름과 시그니처를 가진 추상 메서드 commonMethod를 가지고 있습니다. ConcreteClass는 이 두 인터페이스를 구현하며, commonMethod를 한 번만 구현합니다.

이렇게 하면, ConcreteClass의 인스턴스를 통해 commonMethod를 호출할 수 있으며, 두 인터페이스 타입의 참조 변수로도 동일한 메서드를 호출할 수 있습니다. 출력 결과는 다음과 같습니다:

Implementation of commonMethod
Implementation of commonMethod
Implementation of commonMethod

 

이렇게 구현하면 각 인터페이스에서 요구하는 동일한 이름과 시그니처를 가진 메서드를 한 번만 구현하여, 다중 상속의 문제를 해결할 수 있습니다.

 

Overriding and Hiding Methods

Instance Methods

슈퍼클래스의 인스턴스 메소드와 동일한 시그니처(이름, 파라미터의 수 및 유형) 및 리턴 타입을 갖는 서브클래스의 인스턴스 메소드는 슈퍼클래스의 메소드를 대체(재정의)합니다.

메서드를 재정의[override]하는 하위 클래스의 기능을 통해 클래스는 동작이 "close enough" 슈퍼클래스에서 상속된 다음 필요에 따라 동작을 수정할 수 있습니다. 오버라이드 메서드에는 오버라이드하는 메서드와 동일한 이름, 파라미터 개수 및 타입, 리턴 타입이 있습니다. 오버라이드 메서드는 오버라이드된 메서드에서 리턴된 타입의 하위 타입을 반환할 수도 있습니다. 이 하위 타입을 공변[covariant] 리턴 타입이라고 합니다.

// 슈퍼클래스 Animal
class Animal {
    // 슈퍼클래스의 메서드
    public Animal speak() {
        System.out.println("Animal makes a sound");
        return this;
    }
}

// 하위 클래스 Dog
class Dog extends Animal {
    // 메서드를 재정의 (override)
    @Override
    public Dog speak() {
        System.out.println("Dog barks");
        return this;
    }
}

// 하위 클래스 Cat
class Cat extends Animal {
    // 메서드를 재정의 (override)
    @Override
    public Cat speak() {
        System.out.println("Cat meows");
        return this;
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Animal();
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        // 슈퍼클래스의 메서드 호출
        myAnimal.speak(); // 출력: Animal makes a sound

        // 하위 클래스에서 재정의된 메서드 호출
        myDog.speak(); // 출력: Dog barks
        myCat.speak(); // 출력: Cat meows
    }
}

 

위 코드는, Dog 클래스의 speak 메서드는 Dog 타입을 반환하고, Cat 클래스의 speak 메서드는 Cat 타입을 리턴합니다. 이는 자바에서 허용되는 공변 리턴 타입의 예입니다.

메서드를 오버라이드할 때 슈퍼클래스의 메서드를 오버라이드할 것임을 컴파일러에 지시하는 @Override 어노테이션을 사용할 수 있습니다. 어떤 이유로 컴파일러가 해당 메서드가 슈퍼클래스 중 하나에 존재하지 않음을 감지하면 오류가 발생합니다. @Override에 대한 자세한 내용은 주석을 참조하세요.

 

Static Methods

서브클래스가 슈퍼클래스의 정적 메서드와 동일한 시그니처를 사용하여 정적 메서드를 정의하는 경우 서브클래스의 메서드는 슈퍼클래스의 메서드를 숨깁니다.

정적 메서드를 숨기는 것과 인스턴스 메서드를 오버라이드하는 것의 차이점은 중요한 의미를 갖습니다.
⦁ 호출되는 오버라이드된 인스턴스 메서드의 버전은 하위 클래스에 있는 버전입니다.
⦁ 호출되는 숨겨진 정적 메소드의 버전은 슈퍼클래스에서 호출되는지, 서브클래스에서 호출되는지에 따라 다릅니다.

 

다음 두 개의 클래스가 있습니다. 첫 번째는 하나의 인스턴스 메서드와 하나의 정적 메서드를 포함하는 Animal입니다.

public class Animal {
    public static void testClassMethod() {
        System.out.println("The static method in Animal");
    }
    public void testInstanceMethod() {
        System.out.println("The instance method in Animal");
    }
}

 

Animal의 하위 클래스인 두 번째 클래스는 Cat이라고 합니다.

public class Cat extends Animal {
    public static void testClassMethod() {
        System.out.println("The static method in Cat");
    }
    public void testInstanceMethod() {
        System.out.println("The instance method in Cat");
    }

    public static void main(String[] args) {
        Cat myCat = new Cat();
        Animal myAnimal = myCat;
        Animal.testClassMethod();
        Cat.testClassMethod();
        myAnimal.testInstanceMethod();
    }
}

 

Cat 클래스는 Animal의 인스턴스 메서드를 오버라이드하고 Animal의 정적 메서드를 숨깁니다. 이 클래스의 main 메서드는 Cat 인스턴스를 생성하고 Animal 클래스의 스태틱 메서드인 testClassMethod()를 호출하고 인스턴스 메서드인 testInstanceMethod()를 호출합니다.

 

이 프로그램의 출력은 다음과 같습니다.

The static method in Animal
The static method in Cat
The instance method in Cat

 

약속한 대로 호출되는 숨겨진 정적 메서드의 버전은 슈퍼클래스에 있는 버전이고, 호출되는 오버라이드된 인스턴스 메서드의 버전은 하위 클래스에 있는 버전입니다.

 

Interface Methods

인터페이스의 디폴트 메서드와 추상 메서드는 인스턴스 메서드처럼 상속됩니다. 그러나 클래스 또는 인터페이스의 상위 타입이 동일한 시그니처를 가진 여러 디폴트 메소드를 제공하는 경우 Java 컴파일러는 상속 규칙에 따라 이름 충돌을 해결합니다. 이러한 규칙은 다음 두 가지 원칙에 따라 구동됩니다.

 

 인스턴스 메소드는 인터페이스 디폴트 메소드보다 선호됩니다.

  다음 클래스와 인터페이스를 고려하세요.

public class Horse {
    public String identifyMyself() {
        return "I am a horse.";
    }
}
public interface Flyer {
    default public String identifyMyself() {
        return "I am able to fly.";
    }
}
public interface Mythical {
    default public String identifyMyself() {
        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.identifyMyself());
    }
}

 

Pegasus.identifyMyself 메소드는 I am a Horse라는 문자열을 반환합니다.

 

 다른 후보에 의해 이미 오버라이드된 메서드는 무시됩니다. 이러한 상황은 상위 타입이 공통 조상을 공유할 때 발생할 수 있습니다.

다음 클래스와 인터페이스를 고려하세요.

public interface Animal {
    default public String identifyMyself() {
        return "I am an animal.";
    }
}

public interface EggLayer extends Animal {
    default public String identifyMyself() {
        return "I am able to lay eggs.";
    }
}

public interface FireBreather extends Animal { }

public class Dragon implements EggLayer, FireBreather {
    public static void main (String... args) {
        Dragon myApp = new Dragon();
        System.out.println(myApp.identifyMyself());
    }
}

 

Dragon.identifyMyself 메소드는 I am able to lay eggs 문자열을 리턴합니다.

 

독립적으로 정의된 두 개 이상의 디폴트 메소드가 충돌하거나 디폴트 메소드가 추상 메소드와 충돌하는 경우 Java 컴파일러는 컴파일러 오류를 생성합니다. 상위 타입 메소드를 명시적으로 대체해야 합니다.

 

이제 날 수 있는 컴퓨터로 조종되는 자동차의 예를 생각해 보십시오. 동일한 디폴트 메소드(startEngine)을 제공하는 두 개의 인터페이스(OperateCar 및 FlyCar)가 있습니다.

public interface OperateCar {
    // ...
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}
public interface FlyCar {
    // ...
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}

 

OperateCar와 FlyCar를 모두 구현하는 클래스는 startEngine 메서드를 오버라이드해야 합니다. super 키워드를 사용하여 디폴트 구현 중 하나를 호출할 수 있습니다.

public class FlyingCar implements OperateCar, FlyCar {
    // ...
    public int startEngine(EncryptedKey key) {
        FlyCar.super.startEngine(key);
        OperateCar.super.startEngine(key);
    }
}

 

super 앞에 오는 이름(이 예에서는 FlyCar 또는 OperateCar)은 호출된 메서드에 대한 디폴트를 정의하거나 상속하는 직접적인 슈퍼인터페이스를 참조해야 합니다. 이러한 형태의 메소드 호출은 동일한 시그니처를 가진 디폴트 메소드를 포함하는 여러 구현 인터페이스를 구별하는 것으로 제한되지 않습니다. super 키워드를 사용하여 클래스와 인터페이스 모두에서 디폴트 메서드를 호출할 수 있습니다.

 

클래스에서 상속된 인스턴스 메서드는 추상 인터페이스 메서드를 오버라이드할 수 있습니다. 다음 인터페이스와 클래스를 고려하세요.

public interface Mammal {
    String identifyMyself();
}
public class Horse {
    public String identifyMyself() {
        return "I am a horse.";
    }
}
public class Mustang extends Horse implements Mammal {
    public static void main(String... args) {
        Mustang myApp = new Mustang();
        System.out.println(myApp.identifyMyself());
    }
}

 

Mustang.identifyMyself 메소드는 I am a Horse 라는 문자열을 리턴합니다. Mustang 클래스는 Horse 클래스로부터 identifyMyself 메소드를 상속받으며, 이는 Mammal 인터페이스에 있는 동일한 이름의 추상 메소드를 대체합니다.

 

참고: 인터페이스의 정적 메서드는 상속되지 않습니다.

 

Modifiers

메서드 오버라이딩에서 액세스 제한자는 오버라이딩된 메서드보다 더 넓은 접근을 허용하지만, 더 좁은 접근을 허용하지는 않습니다. 예를 들어, 슈퍼클래스의 protected 인스턴스 메서드는 서브클래스에서 public으로 변경할 수 있지만 private으로는 변경할 수 없습니다.

또한 슈퍼클래스의 인스턴스 메서드를 서브클래스의 static 메서드로 변경하려고 하거나 반대로 변경하려고 하면 컴파일 타임 에러가 발생합니다.

class Animal {
  protected void makeSound() {
    System.out.println("Generic animal sound");
  }
}

class Dog extends Animal {
  @Override // 명시적 오버라이딩 표시 (선택사항)
  public void makeSound() {
    System.out.println("Woof!");
  }
}

public class Main {
  public static void main(String[] args) {
    Animal animal = new Animal();
    animal.makeSound();  // Generic animal sound

    Dog dog = new Dog();
    dog.makeSound();  // Woof!
  }
}

 

설명

  • Animal 클래스는 makeSound 메서드를 protected 액세 제한자로 선언합니다.
  • Dog 클래스는 Animal 클래스를 상속받고 makeSound 메서드를 오버라이딩합니다.
  • 오버라이딩된 메서드의 접근 제한자는 public으로 변경되어 더 넓은 접근을 허용합니다.
  • main 메서드에서 Animal 객체와 Dog 객체를 생성하여 각각의 makeSound 메서드를 호출합니다.

 

컴파일 에러 예시

class Animal {
  public void makeSound() {
    System.out.println("Generic animal sound");
  }
}

class Dog extends Animal {
  @Override
  static void makeSound() {  // 인스턴스 메서드를 static 메서드로 변경하려고 시도
    System.out.println("Woof!");
  }
}

 

위 코드는 컴파일 에러를 발생시킵니다. 

 

Summary

다음 테이블에는 슈퍼클래스의 메서드와 동일한 시그니처를 사용하여 메서드를 정의할 때 발생하는 것들이 요약되어 있습니다.

Defining a Method with the Same Signature as a Superclass's Method

  Superclass Instance Method Superclass Static Method
Subclass Instance Method Overrides Generates a compile-time error
Subclass Static Method Generates a compile-time error Hides

 


참고: 하위 클래스에서는 상위 클래스에서 상속된 메서드를 오버로드할 수 있습니다. 이러한 오버로드된 메서드는 슈퍼클래스 인스턴스 메서드를 숨기거나 오버라이드하지 않습니다. 이는 하위 클래스에 고유한 새로운 메서드입니다.