새소식

반응형
Programming Language/Java

[Java] 자바 - 상속

  • -
반응형

자바 로고 이미지입니다.
Java

자바 - 상속


자바에서 상속은 객체 지향 프로그래밍(OOP)의 핵심 개념 중 하나로, 클래스들 간에 코드를 재사용하고 확장하기 위한 메커니즘을 제공한다. 상속은 기존 클래스의 특성과 동작을 그대로 가져와 새로운 클래스를 만들 수 있게 해 준다. 이때, 기존 클래스는 부모 클래스(또는 상위 클래스)라고 하고, 새로운 클래스는 자식 클래스(또는 하위 클래스)라고 한다.

 

기본개념

상속에 대한 이미지

 

핸드폰과 아이폰 클래스의 관계를 봤을 때 아이폰은 상위 개념으로 핸드폰이라고 할 수 있다. 따라서 위의 예시처럼 아이폰 클래스는 핸드폰 클래스를 상속받아 사용할 수 있다. 또한, 그림에는 없지만 핸드폰의 상위 개념으로 "기계"가 될 수 도 있겠다. 이처럼 현실 세계의 모든 객체는 객체와 객체끼리 부모와 자식 관계를 형성한다고 생각하면 된다. 예시 이미지에서 핸드폰 클래스를 상속받은 아이폰 클래스는 핸드폰 클래스의 field1 필드와 method1()을 아이폰 클래스에서 사용할 수도 있고 재정의할 수 도 있다.

 

class 자식클래스 extends 부모클래스{
    // 필드
    // 생성자
    // 메서드
}

현실에서는 보통 부모 → 자식으로 물려주지만 객체지향 프로그랭에선 자식 → 부모로 상속을 받는다. 즉, 자식이 상속받을 부모를 선택하는 것이다. 위의 예제 코드처럼 자식 클래스를 선언할 때 상속을 받을 부모를 선택해 extends 키워드로 상속을 받을 수 있다.

 

class Parent{

    String field1 = "저는 부모 클래스입니다.";

    void method1(){
        System.out.println("method1() : 저는 부모 클래스로부터 호출 되었습니다.");
    }

}

class Child extends Parent{

    String field2 = "저는 자식 클래스입니다.";

    void method2(){
        System.out.println("method2() : 저는 자식 클래스로부터 호출 되었습니다.");
    }

}

public class Main{
    public static void main(String[] args){
        Child child = new Child();
        System.out.println(child.field1);
        child.method1();
    }
}
저는 부모 클래스입니다.
method2() : 저는 부모 클래스로부터 호출 되었습니다.

실행 결과

 

 

 

다음은 자바에서 상속을 받을 때 알아야 한 몇 가지 특징들이니 꼭 명심하고 상속을 사용하자.

 

하나의 자식 클래스는 하나의 부모 클래스로부터만 상속받을 수 있다.

자바는 다중 상속을 허용하지 않기 때문에 하나의 자식이 여러 개의 부모 클래스를 상속받을 수 없다.

class 자식클래스 extends 부모클래스1, 부모클래스2, ...{

}

불가

 

부모 클래스 생성 시 private 접근 제한을 갖는 필드와 메서드는 상속 대상에서 제외된다.

부모 클래스에서 private으로 선언된 필드와 메서드는 하위 클래스에서 직접적으로 접근할 수 없다. 즉, private으로 선언된 멤버는 상속이라는 개념에서 제외되는 것이다. private 멤버는 오직 그 멤버가 속한 클래스 내부에서만 접근이 가능하다.

 

다만, 상속 관계에서 하위 클래스는 부모 클래스의 public 및 protected 멤버에 대한 접근 권한을 가진다. public 멤버는 어디서든 접근이 가능하며, protected 멤버는 하위 클래스에서도 접근이 가능하지만 외부 클래스에서는 직접적인 접근이 불가능하다. 아래의 예제 코드를 한 번 살펴보며 이해해 보자.

class Parent {
    private int privateField;

    public void publicMethod() {
        System.out.println("Public method in Parent class");
    }

    private void privateMethod() {
        System.out.println("Private method in Parent class");
    }
}

