상속 관계

  • 기존 클래스의 속성(필드)과 기능(메서드)을 자식 클래스에게 그대로 물려주는 것
  • 상속을 사용하려면 extends 키워드를 쓰면 된다.
  • extends 대상은 하나만 선택할 수 있다.
    단일 상속: 자바는 클래스의 다중 상속을 허용하지 않는다. 즉, 부모는 하나만 선택할 수 있다.

 

용어 정리

  • 부모 클래스 (슈퍼 클래스): 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스
  • 자식 클래스 (서브 클래스): 부모 클래스로부터 필드와 메서드를 상속받는 클래스

 

예시

ElectricCar(전기차)GasCar(가솔린차)가 있다. 전기차는 이동(move()) 기능, 충전(charge()) 기능이 있고

가솔린차는 이동(move()) 기능, 주유(fillUp()) 기능이 있다.

이 둘은 공통적인 기능으로 이동(move()) 기능을 가지고 있다. 이런 경우 상속 관계를 사용하는 것이 효과적이다.

 

전기차와 가솔린차를 포괄하는 개념자동차(Car)이다. 자동차(Car)를 부모 클래스로 만들고, 전기차(ElectricCar)와 가솔린차(GasCar)를 자식 클래스로 만들어보자.

 

 

[부모 클래스: Car]

package extends1.ex2;

public class Car {
    public void move(){
        System.out.println("차를 이동합니다.");
    }
}

Car는 부모 클래스가 된다. 여기에는 자동차의 공통 기능인 move()가 포함되어 있다.

 

[자식 클래스1: ElectricCar]

package extends1.ex2;

public class ElectricCar extends Car {
    //전기차 고유의 기능
    public void charge(){
        System.out.println("충전합니다.");
    }
}

전기차는 extends Car을 사용해서 부모 클래스인 Car를 상속 받는다. 상속 덕분에 ElectricCar에서도 move()를 사용할 수 있다.

 

[자식 클래스2: GasCar]

package extends1.ex2;

public class GasCar extends Car {
    //가솔린차 고유의 기능
    public void fillUp(){
        System.out.println("기름을 주유합니다.");
    }
}

가솔린차도 전기차와 마찬가지로 extends Car을 사용해서 부모 클래스인 Car를 상속 받는다. 상속 덕분에 여기서도 move()를 사용할 수 있다.

 

[메인코드]

package extends1.ex2;

public class CarMain {
    public static void main(String[] args) {
        ElectricCar electricCar = new ElectricCar();
        electricCar.move();
        electricCar.charge();

        GasCar gasCar = new GasCar();
        gasCar.move();
        gasCar.fillUp();
    }
}

실행 결과

 

상속 구조도

전기차와 가솔린차가 Car를 상속 받은 덕분에 electricCar.move(), gasCar.move()를 사용할 수 있다.

 

상속은 부모의 기능을 자식이 물려 받는 것이다. 자식 클래스는 부모 클래스의 기능을 물려 받기 때문에 접근할 수 있지만, 부모 클래스는 자식 클래스에 접근할 수 없다.

부모 코드를 보면 자식에 대한 정보가 하나도 없다. 반면에 자식 코드는 extends Car를 통해서 부모를 알고 있다.

 


상속과 메모리 구조

ElectricCar electricCar = new ElectricCar();

new ElectricCar()를 호출하면 ElectricCar뿐만 아니라 상속 관계에 있는 Car까지 함께 포함해서 인스턴스를 생성한다.

참조값은 x001 하나이지만 실제로 그 안에서는 Car, ElectricCar라는 두 가지 클래스 정보가 공존하는 것이다.

상속을 사용하면 단순하게 부모의 필드와 메서드만 물려 받는 게 아니라, 부모 클래스도 함께 포함해서 생성되는 것이다.

외부에서 볼 때는 하나의 인스턴스를 생성하는 것 같지만 내부에서는 부모와 자식이 모두 생성되고 공간도 구분된다.

 

electricCar.charge() 호출

