Computer Science/Java

Java의 상속에 관하여

무니화니 2025. 1. 12. 19:58

윤성우 저자의 '윤성우의 열할 Java 프로그래밍'을 읽으면서, 상속에 대한 개념을 정리하고자 한다.
내 자신이 중요하다고 생각한 부분을 정리하였기에, 책의 내용과 상이할 수도 있고, 모든 내용이 담겨있지도 않다.
chatGPT의 의견도 반영되어 있다.


1. 왜 상속을 사용해야 하는가

보통 상속의 개념을 사용하는 이유를 '코드의 재활용'에서 찾지만, 실제로 이는 옳은 답이 아니다.
상속은 '연관된 클래스들에 대해 공통적인 규칙을 정의할 수 있기' 때문이다.

즉, 기존에 정의된 클래스에, 메서드와 변수를 추가하여 새로운 클라스를 정의할 수 있다.

class Man{
    String name;
    public void tellName(){
        System.out.println("My Name: " +name);
    }
}

과 같은 클래스가 정의되어 있다고 하자.

class BusinessMan extends Man{
    String company;
    public BusinessMan(String company){
    this.company = company;
    }
    public void tellInfo(){
        System.out.println("My company: " + company);
        tellName();
    }
}

와 같은 BusinessMan class를 선언하여, Man 클래스를 상속하였다. (물려받았다.)
즉, BusinessMan 클래스에서는 Man 클래스를 자유롭게 사용할 수 있다. 또한, Businessman 클래스에서의 메서드와 변수를 사용할 수 있다.

이를 UML 기호와 같은 표시를 이용하면, BusinessMan -> Man 처럼 Man에게 화살표를 보이는 것 처럼, 하위클래스에서 상위 클래스로 향하게 그리면 된다.

2. 생성자

또한 클래스에 생성자를 정의할 수 있다.
상위 클래스에서 정의한 변수를 바탕으로, 상속을 받은 클래스가 초기화를 할 수도 있다.

또한, 하위 클래스의 인스턴스 생성 시 상위 클래스와 하위 클래스의 생성자가 모두 호출된다. 이 과정에서, 상위 클래스의 생성자가 먼저 호출되고, 하위클래스의 인스턴스 생성이 이루어진다.

다양한 생성자가 있는 경우가 있다. 예시를 보자.

class SuperCLS {
    public SuperCLS() {
        System.out.println("Con: SuperCLS()");
    }

    public SuperCLS(int i, int j) {
        System.out.println("Con: SuperCLS(int i, int j)");
    }
}

class SubCLS extends SuperCLS {
    public SubCLS() {
        System.out.println("Con: SubCLS()");
    }
    public SubCLS(int i, int j) {
        super(i, j);
        System.out.println("Con: SubCLS(int i, int j)");
    }
}

class SuperSubCon {
    public static void main(String[] args) {
        System.out.println("1. ");
        new SubCLS();

        System.out.println("3. ");
        new SubCLS(1, 2);
    }
}

SubCLS가 SuperCLS를 상속받았다. SubCLS의 생성자는 argument로 아무 것도 받지 않은 경우와, 2개의 정수를 받는 경우가 있다.
해당 경우에는, super()을 이용하여 두 정수를 인자로 전달 받을 수 잇는 생성자를 호출할 수 있다.
여기서, super를 이용한 생성자 호출문은 생성자의 첫 문장으로 들어가야 한다.
(만약 argument가 없는 경우에는, super(); 가 있다고 가정한다.)


기본적으로 IS - A 관계로 상속을 맺는다. 하위 클래스는 상위 클래스의 모든 특성을 지니고, 거기에 자신의 추가적인 특성을 더한다.
즉, 스마트폰은 모바일 폰의 일부이고, 스마트폰은 모바일폰이며, 스마트폰은 일종의 모바일폰이기 때문이기에,
class 스마트폰 extends 모바일폰 과 같이 나타낼 수 있다.
(HAS - A 관계도 상속으로 쓰일 수 있지만, 잘 쓰이지는 않는다.)

