[JAVA] 이것이 자바다_9

Updated:

상속의 첫 번째 정리.
상속의 개념과 클래스 상속, 부모 생성자 호출, 메소드 재정의, final 클래스와 final 메소드에 대해서 알아보자.

상속 개념


객체 지향 프로그램에서 부모 클래스의 멤버를 자식 클래스에게 물려줄 수 있다.
프로그램에서는 부모 클래스를 상위 클래스라고 부르기도 하고, 자식 클래스를 하위 클래스, 또는 파생 클래스라고 부른다.

상속은 이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 코드의 중복을 줄여준다.

상속을 해도 부모 클래스의 모든 필드와 메소드들을 물려받는 것은 아니다.
부모 클래스에서 private 접근 제한 을 갖는 필드와 메소드는 상속 대상에서 제외된다.
그리고 부모 클래스와 자식 클래스가 다른 패키지에 존재한다면 default 접근 제한 을 갖는 필드와 메소드도 상속 대상에서 제외된다.
그 이외의 경우는 모두 상속의 대상이 된다.

상속을 이용하면 클래스의 수정을 최소화시킬 수 있다.
부모 클래스의 수정으로 모든 자식 클래스들의 수정 효과를 가져오기 때문에 유지 보수 시간을 최소화시켜준다.
예를 들어 클래스 B, C가 클래스 A를 상속할 경우 A의 필드와 메소드를 수정함으로써 B, C를 수정하지 않아도 수정된 A의 필드와 메소드를 이용할 수 있다.


클래스 상속

현실에서 상속은 부모가 자식을 선택해서 물려주지만, 프로그램에서는 자식이 부모를 선택한다.

자식 클래스를 선언할 때 어떤 부모 클래스를 상속받을 것인지를 결정하고 선택된 부모 클래스는 다음과 같이 extends 뒤에 기술한다.

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

예를 들어 Car 클래스를 상속해서 SportsCar 클래스를 설계하고 싶다면 다음과 같이 작성하면 된다.

class SportsCar extends Car {
}

다른 언어와는 달리 자바는 다중 상속을 허용하지 않는다.
즉 여러 개의 부모 클래스를 상속할 수 없다.
그러므로 다음과 같이 extends 뒤에 두 개의 부모 클래스는 허용되지 않는다.

class 자식 클래스 extends 부모클래스1, 부모클래스2 {   // 부모클래스2 가 없어야 한다.
    // 오류
}

다음 예제는 핸드폰(CellPhone) 클래스를 상속해서 DMB폰(DmbCellPhone) 클래스를 작성한 것이다.
핸드폰이 부모 클래스가 되고, DMB폰이 자식 클래스가 된다.

// CellPhone.java
public class CellPhone {
    // 필드
    String model;
    String color;

    // 생성자

    // 메소드
    void powerOn() { System.out.println("전원을 켭니다."); }
    void powerOff() { System.out.println("전원을 끕니다."); }
    void bell() { System.out.println("벨이 울립니다."); }
    void sendVoice(String message) { System.out.println("자기: " + message); }
    void receiveVoice(String message) { System.out.println("상대방: " + message); }
    void hangUp() { System.out.println("전화를 끊습니다."); }

}
// DmbCellPhone.java
public class DmbCellPhone extends CellPhone{
    // 필드
    int channel;

    // 생성자
    DmbCellPhone(String model, String color, int channel){
        this.model = model;     // CellPhone으로부터 상속받은 필드
        this.color = color;     // CellPhone으로부터 상속받은 필드
        this.channel = channel;
    }

    // 메소드
    void turnOnDmb(){
        System.out.println("채녈 " + channel + "번 DMB 방송 수신을 시작합니다.");
    }
    void changeChannelDmb(int channel){
        this.channel = channel;
        System.out.println("채널 " + channel + "번으로 바꿉니다.");
    }
    void turnOffDmb(){
        System.out.println("DMB 방송 수신을 멈춥니다.");
    }
}
// DmbCellPhoneExample.java
public class DmbCellPhoneExample {
    public static void main(String[] args){
        // DmbCellPhone 객체 생성
        DmbCellPhone dmbCellPhone = new DmbCellPhone("자바폰", "검정", 10);

        // CellPhone으로부터 상속받은 필드
        System.out.println("모델: " + dmbCellPhone.model);
        System.out.println("색상: " + dmbCellPhone.color);

        // DmbCellPhone의 필드
        System.out.println("채널: " + dmbCellPhone.channel);

        // CellPhone으로부터 상속받은 메소드 호출
        dmbCellPhone.powerOn();
        dmbCellPhone.bell();
        dmbCellPhone.sendVoice("여보세요");
        dmbCellPhone.receiveVoice("안녕하세요! 저는 홍길동인데요");
        dmbCellPhone.sendVoice("아~ 예 반갑습니다.");
        dmbCellPhone.hangUp();

        // DmbCellPhone의 메소드 호출
        dmbCellPhone.turnOnDmb();
        dmbCellPhone.changeChannelDmb(12);
        dmbCellPhone.turnOffDmb();

    }
}