electricCar.charge()를 호출하면 참조값을 확인해서 x001.charge()를 호출한다. 따라서 x001을 찾아서 charge()를 호출하면 되는 것이다. 그런데 상속 관계의 경우에는 내부에 부모와 자식이 모두 존재한다. 이때 부모인 Car를 통해서 charge()를 찾을지, 아니면 ElectricCar를 통해서 charge()를 찾을지 선택해야 한다.

이때는 호출하는 변수의 타입(클래스)을 기준으로 선택한다. electricCar 변수의 타입이 ElectricCar이므로 인스턴스 내부에 같은 타입인 ElectricCar를 통해서 charge()를 호출한다.

 

electricCar.move() 호출

electricCar.move()를 호출하면 먼저 x001 참조로 이동한다. 내부에는 Car, ElectricCar 두 가지 타입이 있다. 이때 호출하는 변수인 electricCar의 타입이 ElectricCar이므로 이 타입을 선택한다.

그런데 ElectricCar에는 move() 메서드가 없다. 상속 관계에서는 자식 타입에 해당 기능이 없으면 부모 타입으로 올라가서 찾는다. 이 경우 ElectricCar의 부모인 Car로 올라가서 move()를 찾는다. 부모인 Car에 move()가 있으므로 부모에 있는 move() 메서드를 호출한다.

 

1. 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성된다.
2. 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 한다. 이때 호출자의 타입을 통해 대상 타입을 찾는다.
3. 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾지 못하면 컴파일 오류가 발생한다.

 

 


상속과 기능 추가

상속 관계의 장점을 알아보기 위해 상속 관계에 다음 기능을 추가해보자.

  • 모든 차량에 문열기(openDoor()) 기능을 추가해야 한다.
  • 새로운 수소차(HydrogenCar)를 추가해야 한다.
    - 수소차는 fillHydrogen() 기능을 통해 수소를 충전할 수 있다.

 

[부모 클래스: Car]

package extends1.ex3;

public class Car {
    public void move(){
        System.out.println("차를 이동합니다.");
    }
    //추가
    public void openDoor(){
        System.out.println("문을 엽니다.");
    }
}

부모 클래스인 Car에 문 열기 기능을 추가하면 Car의 자식들은 해당 기능을 모두 물려받게 된다.

 

[자식 클래스들]

package extends1.ex3;

public class ElectricCar extends Car {
    public void charge(){
        System.out.println("충전합니다.");
    }
}
package extends1.ex3;

public class GasCar extends Car {
    public void fillUp(){
        System.out.println("기름을 주유합니다.");
    }
}
package extends1.ex3;

public class HydrogenCar extends Car {
    public void fillHydrogen(){
        System.out.println("수소를 충전합니다.");
    }
}

수소차를 추가했다. Car를 상속받은 덕분에 move(), openDoor()와 같은 기능을 바로 사용할 수 있다.

수소차는 전용 기능인 수소 충전(fillHydrogen()) 기능을 제공한다.

 

[메인 코드]

package extends1.ex3;

public class CarMain {
    public static void main(String[] args) {
        ElectricCar electricCar = new ElectricCar();
        electricCar.move();
        electricCar.charge();
        electricCar.openDoor();

        GasCar gasCar = new GasCar();
        gasCar.move();
        gasCar.fillUp();
        gasCar.openDoor();

        HydrogenCar hydrogenCar = new HydrogenCar();
        hydrogenCar.move();
        hydrogenCar.fillHydrogen();
        hydrogenCar.openDoor();
    }
}

실행 결과

 

기능 추가와 클래스 확장

상속 관계 덕분에 중복은 줄어들고, 새로운 수소차를 편리하게 확장(extend)한 것을 알 수 있다.

 


상속과 메서드 오버라이딩

부모 타입의 기능을 자식에서는 다르게 재정의하고 싶을 수 있다. 예를 들어 자동차의 경우 Car.move() 라는 기능이 있다. 이 기능을 사용하면 단순히 "차를 이동합니다."라고 출력된다. 전기차의 경우 보통 더 빠르기 때문에 전치가가 move()를 호출한 경우에는 "전기차를 빠르게 이동합니다."라고 출력을 변경하고 싶다.

 

