본문 바로가기

프로그래밍 공부/Java

Java - 상속(Inheritance)

상속이란 기존의 클래스에 기능을 추가하거나 재정의 하여 새로운 클래스를 정의하는 것을 의미한다. JavaScript에서는 클래스보다 프로토타입을 사용하지만 상속에 대한 개념은 클래스를 사용하는 Java 와 비슷하다.

상속을 이용하면 기존에 정의되어 있는 클래스의 모든 필드와 메서드를 물려받아 새로운 클래스를 생성할 수 있다. 이떄 기존에 정의되어 있던 클래스를 부모 클래스(Parent class) 또는 슈퍼 클래스(Super class) 또는 기초 클래스(Base class) 라고 부르며, 상속을 통해 새로 생성이 되는 클래스를 자식 클래스(Child class) 또는 서브 클래스(Sub class) 또는 파생 클래스(Derived class)라고 부른다.

상속은 세 가지 장점이 있다.
1. 기존에 작성된 클래스를 재활용 할 수있다.
2. 자식 클래스 설계시 중복되는 멤버를 미리 부모 클래스에 작성해 놓으면, 자식 클래스에서 해당 멤버를 작성하지 않아도 된다.
3. 클래스 간의 계층적 관계를 구현하고 다형성의 문법적 토대를 마련할 수있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 자식 클래스(Child class) 작성 방법
class ChildClass extends ParentClass { ... }
 
class Parent {
    private int a = 10;        // private 필드
    public int b = 20;        // public 필드
}
 
class Child extends Parent {
    public int c = 30;        // public 필드
    void display(){
    ①    // System.out.println(a);    // 상속받은 private 필드 참조
    ②    System.out.println(b);        // 상속받은 public 필드 참조
    ③    System.out.println(c);        // 자식 클래스에서 선언한 public 필드 참조
    }
}
 
public class Inheritance {
    public static void main(String[] args){
        Child child = new Child();
        child.display();
    }
}
cs

자식 클래스는 부모 클래스의 모든 특성을 물려받아 새롭게 작성된 클래스를 말한다. Java에서 자식 클래스는 위의 코드와 같이 선언한다. 

부모 클래스는 자식 클래스에 포함된 것으로 볼 수 있다. 부모 클래스에서 새로운 필드를 하나 추가하면 자식 클래스에도 자동으로 해당 필드가 추가된 것으로 동작하게 된다. 

자식 클래스에는 부모 클래스의 필드와 메서드만이 상속되며 생성자와 초기화 블록은 상속되지 않는다.  또한 부모 클래스의 접근제어가 private나 default로 설정된 멤버는 자식 클래스에서 상속받지만 접근할 수 없다.

위의 코드에서 ①번(13행) 라인에서는 자식 클래스의 메서드에서 부모 클래스에서 상속받은 public 필드를 참조하고 있다. 이처럼 자식 클래스에서 따로 선언하지 않은 필드라도 해당 이름의 필드를 부모 클래스에서 상속받았다면 문제는 없다. 하지만 주석처리된 ②번(12행) 라인처럼 해당 필드가 부모 클래스의 private 필드라면 접근할 수 없으므로 오류가 발생한다. 또한 자식 클래스에서는 ③번(14행) 라인처럼 자신만의 필드나 메서드를 선언하여 사용할 수 있다.

주의할 점은 Java에서 클래스는 단 한개의 클래스만을 상속받는 단일 상속만 가능하다.

Java에서 Object 클래스는 모든 클래스의 부모 클래스가 되는 클래스이다. 따라서 Java의 모든 클래스는 자동으로 Object 클래스의 모든 필드와 메서드를 상속받게 된다.

즉 Java의 모든 클래스는 별도로 extends 키워드를 사용하여 Object 클래스의 상속을 명시하지 않아도 Object 클래스의 모든 멤버를 자유롭게 사용할 수 있다. Java의 모든 객체에서 toString()이나 clone()과 같은 메서드를 바로 사용할 수 있는 이유가 해당 메서드들이 Object 클래스의 메서드이기 때문이다.

