상속

이전 섹션에서 상속을 여러 번 언급했습니다. Java 언어에서 클래스는 다른 클래스에서 파생될 수 있으며, 따라서 해당 클래스의 필드와 메서드를 상속할 수 있습니다.

Definitions: 다른 클래스에서 파생된 클래스를 서브클래스(파생 클래스, 확장 클래스 또는 자식 클래스라고도 함)라고 합니다. 하위 클래스가 파생된 클래스를 수퍼클래스(베이스 클래스 또는 상위 클래스라고도 함)라고 합니다.

슈퍼클래스가 없는 Object를 제외하고 모든 클래스는 단 하나의 직접적인 슈퍼클래스(단일 상속)를 가집니다. 다른 명시적 슈퍼클래스가 없는 경우, 모든 클래스는 암시적으로 Object의 서브클래스입니다.

클래스는 클래스에서 파생된 클래스에서 파생된 클래스 등으로부터 파생될 수 있으며, 궁극적으로 최상위 클래스인 Object에서 파생될 수 있습니다. 이러한 클래스는 Object로 거슬러 올라가는 상속 체인의 모든 클래스에서 파생된다고 합니다.

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

서브클래스는 수퍼클래스로부터 모든 멤버(필드, 메서드, 중첩 클래스)를 상속받습니다. 생성자는 멤버가 아니므로 서브클래스에서 상속되지 않지만, 수퍼클래스의 생성자는 서브클래스에서 호출할 수 있습니다.

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

계층 구조의 맨 위에는 모든 클래스 중 가장 일반적인 Object가 있습니다. 계층 구조의 맨 아래에 있는 클래스는 보다 전문적인 동작을 제공합니다.

 

상속 예제

다음은 클래스와 객체 섹션에서 소개한 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;
    }   
}

MountainBikeBicycle의 모든 필드와 메서드를 상속하고 seatHeight 필드와 이를 설정하는 메서드를 추가합니다. 생성자를 제외하면 4개의 필드와 5개의 메서드가 있는 새로운 MountainBike 클래스를 처음부터 새로 작성한 것과 같습니다. 하지만 모든 작업을 할 필요는 없습니다. 특히 Bicycle 클래스의 메서드가 복잡하고 디버깅에 상당한 시간이 걸리는 경우 유용할 것입니다.

 

서브클래스에서 할 수 있는 일

서브클래스는 서브클래스가 어떤 패키지에 있든 상관없이 부모의 모든 공개보호 멤버를 상속합니다. 서브클래스가 부모와 같은 패키지에 있는 경우 부모의 패키지 비공개 멤버도 상속합니다. 상속된 멤버를 그대로 사용하거나, 교체하거나, 숨기거나, 새 멤버로 보완할 수 있습니다:

  • 상속된 필드는 다른 필드와 마찬가지로 직접 사용할 수 있습니다.
  • 하위 클래스에서 슈퍼클래스의 필드와 같은 이름으로 필드를 선언하여 필드를 숨길 수 있습니다(권장하지 않음).
  • 수퍼클래스에 없는 새 필드를 서브클래스에 선언할 수 있습니다.
  • 상속된 메서드는 그대로 직접 사용할 수 있습니다.
  • 서브클래스에 슈퍼클래스에 있는 것과 동일한 서명을 가진 새로운 인스턴스 메서드를 작성하여 이를 재정의(overriding)할 수 있습니다.
  • 서브클래스에 슈퍼클래스에 있는 것과 동일한 서명을 가진 새로운 정적 메서드를 작성하여 숨길 수 있습니다.
  • 수퍼클래스에 없는 새로운 메서드를 서브클래스에 선언할 수 있습니다.
  • 암시적으로 또는 키워드 super를 사용하여 수퍼클래스의 생성자를 호출하는 서브클래스 생성자를 작성할 수 있습니다.
  • 이 단원의 다음 섹션에서는 이러한 주제에 대해 자세히 설명합니다.

 

슈퍼클래스의 비공개 멤버

서브클래스는 부모 클래스의 비공개 멤버를 상속하지 않습니다. 그러나 슈퍼클래스에 비공개 필드에 액세스하기 위한 공개 또는 보호 메서드가 있는 경우 하위 클래스에서도 사용할 수 있습니다.

중첩 클래스는 필드와 메서드 모두 포함해서 중첩 클래스의 모든 비공개 멤버에 액세스할 수 있습니다. 따라서 서브클래스가 상속한 공용 또는 보호된 중첩 클래스는 수퍼클래스의 모든 비공개 멤버에 간접적으로 액세스할 수 있습니다.

 

객체 형변환

우리는 객체가 인스턴스화된 클래스의 데이터 타입이라는 것을 보았습니다. 예를 들어 다음과 같이 작성하면

public MountainBike myBike = new MountainBike();