이렇게 부모에게서 상속받은 기능을 자식이 재정의하는 것메서드 오버라이딩(Overriding)이라 한다.

 

[부모 클래스: Car]

package extends1.overriding;

public class Car {
    public void move(){
        System.out.println("차를 이동합니다.");
    }
    
    public void openDoor(){
        System.out.println("문을 엽니다.");
    }
}

 

[자식 클래스1: GasCar]

package extends1.overriding;

public class GasCar extends Car {
    public void fillUp(){
        System.out.println("기름을 주유합니다.");
    }
}

 

[자식 클래스2: ElectricCar]

package extends1.overriding;

public class ElectricCar extends Car {
    @Override
    public void move(){
        System.out.println("전기차를 빠르게 이동합니다.");
    }
    public void charge(){
        System.out.println("충전합니다.");
    }
}

ElectricCar는 부모인 Car의 move() 기능을 그대로 사용하고 싶지 않다. 메서드 이름은 같지만 새로운 기능을 사용하고 싶다. 그래서 ElectricCar의 move() 메서드를 새로 만들었다.

이렇게 부모의 기능을 자식이 새로 정의하는 것메서드 오버라이딩이라 한다.

이제 ElectricCar의 move()를 호출하면 Car의 move()가 아니라 ElectricCar의 move()가 호출된다.

 

@Override
  • 애노테이션(주석과 비슷한 개념)
  • 오버라이딩한 메서드 위에 이 애노테이션을 붙이면 메서드가 오버라이딩 조건에 만족하지 않는 경우에 컴파일 에러를 발생시킨다.
  • 필수는 아니지만 코드의 명확성을 위해 붙여주는 것이 좋다.

[메인 코드]

package extends1.overriding;

public class CarMain {
    public static void main(String[] args) {
        ElectricCar electricCar = new ElectricCar();
        electricCar.move(); //오버라이딩한 메서드 출력

        GasCar gasCar = new GasCar();
        gasCar.move(); //부모(Car)의 메서드 출력
    }
}

실행 결과

 

 

오버라이딩과 클래스

Car의 move() 메서드를 ElectricCar에서 오버라이딩했다.

 

오버라이딩과 메모리 구조

  1. electricCar.move()를 호출한다.
  2. 호출한 electricCar의 타입은 ElectricCar이다. 따라서 인스턴스 내부의 ElectricCar 타입에서 시작한다.
  3. ElectricCar 타입에 move() 메서드가 있다. 해당 메서드를 실행한다. 이때 실행할 메서드를 이미 찾았으므로 부모 타입을 찾지 않는다.

 

※ 오버로딩(Overloading) vs 오버라이딩(Overriding)

  • 메서드 오버로딩: 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러 개 정의하는 것
    "과적"(과하게 물건을 담았다) → 같은 이름의 메서드를 여러 개 정의
  • 메서드 오버라이딩: 상속 관계에서 사용. 부모의 기능을 자식이 다시 정의하는 것
    "Overriding"(무언가를 넘어서 타는 것) → 자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버린다.
    "재정의" → 기존 기능을 다시 정의한다

 

메서드 오버라이딩 조건

  1. 메서드 이름이 같아야 한다.
  2. 매개변수(파라미터) 타입, 순서, 개수가 같아야 한다.
  3. 반환 타입이 같아야 한다.
  4. 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안된다.
    ex) 상위 클래스의 메서드: protected → 하위 클래스: public 또는 protected로 오버라이드
  5. 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없다.
    더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있다.
  6. static, final, private 키워드가 붙은 메서드는 오버라이딩될 수 없다.
    - static: 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없다. 그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 된다.
    - final: final 메서드는 재정의를 금지한다.
    - private: private 메서드는 해당 클래스에서만 접근이 가능하기 때문에 하위 클래스에서 보이지 않는다. 따라서 오버라이딩할 수 없다.
  7. 생성자는 오버라이딩할 수 없다.

 