실행 결과


부모 생성자 호출

현실에서 부모 없는 자식이 있을 수 없듯이 자바에서도 자식 객체를 생성하면, 부모 객체가 먼저 생성되고 자식 객체가 그 다음에 생성된다.

아래 코드는 DmbCellPhone 객체만 생성하는 것처럼 보이지만, 사실은 내부적으로 부모인 CellPhone 객체가 먼저 생성되고, DmbCellPhone 객체가 생성된다.

DmbCellPhone dmbCellPhone = new DmbCellPhone();

모든 객체는 클래스의 생성자를 호출해야만 생성된다.
부모 객체도 예외는 아니다.

그렇다면 부모 객체를 생성하기 위해 부모 생성자를 어디서 호출한 것일까?

부모 생성자는 자식 생성자의 맨 첫 줄에서 호출된다.

예를 들어 DmbCellPhone의 생성자가 명시적으로 선언되지 않았따면 컴파일러는 다음과 같은 기본 생성자를 생성해 낸다.

public DmbCellPhone(){
    super();
}

첫 줄에 super(); 가 추가된 것을 볼 수 있다.
super()는 부모의 기본 생성자를 호출한다.
즉 CellPhone 클래스의 다음 생성자를 호출한다.

public CellPhone(){
}

CellPhone.java 소스 코드에서도 CellPhone의 생성자가 선언되지 않았지만 컴파일러에 의해 기본 생성자가 만들어지므로 문제없이 실행된다.
만약 직접 자식 생성자를 선언하고 명시적으로 부모 생성자를 호출하고 싶다면 다음과 같이 작성하면 된다.

자식클래스( 매개변수선언, ... ) {
    super( 매개값, ...);
    ...
} 

super(매개값, …)는 매개값의 타입과 일치하는 부모 생성자를 호출한다.
만약 매개값의 타입과 일치하는 부모 생성자가 없을 경우 컴파일 오류가 발생한다.
super(매개값, …)가 생략되면 컴파일러에 의해 super()가 자동적으로 추가되기 때문에 부모의 기본 생성자가 존재해야 한다.

부모 클래스에 기본 생성자가 없고 매개 변수가 있는 생성자만 있다면 자식 생성자에서 반드시 부모 생성자 호출을 위해 super(매개값, …)를 명시적으로 호출해야 한다. super(매개값, …)는 반드시 자식 생성자 첫 줄에 위치해야 한다.
그렇지 않으면 컴파일 에러가 발생한다.
다음 예제를 통해 이해를 돕는다.

// People.java
public class People {
    public String name;
    public String ssn;

    public People(String name, String ssn){
        this.name = name;
        this.ssn = ssn;
    }
}

People 클래스는 기본 생성자가 없고 name과 ssn을 매개값으로 받아 객체를 생성시키는 생성자만 있다.
그렇기 때문에 People을 상속하는 Student 클래스는 생성자에서 super(name, ssn)으로 People 클래스의 생성자를 호출해야 한다.

// Student.java
        public class Student extends People{
            public int studentNo;

            public Student(String name, String ssn, int studentNo){
                super(name, ssn);               // 부모 생성자 호출
        this.studentNo = studentNo;
    }
}

Student 클래스의 생성자는 name, ssn, studentNo를 매개값으로 받아서 name과 ssn은 다시 부모 생성자를 호출하기 위해 매개값으로 넘겨준다.
super(name, ssn)은 People 생성자인 People(String name, String ssn)을 호출한다.
super(name, ssn)을 주석처리하면 컴파일 에러가 발생한다.
이 때 발생하는 에러는 “Implicit super constructor People() is undefined. Must explicitly invoke another constructor” 로서, 부모의 기본 생성자가 없으니 다른 생성자를 명시적으로 호출하라는 내용을 담고 있다.