class Child extends Parent {
    // 하위 클래스에서는 private 멤버에 직접 접근할 수 없음
    // private int privateField; // 컴파일 에러

    // public 및 protected 멤버에 대해서는 접근이 가능함
    public void childMethod() {
        System.out.println("Method in Child class");
        publicMethod(); // 상속받은 public 메서드 호출 가능
        // privateMethod(); // 컴파일 에러 - private 메서드에 직접 접근 불가
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.publicMethod(); // 가능
        // child.privateMethod(); // 컴파일 에러 - private 메서드에 직접 접근 불가
    }
}

 

 

부모 생성자 호출

현실에서 부모가 생기기 전에 자식이 먼저 생길 수 없듯이 자바에서도 부모의 생성자가 먼저 호출된 후에 자식의 생성자가 호출된다.

class Parent{
    ...
}

class Child extends Parent{
    ...
}

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

위의 코드를 살펴보았을 때 얼핏 보면 Child 클래스의 객체만 생성하는 것처럼 보이지만, Child 클래스는 Parent 클래스를 상속받고 있다. 그래서 내부적으로는 Parent 클래스의 객체가 생성된 후에, Child 객체가 생성된다.

부모와 자식 객체에 대한 이미지입니다.
부모와 자식 객체

 

자바에선 객체를 생성하려면 해당 클래스의 생성자 메서드가 호출되어야만 한다. 하지만 위의 예제 코드에서 우리는 Child 클래스에서 부모의 생성자를 호출하지 않았는데 어떻게 부모의 생성자가 호출되어 부모 객체가 먼저 생성이 되는 것일까?

 

📢 혹시 자바의 생성자에 대해 아직 배우지 못한 분들이 계시다면, 자바에서 클래스를 정의할 때 생성자를 따로 정의하지 않아도 컴파일러는 자동으로 생성자를 정의하여 호출한다.

 

위의 질문에 대한 정답은 바로 자식 클래스에 있다. 

public Child(){
    super();
}

클래스를 정의할 때 생성자가 명시적으로 선언되지 않았다면 컴파일러는 위와 같이 기본 생성자를 생성한다.  super()는 부모의 기본 생성자를 호출하는 역할을 한다. 즉, Child 객체를 생성하기 위해 Child() 기본 생성자가 호출을 하였지만 super() 메서드가 있어 부모의 기본 생성자를 호출하게 된다. 따라서 결론적으로 부모의 객체가 먼저 만들어지고 난 후에야 자식의 객체가 만들어지는 것이다.

 

하지만 만약 독자분들이 직접 자식 생성자를 선언하고 명시적으로 부모 생성자를 호출하고 싶다면 아래와 같이 작성하면 된다.

public Child(파라미터, ...){
    super(파라미터, ...);
}

super(파라미터,...)는 파라미터 타입과 일치하는 부모 생성자를 호출하게 된다. 만약 파라미터 타입과 일치하는 부모 클래스의 생성자가 존재하지 않는다면 컴파일 에러가 발생할 것이다.

 

super(파라미터,...)가 생략될 경우엔 컴파일러에 의해 super()가 자동적으로 추가되기 때문에 부모의 기본 생성자가 존재해야 한다. 하지만 생성자에 대해 아직 배우지 못했다면 해당 내용을 이해하는데 어려움이 있을 거라 필자는 개인적으로 생각한다. 따라서 생성자에 대해 살펴본 후에 다시 읽어보는 것을 추천한다.

 

메서드 재정의(오버라이딩)

부모 클래스의 모든 메서드가 물려받을 자식 클래스에 100% 맞게 설계되었다면 가장 좋겠지만, 때로는 부모 클래스를 물려받은 자식 클래스에서 부모 클래스의 메서드를 재정의해서 사용할 때가 있을 수 있다. 자바에선 이러한 경우를 메서드 재정의(Overriding)라고 한다.

 

메서드 재정의할 때 알아야 할 규칙