상속과 접근 제어

부모(Parent)와 자식(Child)의 패키지를 따로 분리해보자.

접근 제어자 종류

  • private: 모든 외부 호출을 막는다.
  • default(package-private): 같은 패키지안에서 호출은 허용한다.
  • protected: 같은 패키지안에서 호출은 허용 + 패키지가 다르더라도 상속 관계의 호출은 허용한다.
  • public: 모든 외부 호출을 허용한다.

 

[부모 클래스: Parent]

package extends1.access.parent;

public class Parent {
    public int publicValue;
    protected int protectedValue;
    int defaultValue; //default
    private int privateValue;
    public void publicMethod(){
        System.out.println("Parent.publicMethod");
    }
    protected void protectedMethod(){
        System.out.println("Parent.protectedMethod");
    }
    void defaultMethod(){
        System.out.println("Parent.defaultMethod");
    } //default
    private void privateMethod(){
        System.out.println("Parent.privateMethod");
    }

    public void printParent(){
        System.out.println("==Parent 메서드 안==");
        System.out.println("publicValue = " + publicValue);
        System.out.println("protectedValue = " + protectedValue);
        System.out.println("defaultValue = " + defaultValue);
        System.out.println("privateValue = " + privateValue);

        //부모 메서드 안에서 모두 접근 가능
        defaultMethod();
        privateMethod();
    }
}

부모 클래스인 Parent에는 public, protected, default, private 과 같은 모든 접근 제어자가 필드와 메서드에 모두 존재한다.

 

[자식 클래스: Child]

package extends1.access.child;

import extends1.access.parent.Parent;

public class Child extends Parent {
    public void call(){
        publicValue = 1;
        protectedValue = 1; //상속 관계(O) or 같은 패키지
        //defaultValue = 1; //다른 패키지 접근 불가, 컴파일 오류
        //privateValue = 1; //접근 불가, 컴파일 오류

        publicMethod();
        protectedMethod(); //상속 관계(O) or 같은 패키지
        //defaultMethod(); //다른 패키지 접근 불가, 컴파일 오류
        //privateMethod(); //접근 불가, 컴파일 오류

        printParent(); //public
    }
}

 

패키지가 다른 자식 클래스 Child에서 부모 클래스인 Parent에 얼마나 접근할 수 있는지 확인해보자.

  • publicValue = 1 : 부모의 public 필드에 접근한다. public이므로 접근할 수 있다.
  • protectedValue = 1 : 부모의 protected 필드에 접근한다. 자식과 부모는 다른 패키지이지만, 상속 관계이므로 접근할 수 있다.
  • defaultValue = 1 : 부모의 default 필드에 접근한다. 자식과 부모가 다른 패키지이므로 접근할 수 없다.
  • privateValue = 1 : 부모의 private 필드에 접근한다. private은 모든 외부 접근을 막으므로 자식이라도 호출할 수 없다.

실행 결과

코드를 실행해보면 Child.call() → Parent.printParent() 순서로 호출한다. Child는 부모의 public , protected 필드나 메서드만 접근할 수 있다. 반면에 Parent.printParent() 의 경우 Parent 안에 있는 메서드이기 때문에 Parent 자신의 모든 필드와 메서드에 얼마든지 접근할 수 있다.

 

접근 제어와 메모리 구조

본인 타입에 없으면 부모 타입에서 기능을 찾는데, 이때 접근 제어자가 영향을 준다. 왜냐하면 객체 내부에서는 자식과 부모가 구분되어 있기 때문이다. 결국 자식 타입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출한 것과 같다.

 


super - 부모 참조

부모와 자식의 필드명이 같거나 메서드가 오버라이딩되어 있으면, 자식에서 부모의 필드나 메서드를 호출할 수 없다.

이때 super 키워드를 사용하면 부모를 참조할 수 있다.

super: 부모 클래스에 대한 참조

 

다음 예를 보자. 부모의 필드명과 자식의 필드명이 둘다 value로 똑같다. 메서드도 hello()로 자식에서 오버라이딩 되어 있다. 이때 자식 클래스에서 부모 클래스의 value와 hello()를 호출하고 싶다면 super 키워드를 사용하면 된다.

 

