Java 공부 26일차(상속이란?)2편

임선구·2025년 2월 13일

몸 비틀며 Java

목록 보기
27/58

오늘의 잔디


오늘의 공부


상속과 접근 제어

상속 관계와 접근 제어에 대해 알아보자. 참고로 접근 제어를 자세히 설명하기 위해 부모와 자식의 패키지를 따로 분리하였다. 이 부분에 유의해서 예제를 만들어보자.


접근 제어자를 표현하기 위해 UML 표기법을 일부 사용했다.

  • + : public
  • # : protected
  • ~ : default
  • - : private

접근 제어자를 잠시 복습해보자.

접근 제어자의 종류

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

순서대로 private 이 가장 많이 차단하고, public 이 가장 많이 허용한다.
private -> default -> protected -> public

그림과 같이 다양한 접근 제어자를 사용하도록 코드를 작성해보자.

package extends1.access.parent;
public class Parent {
 public int publicValue;
 protected int protectedValue;
 int defaultValue;
 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");
 }
 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 과 같은 모든 접근 제어자가 필드와 메서드에 모두 존재한다.

package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
 public void call() {
 publicValue = 1;
 protectedValue = 1; //상속 관계 or 같은 패키지
 //defaultValue = 1; //다른 패키지 접근 불가, 컴파일 오류
 //privateValue = 1; //접근 불가, 컴파일 오류
 publicMethod();
 protectedMethod(); //상속 관계 or 같은 패키지
 //defaultMethod(); //다른 패키지 접근 불가, 컴파일 오류
 //privateMethod(); //접근 불가, 컴파일 오류
 printParent();
 }
}

둘의 패키지가 다르다는 부분의 유의하자

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

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

메서드의 경우도 앞서 설명한 필드와 동일하다.

package extends1.access;
import extends1.access.child.Child;
public class ExtendsAccessMain {
 public static void main(String[] args) {
 Child child = new Child();
 child.call();
 }
}

실행 결과

Parent.publicMethod
Parent.protectedMethod
==Parent 메서드 안==
publicValue = 1
protectedValue = 1
defaultValue = 0
privateValue = 0
Parent.defaultMethod
Parent.privateMethod

코드를 실행해보면 Child.call() -> Parent.printParent() 순서로 호출한다.

Child 는 부모의 public , protected 필드나 메서드만 접근할 수 있다. 반면에 Parent.printParent() 의 경우 Parent 안에 있는 메서드이기 때문에 Parent 자신의 모든 필드와 메서드에 얼마든지 접근할 수 있다.

접근 제어와 메모리 구조

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

super - 부모 참조

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

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

package extends1.super1;
public class Parent {
 public String value = "parent";
 public void hello() {
 System.out.println("Parent.hello");
 }
}
package extends1.super1;
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();
 }
}

call() 메서드를 보자.

  • this 는 자기 자신의 참조를 뜻한다. this 는 생략할 수 있다.
  • super 는 부모 클래스에 대한 참조를 뜻한다.
  • 필드 이름과 메서드 이름이 같지만 super 를 사용해서 부모 클래스에 있는 기능을 사용할 수 있다.
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 를 사용한 경우 부모 클래스의 기능을 사용한 것을 확인할 수 있다.

super 메모리 그림

super - 생성자

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

상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다.(규칙)
상속 관계에서 부모의 생성자를 호출할 때는 super(...) 를 사용하면 된다.

예제를 통해 상속 관계에서 생성자를 어떻게 사용하는지 알아보자.

package extends1.super2;
public class ClassA {
 public ClassA() {
 System.out.println("ClassA 생성자");
 }
}

ClassA 는 최상위 부모 클래스이다.

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);
 }
}
  • ClassBClassA 를 상속 받았다. 상속을 받으면 생성자의 첫줄에 super(...) 를 사용해서 부모 클래스의 생성자를 호출해야 한다.
    • 예외로 생성자 첫줄에 this(...) 를 사용할 수는 있다. 하지만 super(...) 는 자식의 생성자 안에서
      언젠가는 반드시 호출해야 한다.
  • 부모 클래스의 생성자가 기본 생성자(파라미터가 없는 생성자)인 경우에는 super() 를 생략할 수 있다.
    • 상속 관계에서 첫줄에 super(...) 를 생략하면 자바는 부모의 기본 생성자를 호출하는 super() 를 자동으로 만들어준다.
    • 참고로 기본 생성자를 많이 사용하기 때문에 편의상 이런 기능을 제공한다.
package extends1.super2;
public class ClassC extends ClassB {
 public ClassC() {
 super(10, 20);
 System.out.println("ClassC 생성자");
 }
}
  • ClassCClassB 를 상속 받았다. ClassB 다음 두 생성자가 있다.
    • ClassB(int a)
    • 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 생성자 a=10 b=20
ClassC 생성자

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


1~3까지의 과정
new ClassC() 를 통해 ClassC 인스턴스를 생성한다. 이때 ClassC() 의 생성자가 먼저 호출되는 것이 맞다. 하지만 ClassC() 의 성생자는 가장 먼저 super(..) 를 통해 ClassB(...) 의 생성자를 호출한다. ClassB() 의생성자도 부모인 ClassA() 의 생성자를 가장 먼저 호출한다.

4~6까지의 과정

  • ClassA() 의 생성자는 최상위 부모이다. 생성자 코드를 실행하면서 "ClassA 생성자" 를 출력한다.
    ClassA() 생성자 호출이 끝나면 ClassA() 를 호출한 ClassB(...) 생성자로 제어권이 돌아간다.
  • ClassB(...) 생성자가 코드를 실행하면서 "ClassB 생성자 a=10 b=20" 를 출력한다. 생성자 호출이 끝나면 ClassB(...) 를 호출한 ClassC() 의 생성자로 제어권이 돌아간다.
  • ClassC() 가 마지막으로 생성자 코드를 실행하면서 "ClassC 생성자" 를 출력한다.

정리

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

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

package extends1.super2;
public class ClassB extends ClassA {
 public ClassB(int a) {
 this(a, 0); //기본 생성자 생략 가능
 System.out.println("ClassB 생성자 a=" + a);
 }
 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) {
 //ClassC classC = new ClassC();
 ClassB classB = new ClassB(100);
 }
}

실행 결과

ClassA 생성자
ClassB 생성자 a=100 b=0
ClassB 생성자 a=100

정리

클래스와 메서드에 사용되는 final

클래스에 final

  • 상속 끝!
  • final 로 선언된 클래스는 확장될 수 없다. 다른 클래스가 final 로 선언된 클래스를 상속받을 수 없다.
  • 예: public final class MyFinalClass {...}

메서드에 final

  • 오버라이딩 끝!
  • final 로 선언된 메서드는 오버라이드 될 수 없다. 상속받은 서브 클래스에서 이 메서드를 변경할 수 없다.
  • 예: public final void myFinalMethod() {...}
profile
끝까지 가면 내가 다 이겨

0개의 댓글