메서드 재정의란 다시 말해 부모 클래스를 물려받은 자식 클래스에서 부모의 메서드를 자식 클래스의 사용 목적에 더 최적하기 위해 메서드를 재정의하는 것이다. 하지만, 메서드를 재정의할 때 알아야 할 몇 가지 규칙이 있기 때문에 아래를 한 번 살펴보자.

 

  • 부모의 메서드와 동일한 시그니처(반환 타입, 메서드 이름, 파라미터(타입과 개수 모두))를 가져야 한다.
  • 접근 제한을 더 강하게 다시 정의할 순 없다.
  • 새로운 예외(Exception)를 throws 할 수 없다.

📢 접근 제한을 더 강하게 다시 정의할 수 없다는 건 무슨 뜻인가?

자바의 접근 제한자의 순서는 private → default → protected → public으로 가장 강한 제한자부터 나열한 것이다. 더 강하게 설정한다는 것은 더 제한적인 접근을 의미한다. 예를 들어, 부모 클래스의 메서드가 protected이면, 자식 클래스에서는 protected 또는 public으로만 재정의할 수 있고 private이나 default으로는 재정의할 수 없다.

 

class Parent{
    void method(){
        System.out.println("저는 Parent 객체의 method() 메서드입니다.");
    }
}

class Child extends Parent{
    void method(){
        System.out.println("저는 Parent 객체의 method() 메서드를 재정의한 메서드입니다.");
    }
}

public class Main{
    public static void main(String[] args){
        Child child = new Child();
        child.method();
    }
}
저는 Parent 객체의 method() 메서드를 재정의한 메서드입니다.

실행 결과

 

📢 그러면 자식 객체에서 부모 메서드를 재정의해서 사용했지만, 부모 메서드를 사용하고 싶을 땐 어떻게 하는가?

좋은 질문이다, 자식 객체에서 부모의 메서드를 재정의 하여 사용했더라도 부모의 메서드를 사용해야 할 때가 있을 수도 있다. 그럴 땐 명시적으로 super 키워드를 붙여서 사용하면 된다.

super.부모메서드();

 

class Parent{
    void method(){
        System.out.println("저는 Parent 객체의 method() 메서드입니다.");
    }
}

class Child extends Parent{
    void method(){
        System.out.println("저는 Parent 객체의 method() 메서드를 재정의한 메서드입니다.");
        super.method(); // super 키워드를 사용하여 부모 메서드 호출
    }
}

public class Main{
    public static void main(String[] args){
        Child child = new Child();
        child.method();
    }
}
저는 Parent 객체의 method() 메서드를 재정의한 메서드입니다.
저는 Parent 객체의 method() 메서드입니다.

실행 결과

 

메서드뿐만 아니라 부모 객체의 필드에 접근하고 싶을 때도 super 키워드를 사용하면 된다.

 

final 클래스와 final 메서드

자바에서 final 키워드 붙은 클래스, 메서드, 필드는 해당 선언이 최종 상태임을 의미한다. 즉, 한 번 선언이 되면 결코 수정될 수 없음을 의미한다. final 키워드가 클래스, 메서드, 필드에 붙을 때마다 각각의 해석이 조금씩 달라지긴 한다. 예를 들어 필드에 붙을 경우 해당 변수를 더 이상 변경할 수 없음을 의미한다. 만약 클래스와 메서드에 붙어있다면 상속과 관련이 있다. 그렇다면 클래스와 메서드에 final 키워드가 붙은 경우 상속에서 어떤 관계가 있는지 살펴보자.

 

상속할 수 없는 final 클래스

클래스를 선언할 때 final 키워드가 붙어있다면 해당 클래스는 상속을 할 수 없는 클래스가 된다. 부모 클래스가 될 수 없기 때문에 자식 클래스 또한 가질 수 없다.

public final class 클래스이름{
    ...
}

 

상속할 수 없는 final 메서드

메서드를 선언할 때도 마찬가지로 final 키워드가 붙으면 해당 메서드는 최종적인 메서드이기 때문에 재정의 할 수 없는 메서드가 된다. 즉, 자식 클래스가 부모 클래스를 상속받았을 때 부모 클래스의 메서드 중에 final 키워드가 붙은 메서드들은 자식 클래스에서 해당 메서드들을 재정의할 수 없다.

public final 리턴타입 메서드이름(파라미터, ...){
    ...
}

 

728x90
반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.