// StudentExample.java
public class StudentExample{
    public static void main(String[] args){
        Student student = new Student("홍길동", "123456-1234567", 1);
        System.out.println("name : " + student.name);           // 부모에서 물려받은 필드 출력
        System.out.println("ssn : " + student.ssn);             // 부모에서 물려받은 필드 출력
        System.out.println("studentNo : " + student.studentNo);
    }
}

실행 결과


메소드 재정의

부모 클래스의 모든 메소드가 자식 클래스에 맞게 설계되어 있다면 가장 이상적인 상속이지만, 어떤 메소드는 자식 클래스가 사용하기에 적합하지 않을 수도 있다.
이 경우 상속된 일부 메소드는 자식 클래스에서 다시 수정해서 사용해야 한다.

자바는 이런 경우를 위해 메소드 오버라이딩(Overriding) 기능을 지원한다.

메소드 제정의(@Override)

메소드 오버라이딩은 상속된 메소드의 내용이 자식 클래스에 맞지 않을 경우, 자식 클래스에서 동일한 메소드를 재정의하는 것을 말한다.
메소드가 오버라이딩되었다면 부모 객체의 메소드는 숨겨지기 때문에, 자식 객체에서 메소드를 호출하면 오버라이딩된 자식 메소드가 호출된다.

메소드를 오버라이딩할 때는 다음과 같은 규칙에 주의해서 작성해야 한다.

  • 부모의 메소드와 동일한 시그니처(리턴 타입, 메소드 이름, 매개 변수 리스트)를 가져야 한다.
  • 접근 제한을 더 강하게 오버라이딩할 수 없다.
  • 새로운 예외(Exception)을 throws할 수 없다.

접근 제한을 더 강하게 오버라이딩 할 수 없다는 것은 부모 메소드가 public 접근 제한을 가지고 있을 때 오버라이딩 하는 자식 메소드는 default나 private 접근 제한으로 수정할 수 없다는 뜻이다.

반대는 가능하다.
부모 메소드가 default 접근 제한을 가지면 재정의되는 자식 메소드는 default 또는 public 접근 제한을 가질 수 있다.

다음 예제를 통해 이해를 돕는다.

// Calculator.java
public class Calculator {
    double areaCircle(double r){
        System.out.println("Calculator 객체의 areaCircle() 실행");
        return 3.14159 * r * r;
    }
}
// Computer.java
public class Computer extends Calculator{
    @Override
    double areaCircle(double r){
        System.out.println("Computer 객체의 areaCircle() 실행");
        return Math.PI * r * r;
    }
}

Calculator의 areaCircle() 메소드는 파이의 값을 3.14159로 계산했지만, 좀 더 정밀한 계산을 위해 Computer의 areaCircle() 메소드는 Math.PI 상수를 이용한다.
@Override 어노테이션은 생략해도 좋으나, 이것을 붙여주게 되면 areaCircle() 메소드가 정확히 오버라이딩된 것인지 컴파일러가 체크하기 때문에 개발자의 실수를 줄여준다.

// ComputerExample.java
public class ComputerExample {
    public static void main(String[] args){
        int r = 10;

        Calculator calculator = new Calculator();
        System.out.println("원면적: " + calculator.areaCircle(r));
        System.out.println();

        Computer computer = new Computer();
        System.out.println("원면적 : " + computer.areaCircle(r));      // 재정의된 메소드 호출
    }
}

실행 결과

부모 메소드 호출(super)

자식 클래스에서 부모 클래스의 메소드를 오버라이딩하게 되면, 부모 클래스의 메소드는 숨겨지고 오버라이딩된 자식 메소드만 사용된다.
그러나 자식 클래스 내부에서 오버라이딩된 부모 클래스의 메소드를 호출해야 하는 상황이 발생한다면 명시적으로 super 키워드를 붙여서 부모 메소드를 호출할 수 있다.
super는 부모 객체를 참조하고 있기 때문에 부모 메소드에 직접 접근할 수 있다.

super.부모메소드();

Airplane 클래스를 상속해서 SupersonicAirplane 클래스를 만들어 보자.
Airplane의 fly() 메소드는 일반 비행이지만 SupersonicAirplane의 fly() 는 초음속 비행 모드와 일반 비행 모드 두 가지로 동작하도록 설계한다.