[부모 클래스: Parent]

package extends1.super1;

public class Parent {

    public String value = "parent";

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

 

[자식 클래스: Child]

package extends1.super1;

public class Child extends Parent{

    public String value = "child";
    //부모의 hello()를 오버라이드한 메서드
    @Override
    public void hello(){
        System.out.println("Child.hello");
    }
    //자식 고유의 메서드
    public void call(){
        System.out.println("this value = " + this.value); //this 생략 가능
        System.out.println("super value = " + super.value); //부모의 value 호출

        this.hello(); //this 생략 가능
        super.hello(); //부모의 hello() 호출
    }
}

자식 고유의 call() 메서드를 보자.

  • this는 자기 자신의 참조를 뜻한다. this는 생략이 가능하다.
  • super는 부모 클래스에 대한 참조를 뜻한다.
  • 필드 이름과 메서드 이름이 같지만 super를 사용해서 부모 클래스에 있는 기능을 사용할 수 있다.

[메인 코드]

package extends1.super1;

public class Super1Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.call();
    }
}

실행 결과

실행 결과를 보면 super를 사용한 경우 부모 클래스의 기능을 사용한 것을 확인할 수 있다.

 

super 메모리 그림

 


super - 생성자

상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다.
- 자식 클래스의 생성자는 부모 클래스의 생성자를 먼저 호출한다.
- 명시적으로 호출하지 않을 경우(특정 생성자를 지정하지 않을 경우) 부모 클래스의 기본 생성자가 자동으로 호출된다.
→ 자식클래스 생성자에서는 묵시적으로 부모의 기본생성자를 호출하는 super()를 호출한다.
- 만약 자식 클래스 생성자에서 부모 클래스의 특정 생성자를 호출하지 않으면서 부모 클래스에 기본 생성자가 존재하지 않는 경우, 자식 클래스 객체 생성시 에러가 발생한다.

 

상속 관계에서 부모의 생성자를 호출할 때는 super(...)를 사용하면 된다.

 

예제를 통해 알아보자.

 

[최상위 부모 클래스: ClassA]

package extends1.super2;

public class ClassA {
    //기본 생성자
    public ClassA(){
        System.out.println("ClassA 생성자");
    }
}

 

[ClassA의 자식 클래스: ClassB]

package extends1.super2;

public class ClassB extends ClassA{
    //인자 생성자1
    public ClassB(int a){
        super(); //기본 생성자 생략 가능
        System.out.println("ClassB 생성자 a=" + a);
    }
    //인자 생성자2
    public ClassB(int a, int b){
        super(); //기본 생성자 생략 가능
        System.out.println("ClassB 생성자 a=" + a + " b=" + b);
    }
}
  • ClassB는 ClassA를 상속받았다. 상속을 받으면 생성자의 첫줄에 super(...)을 사용해서 부모 클래스의 생성자를 호출해야 한다.
  • 부모 클래스의 생성자가 기본 생성자(파라미터가 없는 생성자)인 경우에는 super()를 생략할 수 있다.
    - 상속 관계에서 첫줄에 super(...)를 생략하면 자바는 부모의 기본 생성자를 호출하는 super()를 자동으로 만들어준다.
    - 기본 생성자를 많이 사용하기 때문에 편의상 이런 기능을 제공한다.

 

[ClassB의 자식 클래스: ClassC]

package extends1.super2;

public class ClassC extends ClassB{
    //기본 생성자
    public ClassC(){
        super(10, 20); //ClassB의 인자 생성자2 선택
        System.out.println("ClassC 생성자");
    }
}
  • ClassC는 ClassB를 상속받았다. ClassB에는 두 생성자가 있다.
    인자 생성자1: ClassB(int a)
    인자 생성자2: ClassB(int a, int b))
  • 생성자는 하나만 호출할 수 있다. 두 생성자 중에 하나를 선택하면 된다.
    - super(10, 20)을 통해 부모 클래스의 ClassB(int a, int b) 생성자를 선택했다.
  • ClassC의 부모인 ClassB에는 기본 생성자가 없다. 따라서 부모의 기본 생성자를 호출하는 super()를 사용하거나 생략할 수 없다.

 