super 키워드는 부모 클래스로부터 상속받은 필드나 메서드를 자식 클래스에서 참조하는 데 사용하는 참조 변수이다. 인스턴스 변수의 이름과 지역 변수의 이름이 같을 경우 인스턴스 변수 앞에 this 키워드를 사용해 구분할 수 있었는데 이와 마찬가지로 부모 클래스의 멤버와 자식 클래스의 멤버 이름이 같을 경우 super 키워드를 사용해 구별할 수 있다.

Java에서는 super 참조 변수를 사용해 부모 클래스의 멤버에 접근할 수 있고 this와 마찬가지로 super 참조 변수를 사용할 수 있는 대상도 인스턴스 메서드 뿐이며, 클래스 메서드에서는 사용할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// super 키워드의 사용
class Parent {int a = 10;}
 
class Child extends Parent{
    void display(){
        System.out.println(a);
        System.out.println(this.a);
        System.out.println(super.a);
    }
}
 
public class Inheritance {
    public static void main(String[] args){
        Child child = new Child();
        child.display();
    }
}
cs

위의 코드에서 int 자료형 변수 num은 부모 클래스인 Parent 클래스에서만 선언되어 있다. 따라서 지역변수와 this 참조 변수 그리고 super 참조 변수 모두 같은 값을 출력하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// super 키워드의 사용
class Parent {int a = 10;}
 
class Child extends Parent{
    int a = 20;
    void display(){
        System.out.println(a);
        System.out.println(this.a);
        System.out.println(super.a);
    }
}
 
public class Inheritance {
    public static void main(String[] args){
        Child child = new Child();
        child.display();
    }
}
cs

하지만 위의 코드에서 int 자료형 변수 num은 자식 클래스인 Child 클래스에서도 선언이 되어 있는데 지역 변수 this 참조 변수는 자식 클래스에서 대입된 값을 출력하고, super 참조 변수만이 부모 클래스에서 대입된 값을 출력하는 결과가 나온다.

this() 메서드가 같은 클래스의 다른 생성자를 호출할 때 사용된다면, super() 메서드는 부모 클래스의 생성자를 호출할 때 사용된다.

자식 클래스의 인스턴스를 생성하면 해당 인스턴스에는 자식 클래스의 고유 멤버뿐만 아니라 부모 클래스의 모든 멤버까지도 포함되어 있다. 따라서 부모 클래스의 멤버를 초기화하기 위해서는 자식 클래스의 생성자에서 부모 클래스의 생성자까지 호출해야 한다. 이런 부모 클래스의 생성자 호출은 모든 클래스의 부모 클래스인 Object 클래스의 생성자까지 계속 거슬러 올라가며 수행된다.

따라서 Java 컴파일러는 부모 클래스의 생성자를 명시적으로 호출하지 않는 모든 자식 클래스의 생성자 첫줄에 자동으로 super() 명령문을 추가하여, 부모 클래스의 멤버를 초기화할 수 있도록 한다.

주의할 점은 Java 컴파일러는 컴파일 시 클래스에 생성자가 하나도 정의되어 있지 않아야만 자동으로 기본 생성자를 추가해 준다. 만약 부모 클래스에 매개변수를 가지는 생성자를 하나라도 선언했다면 부모 클래스에서는 기본 생성자가 자동으로 추가되지 않을 것이다.

1
2
3
4
class Parent {
    int a;
    Parent(int n) {a = n;}
}
cs

그러나 Parenrt 클래스를 상속받은 자식 클래스에서 super() 메서드를 사용해 부모 클래스의 기본 생성자를 호출하면 오류가 발생하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
class Parent {
    int a;
    Parent(int n) {a = n;}
}
 
class Child extends Parent{
    int b;
    Child(){
        super();
        b = 20;
    }
}
cs

이유는 부모 클래스인 Parent 클래스에는 기본 생성자가 추가되어 있지 않기 때문이다. 따라서 매개변수를 가지는 생성자를 선언해야 할 경우에는 되도록이면 아래 처럼 기본 생성자까지 명시적으로 선언해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Parent {
    int a;
    Parent() {a = 10;}
    Parent(int n) {a = n;}
}
 
