오늘의 잔디
오늘의 공부

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

접근 제어자를 표현하기 위해 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 는 이름 그대로 부모 클래스에 대한 참조를 나타낸다.
다음 예를 보자. 부모의 필드명과 자식의 필드명이 둘다 value 로 똑같다. 메서드도 hello() 로 자식에서 오버라이딩 되어 있다. 이때 자식 클래스에서 부모 클래스의 value 와 hello() 를 호출하고 싶다면 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 메모리 그림

상속 관계의 인스턴스를 생성하면 결국 메모리 내부에는 자식과 부모 클래스가 각각 다 만들어진다. 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);
}
}
ClassB 는 ClassA 를 상속 받았다. 상속을 받으면 생성자의 첫줄에 super(...) 를 사용해서 부모 클래스의 생성자를 호출해야 한다.this(...) 를 사용할 수는 있다. 하지만 super(...) 는 자식의 생성자 안에서super() 를 생략할 수 있다.super(...) 를 생략하면 자바는 부모의 기본 생성자를 호출하는 super() 를 자동으로 만들어준다.package extends1.super2;
public class ClassC extends ClassB {
public ClassC() {
super(10, 20);
System.out.println("ClassC 생성자");
}
}
ClassC 는 ClassB 를 상속 받았다. 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 로 선언된 클래스를 상속받을 수 없다.public final class MyFinalClass {...}메서드에 final
final 로 선언된 메서드는 오버라이드 될 수 없다. 상속받은 서브 클래스에서 이 메서드를 변경할 수 없다.public final void myFinalMethod() {...}