[메인 코드]

package extends1.super2;

public class Super2Main {
    public static void main(String[] args) {
        ClassC classC = new ClassC();
    }
}

실행 결과

실행해보면 ClassA → ClassB → ClassC 순서로 실행된다. 생성자의 실행 순서가 결과적으로 최상위 부모부터 실행돼서 하나씩 아래로 내려오는 것이다. 따라서 초기화는 최상위 부모부터 이루어진다. 왜냐하면 자식 생성자의 첫 줄에서 부모의 생성자를 호출해야 하기 때문이다.

 

 

정리

  • 상속 관계의 생성자 호출은 결과적으로 부모에서 자식 순서로 실행된다. 따라서 부모의 데이터를 먼저 초기화하고 그 다음에 자식의 데이터를 초기화한다.
  • 상속 관계에서 자식 클래스의 생성자 첫줄에 반드시 super(...)를 호출해야 한다. 단, 기본 생성자(super())인 경우 생략할 수 있다.

this(...)와 함께 사용

코드의 첫줄에 this(...)를 사용하더라도 반드시 한번은 super(...)를 호출해야 한다.

 

[ClassB 코드 변경]

package extends1.super2;

public class ClassB extends ClassA{
    //인자 생성자1
    public ClassB(int a){
        //super(); 생략되어 있음
        this(a, 0); //본인의 인자 생성자2를 호출
        System.out.println("ClassB 생성자 a=" + a);
    }
    //인자 생성자2
    public ClassB(int a, int b){
        super(); //기본 생성자 생략 가능
        System.out.println("ClassB 생성자 a=" + a + " b=" + b);
    }
}

 

[메인 코드 변경]

package extends1.super2;

public class Super2Main {
    public static void main(String[] args) {
        ClassB classB = new ClassB(100);
    }
}

실행 결과

생성자 첫줄에 this(...)를 사용할 수 있다. 하지만 super(...)은 자식의 생성자 안에서 언젠가는 반드시 호출해야 한다.

 

위의 코드에서는 main코드 → ClassB 인자 생성자1 → this(a, 0)을 만나서 → ClassB 인자 생성자2 → super()를 만나 → ClassA 기본 생성자로 접근

 

super()를 결국에는 만나게 된다.

 


부모 클래스에서 기본 생성자가 없는 경우

부모 클래스에서 기본 생성자가 없으면 상속이 안 되는 경우가 발생한다.

 

예제를 통해 살펴보자.

 

[부모 클래스: M1_Person]

package day08;

public class M1_Person { //extends Object
	int no;
	String name;
	String tel;
	
	//인자생성자
	public M1_Person(int no, String name, String tel) {
		this.no = no;
		this.name = name;
		this.tel = tel;
	}
	
	//toString():Object클래스에서는 주소값 반환
	
	@Override
	public String toString() {
		String str = "---Person---\n";
		str += "No: " + no + "\nName: " + name + "\nTel: " + tel;
		return str;
	}
}

학사 관리 프로그램을 만들어보자. 학생보다 상위의 사람 클래스를 만들어서 id number(no), 이름, 전화번호 멤버필드를 선언했고, 모든 멤버변수의 값을 초기화해주는 인자 생성자도 생성하였다. 추가로 상위 클래스인 Object의 toString() 함수를 오버라이드하는 메서드도 정의했다.

 

[자식 클래스: M1_Student]

package day08;

public class M1_Student extends M1_Person{
	String className;
	
    //인자 생성자
	public M1_Student(int no, String name, String tel, String className) {
		//super(); 생략되어 있음 → 부모 클래스에 기본 생성자가 없으므로 실행X
  		this.no = no;
		this.name = name;
		this.tel = tel;
		this.className = className;
	}
	
