공부/Java

자바 상속이란

Stair 2024. 9. 3. 10:22
반응형

다음과 같은 전기, 가스 자동차 클래스와 메인 클래스가 있다고 가정하자

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

    public void charge(){
        System.out.println("충전합니다.");
    }
}
public class GasCar {
    public void move(){
        System.out.println("차를 이동합니다.");
    }
    public void fillUp(){
        System.out.println("기름을 주유합니다.");
    }
}
public static void main(String[] args) {
    ElectricCar electricCar = new ElectricCar();
    electricCar.move();
    electricCar.charge();

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

 

전기차(ElectricCar)와 가솔린차(GasCar)를 만들었다. 전기차는 이동, 충전 기능이 있고, 가솔린차는 이동, 주유 기능이 있다.

 

전기차와 가솔린차는 자동차(Car)의 좀 더 구체적인 개념이다. 반대로 자동차(Car)는 전기차와 가솔린차를 포함하는 추상적인 개념이다. 그래서인지 잘 보면 둘의 공통 기능이 보인다. 바로 이동(move)이다.

이런 경우에 상속을 사용하는 것이 효과적이다.

 

 

상속 관계

상속은 객체 지향 프로그램의 핵심 요소 중 하나로, 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용하게 해준다.

이름 그대로 기존 클래스의 속성과 기능을 그대로 물려받는 것이다.

상속을 사용하려면 extends 키워드를 사용하면 된다. extends대상은 하나만 선택할 수 있다.

 

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

-자식 클래스(서브 클래스) : 부모클래스로부터 필드와 메서드를 상속받는 클래스

 

위 코드에서 공통 메서드인 move()를 가지고 있는 car를 만들어보자

public class Car {

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

    }

}
public class ElectricCar extends Car{

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

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

 

전기자동차와 가스카 모두 car를 상속 받았기때문에 move 메서드를 사용할 수 있다.

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

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

 

 

자식은 부모가 누구인지 알 수 있다. 하지만 부모는 어떤 클래스가 상속을 받는지 모른다.

 

상속은 부모의 기능을 자식이 물려 받는 것이다. 따라서 자식이 부모의 기능을 물려 받아서 사용할 수 있다.

하지만 "부모 클래스는 자식 클래스에 접근할 수 없다."

 

참고로 자바는 다중 상속을 지원하지 않는다. 따라서 부모는 하나만 선택할 수 있다. 하지만 부모의 부모의 부모 이런 경우는 괜찮다.

다중 상속을 사용하게 되면 각 부모에 동일한 메서드가 존재할 시 어떤 메서드를 호출해야 하는지 문제점이 발생한다(다이아몬드 문제) 그리고 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해질 수 있다.

 

 

***상속과 메모리 구조는 다음과 같다***

매우 중요하니 제대로 이해하여야 한다.

ElectricCar electricCar = new ElectricCar();

 

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

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

*상속이라고 해서 단순하게 부모ㅓ의 필드와 메서드만 물려받는 것이 아니다. 상속 관계를 사용하면 부모 클래스도 함께 포함해서 생성된다.

 

electricCar.charge()호출

electricCar.charge()를 호출하면 x001을 통해 x001.charge()를 호출한다. 그런데 내부 부모와 자식이 모두 존재하기에, 이때 부모인 Car를 통해 찾을지 ElectricCar를 통해 찾을지 선택해야한다.

이때는 호출하는 변수의 타입(클래스)을 기준으로 선택한다.(현재 호출을 electricCar.charge()로 호출했기에 electricCar에서 우선적으로 찾는다.)

 

 

electricCar.move()호출

electricCar.move()를 호출하면 먼저 x001로 참조 이동한다. 내부에는 Car, ElectricCar 두가지 타입이 있다. 이때 호출하는 변수인 electricCar의 타입이 ElectricCar이므로 이 타입을 우선 선택한다. 하지만 이 타입에는 move()메서드가 없기 때문에 부모타입으로 올라가서 move()를 찾아 호출하게 된다.

 

만약 부모에서도 해당 기능을 찾지 못하면 더 상위 부모에서 필요한 기능을 찾는다. 찾아도 계속 없으면 컴파일 에러가 나타난다.

 

정리

1. *****상속 관계의 객체를 생성하면, 그 내부에는 부모와 자식이 모두 생성된다.

2. 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 한다. 이때 호출자의 타입을 통해 대상 타입을 찾는다.

3. 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾지 못하면 컴파일 에러가 발생한다.

 

 

이제 위 상속을 활용하여 자동차에 다음 기능들을 활용하여 보자.

- 모든 차량에 문열기 기능을 추가해보자.

- 수소차량을 추가해보자(fillHydrogen()) 기능을 통해 수소를 충전할 수 있다.

public class Car {

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

    }