// Airplane.java
public class Airplane {
    public void land(){
        System.out.println("착륙합니다.");
    }
    public void fly(){
        System.out.println("일반비행합니다.");
    }
    public void takeOff(){
        System.out.println("이륙합니다.");
    }
}
// SupersonicAirplane.java
public class SupersonicAirplane extends Airplane {
    public static final int NORMAL = 1;
    public static final int SUPERSONIC = 2;

    public int flyMode = NORMAL;

    @Override
    public void fly() {
        if(flyMode == SUPERSONIC){
            System.out.println("초음속비행합니다.");
        } else{
            // Airplane 객체의 fly() 메소드 호출
            super.fly();
        }
    }
}

자주 사용되는 고정값들은 상수를 사용함으로써 가독성을 높여준다.

// SupersonicAirplaneExample.java
public class SupersonicAirplaneExample {
    public static void main(String[] args){
        SupersonicAirplane sa = new SupersonicAirplane();
        sa.takeOff();
        sa.fly();
        sa.flyMode = SupersonicAirplane.SUPERSONIC;
        sa.fly();
        sa.flyMode = SupersonicAirplane.NORMAL;
        sa.fly();
        sa.land();
    }
}

실행 결과


final 클래스와 final 메소드


final 키워드는 클래스, 필드, 메소드 선언 시에 사용할 수 있다.
final 키워드는 해당 선언이 최종 상태이고, 결코 수정될 수 없음을 뜻한다.
final 키워드가 클래스, 필드, 메소드 선언에 사용될 경우 해석이 조금씩 달라진다.

필드 선언 시에 final이 지정되면 초기값 설정 후, 더 이상 값을 변경할 수 없다는 것을 알았다.
그렇다면 클래스와 메소드에 final이 지정되면 어떤 효과가 날까?
클래스와 메소드 선언 시에 final 키워드가 지정되면 상속과 관련이 있다.

상속할 수 없는 final 클래스

클래스를 선언할 때 final 키워드를 class 앞에 붙이게 되면 이 클래스는 최종적인 클래스이므로 상속할 수 없는 클래스가 된다.

즉, final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없다는 것이다.

public final class 클래스 { ... }

final 클래스의 대표적인 예는 자바 표준 API에서 제공하는 String 클래스이다.
String 클래스는 다음과 같이 선언되어 있다.

public final class String {..}

그래서 다음과 같이 자식 클래스를 만들 수 없다.

public class NewString extends String {...}     // 오류 발생

다음 예시를 통해 final 클래스의 상속 불가 이해를 돕는다.

// Member.java
public final class Member{
}
// VeryVeryImportantPerson.java
// Member를 상속할 수 없음
public class VeryVeryImportantPerson extends Member {       // 오류 발생
}

오버라이딩할 수 없는 final 메소드

메소드를 선언할 때 final 키워드를 붙이게 되면 이 메소드는 최종적인 메소드이므로 오버라이딩(Overriding)할 수 없는 메소드가 된다.
즉, 부모 클래스를 상속해서 자식 클래스를 선언할 때 부모 클래스에 선언된 final 메소드는 자식 클래스에서 재정의할 수 없다는 것이다.

public final 리턴타입 메소드( [매개변수, ...] ) {...}

다음 예제를 통해 메소드 오버라이딩 불가 이해를 돕는다.

// Car.java
public class Car {
    // 필드
    public int speed;

    // 메소드
    public void speedUp() { speed += 1; }

    // final 메소드
    public final void stop(){
        System.out.println("차를 멈춤");
        speed = 0;
    }
}
// SportsCar.java
public class SportsCar extends Car {
    @Override
    public void speedUp() { speed += 10; }

    // 오버라이딩을 할 수 없음
    @Override
    public void stop(){
        System.out.println("스포츠카를 멈춤");
        speed = 0;
    }
}

상속의 개념, 클래스 상속, 부모 생성자 호출, 메소드 재정의, final 클래스와 final 메소드에 대해 정리해보았다.
final 키워드가 static이랑 항상 헷갈렸는데 확실히 정리된 것 같아 기쁘다.
확실히 초반보다는 이해하는 시간과 포스팅 시간이 길어지기는 했지만 시간이 많으므로 차분히 정리한다…

Leave a comment