3.클래스의 상속과 참조 가능성

class Cake {
    public void sweet()
}
class CheeseCake extends Cake{
    public void milky()
}
class StrawberryCheeseCake extends CheeseCake{
    public void sour()
 }

와 같은 형태의 클래스 상속이 있다고 하자.
StrawberryChesseCake인스턴스는 CheeseCake 인스턴스이면서, Cake 인스턴스이다.
따라서,

Cake cake1= new StrawberryCheeseCake();
CheeseCake cake2= new StrawberryCheeseCake();

와 같이 참조가 가능하다.
하지만, cake1은 오로지 cake1.sweet(); 의 메서드만 호출할 수 있고,
cake2는 cake2.sweet(), cake2.milky()의 메서드를 호출 할 수 있다.

이는 배열을 생성하여 참조할 수 있다.

super 키워드 정리
super 키워드는 상속 관계에서 자식 클래스가 부모 클래스의 멤버 ( 필드,메서드,생성자)에 접근할 때 사용됨.

  1. 생성자 호출
    class 부모 {
     부모() { // 부모 생성자
     }
    }
class 자식 extends 부모 {  
자식() {  
super(); // 부모 클래스의 생성자 호출  
}  
}
  1. 메서드 호출
class 부모 {  
void 메서드() {  
System.out.println("부모 메서드");  
}  
}

class 자식 extends 부모 {  
void 메서드() {  
super.메서드(); // 부모 클래스의 메서드 호출  
System.out.println("자식 메서드");  
}  
}
  1. 부모 클래스 필드
class 부모 {  
int 숫자 = 10;  
}

class 자식 extends 부모 {  
int 숫자 = 20;


void 출력() {
    System.out.println(super.숫자); // 부모 클래스의 숫자 (10)
    System.out.println(this.숫자); // 자식 클래스의 숫자 (20)
    }
}

4. instanceof 연산자

특정 객체가 특정 클래스거나 인터페이스의 인스턴스인지 사용되는 연산자

객체 instanceof 클래스명 와 같은 방식으로 사용되어, if문에 자주 사용됨.

5. 클래스와 메서드의 final 선언

public final class MyClass 와 같이, final 키워드가 부여되면,
다른 클래스가 해당 클래스를 상속할 수 없다.
이와 같이,

class myClass{  
public final void func(int n){}  
}

처럼 final이 메서드에 사용되면 이 메서드는 다른 클래스에서 오버라이딩 할 수 없다.

6. @Override 애너테이션

메서드가 부모 클래스나 인터페이스의 메서드를 override (오버라이드, 재정의)하고 있음을 명시한다.
만약 메서드 이름을 잘못 적거나, 부모 클래스와 메서드 이름이 일치하지 않으면 컴파일러가 오류를 발생시킨다.


class 부모 {  
void 인사() {  
System.out.println("안녕하세요, 부모입니다.");  
}  
}

class 자식 extends 부모 {  
@Override  
void 인사() {  
System.out.println("안녕하세요, 자식입니다.");  
}  
}

7. 인터페이스

인터페이스는 클래스가 구현해야 하는 메서드의 계약서 역할을 한다고 생각할 수 있다.

interface 비행기 {  
void 이륙();  
void 착륙();  
}

class 여객기 implements 비행기 {  
@Override  
public void 이륙() {  
System.out.println("여객기가 이륙합니다.");  
}


@Override
public void 착륙() {
    System.out.println("여객기가 착륙합니다.");
}

}

다음 코드처럼, 구현의 세부 사항을 구현하지 않고, 공통적인 기능을 추상화할 때 사용할 수 있다.

인터페이스는 다중 상속을 지원하기에, 한 클래스가 여러 인터페이스의 기능을 사용할 수 있다.

이를 이용하여 코드의 유연성을 확보할 수 있고, 코드의 결합도 또한 낮출 수 있다.