그러면 myBikeMountainBike 타입입니다.

MountainBikeBicycleObject에서 파생됩니다. 따라서 MountainBikeBicycle이면서 Object이기도 하며, Bicycle 또는 Object 객체가 호출되는 곳이면 어디에서나 사용할 수 있습니다.

그 반대의 경우도 마찬가지입니다. BicycleMountainBike일 수도 있지만 반드시 그렇지는 않습니다. 마찬가지로, ObjectBicycle 또는 MountainBike일 수 있지만 반드시 그렇지는 않습니다.

형변환은 상속 및 구현에서 허용되는 객체 중 한 타입의 객체를 다른 타입 대신 사용하는 것을 나타냅니다. 예를 들어 다음과 같이 작성하면

Object obj = new MountainBike();

이면 objObject이면서 동시에 MountainBike입니다(objMountainBike가 아닌 다른 객체가 할당될 때까지는). 이를 암시적 형변환 이라고 합니다.

반면에 다음과 같이 작성하면

MountainBike myBike = obj;

를 사용하면 컴파일러에 objMountainBike로 알려지지 않았기 때문에 컴파일 타임 오류가 발생합니다. 하지만 명시적 형변환을 통해 컴파일러에게 MountainBikeobj에 할당하겠다고 약속할 수 있습니다:

MountainBike myBike = (MountainBike)obj;

이 형변환은 컴파일러가 objMountainBike라고 안전하게 가정할 수 있도록 객체에 MountainBike가 할당되었는지 런타임 검사를 삽입합니다. 런타임에 objMountainBike가 아닌 경우 예외가 발생합니다.

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

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

여기서 instanceof 연산자는 objMountainBike를 참조하는지 확인하므로 런타임 예외가 발생하지 않는다는 것을 알고 형변환을 할 수 있습니다.

 

상태, 구현 및 타입의 다중 상속

클래스와 인터페이스의 중요한 차이점 중 하나는 클래스는 필드를 가질 수 있지만 인터페이스는 필드를 가질 수 없다는 점입니다. 또한 클래스를 인스턴스화하여 객체를 만들 수 있지만 인터페이스에서는 할 수 없습니다. 객체란 무엇인가요? 섹션에서 설명한 대로 객체는 필드에 상태를 저장하며, 필드는 클래스에 정의됩니다. Java 프로그래밍 언어에서 둘 이상의 클래스 확장을 허용하지 않는 이유 중 하나는 여러 클래스에서 필드를 상속하는 기능인 상태의 다중 상속 문제를 피하기 위해서입니다. 예를 들어 여러 클래스를 확장하는 새 클래스를 정의할 수 있다고 가정해 보겠습니다. 해당 클래스를 인스턴스화하여 객체를 생성하면 해당 객체는 클래스의 모든 수퍼클래스로부터 필드를 상속받게 됩니다. 서로 다른 슈퍼클래스의 메서드나 생성자가 동일한 필드를 인스턴스화하면 어떻게 될까요? 어떤 메서드나 생성자가 우선할까요? 인터페이스에는 필드가 포함되어 있지 않으므로 상태의 다중 상속으로 인해 발생하는 문제에 대해 걱정할 필요가 없습니다.

구현의 다중 상속 은 여러 클래스에서 메서드 정의를 상속하는 기능입니다. 이러한 타입의 다중 상속에서는 이름 충돌 및 모호성 등의 문제가 발생합니다. 이러한 유형의 다중 상속을 지원하는 프로그래밍 언어의 컴파일러는 이름이 같은 메서드를 포함하는 수퍼클래스를 만나면 어떤 멤버나 메서드를 액세스하거나 호출할지 결정할 수 없는 경우가 있습니다. 또한 프로그래머가 슈퍼클래스에 새 메서드를 추가하여 무의식적으로 이름 충돌을 일으킬 수도 있습니다. 기본 메서드는 구현의 다중 상속이라는 한 가지 형태를 도입합니다. 클래스는 둘 이상의 인터페이스를 구현할 수 있으며, 동일한 이름을 가진 기본 메서드를 포함할 수 있습니다. Java 컴파일러는 특정 클래스가 사용하는 기본 메서드를 결정하기 위한 몇 가지 규칙을 제공합니다.

Java 프로그래밍 언어는 클래스가 둘 이상의 인터페이스를 구현할 수 있는 기능인 타입의 다중 상속을 지원합니다. 객체는 자체 클래스의 타입과 클래스가 구현하는 모든 인터페이스의 타입 등 여러 타입을 가질 수 있습니다. 즉, 변수가 인터페이스의 타입으로 선언되면 그 값은 인터페이스를 구현하는 모든 클래스에서 인스턴스화된 모든 객체를 참조할 수 있습니다. 이에 대해서는 인터페이스를 타입으로 사용하기 섹션에서 설명합니다.

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