    //추가
    public void openDoor(){
        System.out.println("문을 엽니다.");
    }

}
public class HydrogenCar extends Car{

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

 

코드는 다음과 같다.

 

 

이제 상속과 메서드 오버라이딩에 대해 알아보자.

부모 타입의 기능을 자식에서 다르게 재정의 하는것을 메서드 오버라이딩이라고 한다.

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

 

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

public class ElectricCar extends Car {

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

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

 

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

+ @가 붙은 부분을 애노테이션이라고 한다. 애노테이션은 주석과 비슷한데, 프로그램이 읽을 수 있는 특별한 주석이라 생각하면 된다. 

이 애노테이션은 상위 클래스의 메서드를 오버라이드 하는 것임을 나타낸다.

이름 그대로 오버라이딩한 메서드 위에 이 애노테이션을 붙여야한다.(안붙혀도 IDE가 알아서 하긴 한다...)

 

가독성도 있고, 메서드의 이름을 틀릴 수 있으니 @Override를 꼭 붙히도록 한다.

 

Override할때 메서드 구조는 어떻게 될까?

1. electricCar.move()를 호출한다.

2. 호출한 electricCar의 타입은 ElectricCar이다. 따라서 인스턴스 내부의 ElectricCar타입에서 시작한다.

3. ElectricCar 타입에 move()메서드가 있다. 해당 메서드를 실행한다. 이때 실행할 메서드를 이미 찾았으므로 부모타입을 찾지 않는다.

 

 

오버로딩 and 오버라이딩

오버로딩과 오버라이딩은 이름만 비슷할 뿐 전혀 다른 기능을 제공한다.

 

메서드 오버로딩 : 메서드 이름이 같고 매개변수가 다른 메서드를 정의하는 것(메서드 재정의), 오버로딩은 번역하면 과적인데, 과하게 물건들 담았다는 뜻이다. 따라서 같은 이름의 메서드를 여러개 정의했다고 이해하면 된다.

 

메서드 오버라이딩 : 하위 클래스에서 상위 클래스의 메서드를 재정의 하는 과정을 의미한다. 오버라이딩을 단순하게 해석하면 무언가를 넘어서 타는 것을 말한다. 자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버린다고 이해하면 된다.

 

 

메서드 오버라이딩은 몇가지 조건을 충족해야 오버라이딩이 가능하다.

1. 메서드 이름이 같아야한다.

2. 파라미터 타입, 순서, 개수가 같아야한다.

3. 반환 타입이 같아야 한다.

4. 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안된다.

5. 생성자는 오버라이딩 할 수 없다.

6. static, final, private 키워드가 붙은 메서드는 오버라이딩 될 수 없다.

-static은 클래스 레벨에서 작동하므로 의미가 없다.

-final 메서드는 재정의를 금지하기 때문

-private 메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않는다.

 

 

 

만약 패키지가 다를때 상속을 받으려면 어떻게 해야할까?

우선 패키지가 다르면 private와 default여선 안된다.

public은 아무런 제약사항이 없고, protected는 같은 패키지 및 상속 관계에서만의 접근을 허용한다.

 

public class Parent {

    public int publicValue;
    protected int protectedValue;
    int defaultValue;
    private int privateValue;

    public void publicMethod(){
        System.out.println("퍼블릭 메서드");
    }

    protected void protectedMethod(){
        System.out.println("프로텍티드 메서드");
    }

    void defaultMethod(){
        System.out.println("디폴트 메서드");
    }

    private void privateMethod(){
        System.out.println("프라이베이트 메서드");
    }

    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();
    }

}

 

public class Child extends Parent {

    public void call(){
        publicValue = 1;
        protectedValue = 1;//상속관계이기에
//        defaultValue; //다른 패키지 접근 불가
//        privateValue; //접근 불가


        publicMethod();
        protectedMethod();
//        defaultMethod();
//        privateMethod();

        printParent();
    }





}

 

위의 Parent를 상속받는 Child가 있다. 이 child를 보면 패키지가 다르게 설정되었다

 

이렇게 다르게 설정 되었을때 접근 가능한 접근 제어자는 protected와 public 두개 뿐이기에

defaultMethod, privateMethod, defaultValue, privateValue 는 접근이 불가능하다.

그러나 printParent를 통해서는 호출이 가능한데 printParent메서드는 public이면서 부모쪽에 정의되어있기 때문에 부모 메서드 안에서는 접근이 가능하다.

 

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

 

 

 

 

이런 상속을 받을때 자식과 필드명이 같거나 메서드가 오버라이딩 되어있으면, 자식에서 부모의 필드나 메서드를 호출할 수 없다. 이런 문제를 해결하기 위해 super 키워드를 사용하여 해결한다. 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);
        System.out.println("super value ="+super.value);

        this.hello();
        super.hello();
    }

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

    child.call();
}

 

call()메서드를 확인하자

this는 자기 자신의 참조를 뜻한다. this는 생략할 수 있다.

super는 부모 클래스에 대한 참조를 뜻한다.

필드 이름과 메서드 이름이 같지만 super를 사용해서 부모 클래스에 있는 기능을 사용할 수 있다.

 

 

앞서 말했든 상속 관계의 인스턴스를 생성하면 결국 메모리 내부에는 자식과 부모 클래스가 각각 다 만들어진다. Child를 만들면 부모인 Parent까지 함께 만들어지는 것이다. 따라서 각각의 생성자도 모두 호출이 되어야 한다.

 

규칙 : 상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다!

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

public class ClassA {

    public ClassA(){
        System.out.println("classA생성자.");
    }
}
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);
    }
}
public class ClassC extends ClassB{
    public ClassC(){
        super(10, 20);
        System.out.println("ClassC 생성자");
    }
}

 

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

 

결과

classA생성자.
ClassB 생성자 a=10, b=20
ClassC 생성자

 

 

클래스B는 클래스A를 상속받고, 클래스C는 클래스B를 상속받는 코드가 있다.

생성자의 첫줄에 super();를 호출하였다.( 규칙 : 상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다) 하지만 이 경우 클래스A는 기본 생성자이기에 생략이 가능하다.

 

반응형

'공부 > Java' 카테고리의 다른 글

자바 다형성 2편 (Polymorphism)  (1) 2024.09.05
자바 다형성 1편 (Polymorphism)  (0) 2024.09.04
자바 final  (1) 2024.09.02
자바 메모리 구조와 static  (2) 2024.09.02
자바 접근 제어자  (5) 2024.08.31