	//toString() 오버라이드
	@Override
	public String toString() {
		String str = super.toString(); //no, name, tel을 반환함
		str = str.replace("Person", "Student"); //Person을 Student로 교체
		str += "\nClass: " + className;
		return str;
	}
}

M1_Person 클래스를 상속받는 M1_Student 클래스를 정의하였다.

M1_Person 클래스를 상속받았기 때문에 이에 대한 멤버변수와 메서드를 M1_Student가 상속받게 된다. 즉, M1_Student의 멤버변수는 no, name, tel에다가 자신의 고유 변수인 className까지 총 4개이다.

부모 클래스와 마찬가지로 M1_Student 클래스에 인자 생성자를 생성하였다.

이 코드는 겉보기에는 문제가 없어보인다. 그러나 컴파일 에러가 발생한다.

 

컴파일 에러

암시적 슈퍼 생성자 M1_Person()이 정의되지 않았습니다

 

"자식클래스의 생성자는 부모클래스의 생성자를 먼저 호출한다." , "명시적으로 호출하지 않을 경우 부모 클래스의  기본 생성자가 자동적으로 호출된다." 라는 생성자의 정의에 의해 발생한 에러이다.

 

자식 클래스의 생성자는 부모 클래스의 생성자를 호출해야하는데, 현재 M1_Student 생성자를 보면 알 수 있듯이 부모 클래스의 생성자를 호출하는 메서드인 super(...)가 없다. 즉, 시적으로 호출하지 않았기 때문에 자바가 M1_Student 생성자 내에서 부모 클래스의 기본 생성자를 자동적으로 호출하려했으나 현재 부모 클래스에는 기본 생성자가 없기 때문에 위 에러가 발생한 것이다.

 

그렇다면 위 문제를 어떻게 해결할까?

 


해결방안

  1. 부모 클래스에서 기본 생성자 만들기
  2. 자식 클래스에서 명시적으로 super(인자) 생성자를 호출하기

첫번째 방법은 에러 제거만을 목적으로 불필요한 코드를 추가하기 때문에 추천하지 않는 방법이다. 자식 클래스 생성자 내에서 부모 클래스의 생성자를 명시적으로 호출하는 두 번째 방법을 사용하자.

 

[자식 클래스: M1_Student]

package day08;

public class M1_Student extends M1_Person{
	String className;
	
    //인자 생성자
	public M1_Student(int no, String name, String tel, String className) {
		super(no, name, tel); //명시적 호출
		this.className = className;
	}
	
	//toString() 오버라이드
	@Override
	public String toString() {
		String str = super.toString(); //no, name, tel을 반환함
		str = str.replace("Person", "Student"); //Person을 Student로 교체
		str += "\nClass: " + className;
		return str;
	}
}

M1_Student 생성자 안에서 super(...) 메서드를 통해 부모 클래스의 생성자를 명시적으로 호출해주고, 부모 클래스에 없는 className은 this를 통해 초기화하였다.

 


오버라이딩 - 기존 부모 코드를 그대로 쓰되 내용을 추가하고 싶은 경우

[부모 클래스: Human]

package day07.inheritance;

public class Human {
	String name;
	int height;
	
	public void showInfo() {
		System.out.println("이름: " + name);
		System.out.println("키: " + height);
	}
}

 

[자식 클래스: Aquaman]

package day07.inheritance;

public class Aquaman extends Human {
	
	double speed;
	
	
	public Aquaman(String name, int height, double speed) {
		this.name = name;
		this.height = height;
		this.speed = speed;
	}
	
	@Override
	public void showInfo() {
		super.showInfo(); //기존 코드: 이름, 키 출력
		System.out.println("스피드: " + speed); //추가 코드: 스피드 출력
	}
}

부모 클래스인 Human에 있는 showInfo() 메서드를 그대로 사용하되, 내용을 추가해서 쓰고 싶다면

메서드를 오버로드하여 그 내부에 super.메서드명()을 통해 오버로딩한 메서드를 가져와 쓰면서

새롭게 사용하고 싶은 내용을 추가해주면 된다.

+ Recent posts