class Child extends Parent{
    int b;
    Child(){
        super();
        b = 20;
    }
}
cs

아래의 코드는 super() 메서드가 어떻게 호출되는지 보여준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// super() 메서드의 사용
class Parent {
    int a;
    Parent() {a = 10;}
    Parent(int n) {a = n;}
}
class Child extends Parent{
    int b;
    Child(){
    ①    //super(40);
        b = 20;
    }
    void display(){
        System.out.println(a);
        System.out.println(b);
    }
}
public class Inheritance {
    public static void main(String[] args){
        Child child = new Child();
        child.display();
    }
}
cs

위의 코드를 실행하면 Java 컴파일러는 주석 처리된 ①번(10행) 라인에 자동으로 super(); 메서드를 삽입한다. 따라서 변수 a는 10으로 초기화가 된다. 하지만 번(10행) 라인의 주석 처리를 해제하고 실행하면 부모 클래스인 Parent 클래스는  두 번째 생성자에 의해 초기화 되는데 이경우에는 변수 a는 40으로 초기화가 된다.

메서드 오버라이딩(method overriding)은 상속 관계에 있는 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 같은 시드니쳐를 가지는 메서드로 다시 정의하는 것이다. Java에서 자식 클래스는 부모 클래스의 private 멤버를 제외한 모든 메서드를 상속받는다.

이렇게 상속받은 메서드는 그대로 사용해도 되고, 필요한 동작을 위해 재정의해 사용할 수도 있다. 즉 메서드 오버라이딩이란 상속받은 부모 클래스의 메서드를 재정의해서 사용하는 것으로 정리할 수 있다.

오버라이딩에는 조건이 있다.
1. 오버라이딩이란 메서드의 동작만을 재정의하는 것이다, 따라서 메서드의 선언부는 기존 메서드와 동일해야 한다. 하지만 메서드의 리턴 타입은 부모 클래스의 리턴 타입으로 타입 변환할 수 있는 타입이라면 변경이 가능하다.
2. 부모 클래스의 메서드보다 접근 제어자를 더 좁은 범위로 변경할 수 없다.
3. 부모 클래스의 메서드보다 더 큰 범위의 예외를 선언할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 메서드 오버라이딩(Method overriding)
class Parent {
    void display() {System.out.println("부모 클래스의 display() 메서드이다.");}
}
 
class Child extends Parent {
    void display() {System.out.println("자식 클래스의 display() 메서드이다.");}
}
 
public class Inheritance {
    public static void main(String[] args){
        Parent parent = new Parent();
        parent.display();
        Child child = new Child();
        child.display();
        Parent parent = new Child();
        parent.display();        // Child parent = new Parent();
    }
}
cs

위의 코드를 실행하면 

부모 클래스의 display() 메서드입니다.
자식 클래스의 display() 메서드입니다.
자식 클래스의 display() 메서드입니다.

의 결과가 차례대로 출력이 된다. 세 번째와 같은 인스턴스의 참조가 허용되는 이유는 바로 자바에서의 다형성(Polymorphism) 때문이다.

오버로딩과 오버라이딩은 그 단어의 유사람으로 인해 혼동하기 쉽다. 하지만 그 개념은 확실하게 다르다. 

오버로딩(Overloading) : 새로운 메서드를 정의하는 것
오버라이딩(Overriding) : 상속받은 기존의 메서드를 재정의하는 것

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Parent {
    void display(){
        System.out.println("부모 클래스의 display() 메서드이다.");
    }
}
 
class Child extends Parent {
    // 오버라이딩된 display() 메서드
    void display(){
        System.out.println("자식 클래스의 display() 메서드이다.");
    }
    void display(String str){
        // 오버로딩된 display() 메서드
        System.out.println(str);
    }
}
 
public class Main {
    public static void main(String[] args){
        Child child = new Child();
        child.display();
        child.display("오버로딩된 display() 메서드입니다.");
    }
}
cs