Java/문법

[JAVA 중급 스터디 - 7~9회차] 상속

soowitty 2024. 3. 20. 14:17

상속

상속이란?

    • 기존 클래스의 속성을 물려 받아, 새로운 클래스를 생성하는 것
    • extends : java에서 상속을 위해 사용하는 키워드
      • 하나의 대상만 선택 가능. (단일상속만 지원)
        • java는 다중상속을 지원하지 않음!
        • 해결 방안 : implements 키워드를 이용한 인터페이스의 다중구현 으로 해결
단일 상속 vs. 다중 상속
1) 단일 상속

2) 다중 상속

 

자바가 다중상속을 지원하지 않는 이유

-  다이아몬드 문제 발생 가능성 때문
1) GrandFather 클래스에 memberMethod() 라는 이름의 메소드가 있다고 가정하자.
2) FatherAFatherB가 각각 memberMethod() 오버라이딩하여 구현했다.
3) FatherAFatherB를 모두 상속받은 Son 클래스는 어떤 부모의 memberMethod()를 사용해야할지 애매함 -> 충돌 발생!

 

상속과 메모리 구조

1. ElectricCar electricCar = new ElectricCar();

  • new ElectricCar() 을 호출하면 -> ElectricCar 뿐만 아니라 상속 관계에 있는 Car이 함께 포함되어 인스턴스가 생성됨
  • 참조값은 x001 하나이지만, 실제로 그 안에는 CarElectricCar 두개의 클래스 정보가 공존!
    => 상속은 단순히 부모의 필드와 메서드만 물려 받는 것이 아님. 부모 클래스 자체를 함께 포함하여 객체를 생성!
💡 상속 관계의 객체를 생성하면, 인스턴스 내부에 부모와 자식이 모두 생성된다.

2. electricCar.charge()

  • electricCar.charge() 를 호출하면 -> 참조값을 확인하여 x001.charge()를 호출함.
  • 이때, 상속 관계의 경우 x001 내부에 부모와 자식이 모두 존재함.
  • 부모인 Car을 통해서 charge()를 찾을지, ElectricCar을 통해서 charge()를 찾을지 선택해야 함.
    • 호출하는 변수의 타입(클래스)를 기준으로 선택한다.
    • electricCar변수의 타입이 ElectricCar 클래스이므로, ElectricCar을 통해서 charge()를 호출한다.
💡 상속 관계의 객체를 호출할 때, 호출자의 타입과 동일한 대상 타입을 찾는다.

3. electricCar.move()

  • elctricCar.move() 를 호출하면 -> x001 참조로 이동
  • Car, ElectricCar 두가지 타입이 있는데, 호출하는 변수인 electricCar의 타입인 ElectricCar 선택
  • 그런데, ElectricCar에는 move() 메소드가 존재하지 않음!
  • 상속 관계에서, 자식 타입에 해당 기능이 없으면 부모 타입으로 올라가서 찾는다.
    • ElectricCar의 부모인 Car로 올라가서 move()를 찾는다.
    • 부모인 Carmove()가 존재하므로 부모에 있는 move() 메서드를 호출한다.
💡 현재 타입에서 기능을 찾지 못하면, 부모 타입으로 이동하여 찾아서 실행한다.
   찾지 못하면 최상위 부모까지 이동하게되고, 그곳에도 없다면 컴파일 오류가 발생한다.

 

상속과 기능 추가

상속 관계에 메소드와 클래스를 추가해야 한다.

  • 모든 차량에 문열기(openDoor()) 기능을 추가했다.
  • 수소차(HydrogenCar) 추가되었다.
    • 수소차는 fillHydrogen() 기능을 통해 수소를 충전할 수 있다.

1. Car 클래스에 openDoor() 메서드를 추가한다.

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

    //추가
    public void openDoor() {
        System.out.println("문을 엽니다.");
    }
}
  • 모든 차량에 문열기 기능을 추가하고자 할 때, 상위 부모인 CaropenDoor() 기능을 추가하면 된다.
    • 모든 차량이 Car 클래스를 상속하고 있기 때문에 가능하다.

2. HydrogenCar 클래스를 추가하고, fillHydrogen() 메서드를 추가한다.

public class HydrogenCar extends Car {
    public void fillHydrogen() {
        System.out.println("수소를 충전합니다.");
    }
}
  • Car 클래스를 상속받았기 때문에 move(), openDoor() 기능을 바로 사용할 수 있다.

3. ElectricCar 클래스는 손대지 않아도 된다.

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

Main 클래스

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(); //문을 엽니다.
    }
}
💡 상속관계 덕분에 중복이 줄어들고, 새로운 클래스를 편리하게 확장할 수 있다.

메서드 오버라이딩과 메모리 구조

메서드 오버라이딩

  • 부모에게서 상속받은 기능을 자식이 재정의하는 것

메모리 구조

  • Carmove() 메서드를 ElectricCar에서 오버라이딩했다.
  1. elctricCar.move()를 호출한다.
  2. 호출한 electricCar의 타입은 ElectricCar이다. 따라서 인스턴스 내부의 ElectricCar 타입에서 시작한다.
  3. ElectricCar 타입에 move() 메서드가 존재하므로 해당 메서드를 실행한다. 실행할 메서드를 찾았으므로, 부모 타입을 탐색하지 않는다.

오버라이딩 vs. 오버로딩

메서드 오버라이딩(Overriding)

  • 부모에게서 상속 받은 기능을 자식이 재정의 하는 것

오버라이딩 성립 조건

  • 메서드 이름 : 메서드 이름이 일치해야 함
  • 메서드 매개변수(파라미터) : 매개변수의 개수, 순서, 데이터 타입이 일치해야 함
  • 반환 타입 : 메서드의 return 타입이 일치해야 함 (반환 타입이 하위 클래스 타입일 수 있음)
  • 접근 제어자 : 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이면 안 됨.
    • ex) 상위 클래스 메서드 : protected 이면, 하위클래스에서 public, protected로만 오버라이드 가능! (private, default로는 불가능)
  • static, final, private 키워드가 붙은 메서드는 오버라이딩 될 수 없음!
    • static은 클래스 레벨에서 작동한다. 따라서, 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없음.
      • 그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 됨
    • final 메서드는 재정의를 금지함
    • private 메서드는 해당 클래스에서만 접근 가능하기 때문에 오버라이딩 불가능
  • 생성자는 오버라이딩 할 수 었다.

메서드 오버로딩(Overloading)

  • 메서드 이름이 같고, 매개변수(파라미터)가 다른 메서드를 여러개 정의하는 것 (같은 클래스 내부에서 메소드를 확장하기 위한 개념)

오버로딩 성립 조건

  • 메소드 이름이 일치해야 함
    • 메소드 매개변수의 개수 또는 타입이 서로 달라야 함
      • 개수가 같다면 타입을 다르게, 타입이 같다면 개수를 다르게 해야 함(둘 중에 하나는 반드시 다르게!)
    • 메소드의 return 타입이 서로 다라야 함

 

super - 부모 참조

  • 부모와 자식의 필드명이 같거나 메서드가 오버라이딩 되어 있으면, 자식에서 부모의 필드나 메서드를 호출할 수 없다. 이때, super 키워드를 사용하면 부모를 참조할 수 있다. super는 이름 그대로 부모 클래스에 대한 참조를 뜻한다.
  • this : 자기 자신 클래스에 대한 참조
  • super : 부모 클래스에 대한 참조

코드 예시

public class Parent {
    public String value = "parent";

    public void hello() {
        System.out.println("Parent.hello");
    }
}
public class Child extends Parent {
    public String value = "child";

    @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);
        this.hello(); //this 생략 가능
        super.hello();
    }
}
package extends1.super1;
    public class Super1Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.call();
    }
}
// 출력
this value = child
super value = parent
Child.hello
Parent.hello

메모리 구조

super - 생성자

    • 상속 관계의 인스턴스를 생성하면 결국 메모리 내부에는 자식과 부모 클래스가 모두 만들어짐.
      • Child를 생성하면 부모인 Parent까지 함께 만들어짐
  • 각각의 생성자도 모두 호출되어야 함

 

상속 관계를 사용하면, 자식 클래스의 생성자에서 super()를 이용하여 부모 클래스의 생성자를 반드시 호출해야 한다.
package extends1.super2;
public class ClassA {
    public ClassA() {
        System.out.println("ClassA 생성자");
    }
}
// Class A 기본 생성자 존재
package extends1.super2;
public class ClassB extends ClassA {
    public ClassB(int a) {
        super(); // 생략 가능 (기본 생성자)
        System.out.println("ClassB 생성자 a="+a);
    }
    public ClassB(int a, int b) {
        super(); // 생략 가능 (기본 생성자)
        System.out.println("ClassB 생성자 a="+a + " b=" + b);
    }
}
// Class B 기본 생성자 없음
package extends1.super2;
public class ClassC extends ClassB {
    public ClassC() {
        super(10, 20); // 생략 불가능
        System.out.println("ClassC 생성자");
    }
}
package extends1.super2;
public class Super2Main {
    public static void main(String[] args) {
        ClassC classC = new ClassC();
    }
}
// 출력
ClassA 생성자
ClassB 생성자 a=10 b=20
ClassC 생성자

메모리 구조