Java 재활 훈련 4일차 - 상속

0

java

목록 보기
4/18

상속

상속(inheritance)는 부모가 자식에게 물려주는 행위를 말한다. 객체 지향 프로그래밍에서는 부모 클래스의 필드와 메서드를 자식 클래스에게 물려줄 수 있다.

-------parent------
| field1, method1 |
-------------------
        ^
        |
        |
        |
------child--------
| field1, method1,|
| field2, method2 |
-------------------

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

상속 방법은 다음과 같다.

public class Parents {
    String field1;
    void method1() { }
}

public class Child extends Parents {
    String field2;
    void method2() { }
}

잘보면 자식이 부모 클래스 이름으로 extends를 사용하고 있는 것을 볼 수 있다. 이는 자식이 상속을 결정한다는 의미로 현실 세계의 논리와는 약간 다르다. 자식이 상속을 결정하므로 위의 그림에서 화살표의 방향이 자식 -> 부모 방향으로 가는 것이다.

python, c++과 달리 자바는 다중 상속을 지원하지 않는다.

자식이 부모 객체를 상속한 상태에서 자식 객체를 생성하면 메모리에 다음과 같이 생성된 것이다.

---------heap----------
| ----Parent 객체----- |
| | field1, method1 | |
| ------------------- |
|         ^           |
|         |           |
|         |           |
| -----Child 객체----- |
| | field2, method2 | |
| ------------------- |
-----------------------

Child c = new Child();를 실행하면 Child 객체 뿐만 아니라, Parent 객체도 생성하여 지칭하고 있다는 것이다.

따라서, 자식 객체가 생성될 때에 부모 객체도 생성되어야 한다는 것인데, 부모 객체가 생성된다는 것은 부모 객체의 생성자도 어딘가에서는 실행이 되어야 한다는 것이다. 이는 사실, 컴파일 단계에서 자식 클래스인 Child의 생성자에 super()라는 코드를 추가하여 부모 객체의 생성자를 호출하는 것이다.

  • Phone.java
public class Phone {
    public String model;
    public String color;

    public Phone() {
        System.out.println("Phone 생성자 호출");
    }
}
  • SmartPhone.java
public class SmartPhone extends Phone{
    public SmartPhone(String model, String color) {
        // super() <-- compile 단계에 추가된다.
        this.model = model;
        this.color = color;
        System.out.println("SmartPhone 생성자 호출");
    }
}

결과는 다음과 같다.

Phone 생성자 호출
SmartPhone 생성자 호출

자식 객체인 SmartPhone를 생성하는 생성자에서 compiler가 자동으로 super() code를 넣어 실행하는 것이다. 따라서, 부모 객체인 Phone의 생성자가 먼저 실행되는 것이다.

super()를 명시적으로 적어주어 compiler가 자동으로 super()를 추가시켜주지 않게 만들 수도 있다. 이는 부모 객체의 생성자가 단순히 super()로 실행되는 기본 생성자가 아닌 경우이다.

  • Phone.java
public class Phone {
    public String model;
    public String color;

    public Phone(String model, String color) {
        this.model = model;
        this.color = color;
        System.out.println("Phone 생성자 호출");
    }
}

부모 객체인 Phone 클래스의 생성자가 Phone(String model, String color)으로 기본 생성자가 아닌 매개변수가 있는 생성자이다. 이러한 경우 super()만으로는 해당 생성자가 호출되지 않기 때문에, 자식 객체인 SmartPhone에서 이를 명시적으로 호출해주어야 한다. 만약 호출해주지 않으면 부모 클래스인 Phone 객체의 생성자로 무엇을 실행시켜줄 지 모르므로 compile 단계에서 에러가 발생한다.

Phone class 코드를 위와 같이 바꾸고 SmartPhone class로 돌아가보면 error가 생길 것이다.

There is no parameterless constructor available in 'Phone'

부모 클래스인 Phone의 생성자를 명시적으로 적어달라는 이야기이다.

  • SmartPhone.java
public class SmartPhone extends Phone{
    public SmartPhone(String model, String color) {
        super(model, color);
        System.out.println("SmartPhone 생성자 호출");
    }
}

다음과 같이 SmartPhone 클래스의 생성자에 부모 클래스인 Phone 생성자를 super로 명시적으로 적어주어야 하는 것이다.

메서드 재정의

부모 클래스로부터 부여받은 메서드를 자식 클래스에서 다시 정의하는 것을 '메서드 오버라이드'라고 한다.

'메서드 오버라이드'는 상속된 메서드를 자식 클래스에서 재정의하는 것을 말한다. 메서드 오버라이딩되었다면 부모 메서드는 숨겨지고, 자식 메서드가 우선적으로 사용된다. 단, 메서드 오버라이드를 위해서는 다음의 조건을 만족해야한다.

  1. 부모 메서드의 선언부(리턴 타입, 메서드 이름, 매개변수)와 동일해야 한다.
  2. 접근 제한을 더 강하게 오버라이딩 할 수 없다. public -> priavte 불가
  3. 새로운 예외를 throw할 수 없다.

다음의 예제를 보도록 하자. 부모 객체인 Calculator를 상속 받은 ComputerareaCircle 메서드를 오바리이딩하여 더 정확한 원의 넓이를 계산하고 있다.

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

참고로 @Override annotation은 생략이 가능하다. 단지, 해당 어노테이션이 있으면 컴파일 단계에서 정확히 오버라이딩 되었는지 체크하고 문제가 있다면 컴파일 에러를 출력한다.

  • Main.java
public class Main {
    public static void main(String[] args) {
        int r = 10;
        Calculator calculator = new Calculator();
        System.out.println(calculator.areaCircle(r));

        Computer computer = new Computer();
        System.out.println(computer.areaCircle(r));
    }
}

결과는 다음과 같다.

areaCircle 객체의 areaCircle 실행
314.0
Computer 객체의 areaCircle 실행
314.1592653589793

오버라이딩에 성공한 것을 볼 수 있다.

자식 클래스에서 부모 클래스의 메서드를 오버라이딩하면 부모 클래스의 메서드가 감춰진다. 만약, 부모 클래스의 메서드를 자식 클래스에서 호출하고 싶다면 super.method를 사용하면 된다.

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

super.areaCircle(r);를 통해서 부모 객체의 areaCircle를 호출한 것을 볼 수 있다.

final 클래스와 final 메서드

final 클래스와 final 메서드는 상속과 관련이 있다.

클래스를 선언할 때 final키워드를 붙이면 최종적인 클래스이므로 더 이상 상속할 수 없는 클래스가 된다. 즉 final 클래스는 부모 클래스가 될 수 없다.

public final class 클래스 {}

대표적인 예가 String 클래스이다. String 클래스는 다음과 같이 선언되어 있다.

public final class String {}

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

public class NewString extends String {}

final 메서드는 이 메서드에 대해서 오버라이딩 할 수 없다는 것을 나타낸다. 즉, 부모 클래스를 상속해서 자식 클래스를 선언할 때, 부모 클래스에 선언된 final 메서드는 자식 클래스에서 재정의될 수 없다.

public final 리턴타입 메서드(매개변수) {}

다음 예제는 Car 클래스의 stop 메서드를 final로 선언했기 때문에 자식 클래스인 SportsCar에서 stop 메서드를 오버라이딩 할 수 없다.

  • Car.java
public class Car {
    public int speed;

    public void speedUp() {
        speed += 1;
    }

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

다음의 에러가 발생한다.

stop()' cannot override 'stop()' in 'Car'; overridden method is final

final로 선언된 메서드를 오버라이딩 할 수 없다는 것이다.

protected 접근 제한자

protected 접근 제한자는 상송과 관련이 있다. publicdefault 접근 제한자 중간쯤이라고 생각하면 된다.

접근 제한자제한 대상제한 범위
protected필드, 생성자, 메서드같은 패키지이거나, 자식 객체만 사용 가능

protected는 같은 패키지에서는 default처럼 접근이 가능하나, 다른 패키지에서는 자식 클래스만 접근을 허용한다.

package1를 만들고, A.java, B.java를 만들자.

  • package1/A.java
package package1;

public class A {
    protected String field;

    protected A() {}

    protected void method() {}
}

protected는 같은 패키지에서는 default와 같은 접근 제한이 가능하다고 했다. 따라서, 같은 패키지에서는 접근이 가능하다.

  • package1/B.java
package package1;

public class B {
    public void method() {
        A a = new A();
        a.field = "value";
        a.method();
    }
}

B 클래스는 A 클래스와 같은 패키지이기 때문에 A 클래스의 protected 생성자, 필드, 메서드에 모두 접근이 가능하다.

package2 패키지를 만들고 C.java 파일에 다음의 코드르 넣어보도록 하자.

  • package2/C.java
package package2;

public class C {
    public void method() {
        A a = new A(); // error
        a.field = "value"; // error
        a.method(); // error
    }
}

A 클래스를 가져올 수는 있지만 protected로 된 생성자를 호출할 수 없어 객체를 생성할 수도 없으며 field, method 모두 접근이 불가능하다. 이는 protected 접근 제한자가 default처럼 동작하여 다른 패키지에서 사용할 수 없도록 하기 때문이다.

package2에 D.java 파일을 만들고 A 클래스를 상속해보도록 하자.

  • package2/D.java
package package2;

import package1.A;

public class D extends A {
    public D() {
        super(); // protected 생성자 호출 가능
    }
    public void method1() {
        this.field = "value"; // protected field 접근 가능
        this.method(); // protected method 접근 가능
    }

    public void method2() {
        A a = new A(); // 상속이 아닌 직접 접근은 불가
        a.field = "value"; // 상속이 아닌 직접 접근은 불가
        a.method(); // 상속이 아닌 직접 접근은 불가
    }
}

다른 패키지이지만 상속한 다음 protected로 제한된 생성자, field, method에 접근이 가능한 것을 볼 수 있다. 단 이는 superthis를 통해서 상속받은 부모 객체에 접근할 때만 가능한 것이지, new A();와 같이 직접 접근하는 것은 불가능하다.

타입 변환

기본 타입 간의 변환이 가능한 것처럼 클래스도 클래스 타입 변환이 가능한데, 이는 상속 관계에 있는 클래스 사이에서 발생한다.

자동 타입 변환은 다음과 같은 조건에서 발생한다.

부모타입 변수 = 자식타입객체;

자식 객체는 부모 객체의 모든 특성을 상속 받았기 때문에 자식 객체는 부모 타입 변수에 들어갈 수 있다. 가령, 동물과 고양이 관계에서 '고양이는 동물이다'가 가능하다.

class Animal {
    
}

class Cat extends Animal {
    
}

CatAnimal 클래스를 상속하므로 Animal이 가진 field, method들을 모두 가지고 있다. 따라서, 다음과 같이 Animal 변수에 Cat 객체가 들어갈 수 있다.

Cat cat = new Cat();
Animal animal = cat;

메모리 상태를 표현하면 다음과 같다. catanimal 변수는 타입만 다를 뿐 동일한 Cat객체를 참조한다. 단지, animal 변수는 Cat 객체만의 field, method를 실행할 버튼이 없는 것일 뿐이다.

              --Animal--
              |        |
              ----------
                  ^
                  |
                  |
              ---Cat---
cat --------> |       |
animal -----> |       |
              ---------

상속 계층이 좀 더 깊은 경우, 바로 위의 부모가 아니라도 상속 계층에서 상위 타입이라면 자동 타입 변환이 일어날 수 있다.

    A
  /   \
 B     C
 |     |
 D     E

다음의 구조를 코드로 표현하면 다음과 같다.

B b = new B();
C c = new C();
D d = new D();
E e = new E();

A a1 = b; // ok
A a2 = c; // ok
A a3 = d; // ok
A a4 = e; // ok

단, 다음은 불가능하다.

B b1 = e; // 불가
C c1 = d; // 불가

이들은 상속 관계가 없기 때문이다.

부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에서 선언된 필드와 메서드만 접근이 가능하다. 이는 부모 타입 변수의 버튼이 자식 클래스의 것들을 모르기 때문이다. 따라서, 부모 타입 변수가 자식 객체를 참조하고 있지만 접근 가능한 맴버와 메서드는 부모 클래스로 한정된다.

그러나, 자식 클래스에서 오버라이딩된 메서드가 있다면 부모 메서드 대신 오버라이딩된 메서드가 호출된다. 이는 다형성과 관련 있기 때문에 잘 알아두어야 한다.

  • Parent.java
public class Parent {
    public void method1() {
        System.out.println("Parent-method1");
    }

    public void method2() {
        System.out.println("Parent-method2");
    }
}

Parent 클래스를 상속하고 method2를 오버라이딩하는 Child 클래스를 만들어보자.

  • Child.java
public class Child extends Parent {
    @Override
    public void method2() {
        System.out.println("Child-method2");
    }

    public void method3() {
        System.out.println("Child-method3");
    }
}

method2를 오버라이딩한 것을 볼 수 있다. method3Child 클래스에서만 사용할 수 있다.

  • Main.java
public class Main {
    public static void main(String[] args) {
        Child child = new Child();

        Parent parent = child;
        parent.method1(); // Parent-method1
        parent.method2(); // Child-method2
        // parent.method3()
    }
}

method1는 오버라이딩되지 않았으므로 부모 클래스에서의 코드와 동일한 결과가 실행되었지만, method2는 자식 클래스에서 오버라이딩되었으므로 자식 객체의 메서드가 호출되는 것이다.

자식 타입은 부모 타입으로 자동 변환되지만, 반대로 부모 타입은 자식 타입으로 자동 변환되지 않는다. 이는 부모 타입이 자식 타입의 특질(field, method)이 없기 때문이다.

그런데, '강제 타입 변환'을 사용하여 부모 타입 객체를 자식 타입 객체로 바꿀 수 있다.

자식타입 변수 = (자식타입) 부모타입객체;

그러나, '강제 타입 변환'을 사용한다고 해서 무조건 강제 타입 변환을 시킬 수 있는 것은 아니다. 이것이 가능한 경우는 자식 객체가 부모 타입으로 자동 변환된 후에 다시 자식 타입으로 변환해야할 때 '강제 타입 변환'을 사용할 수 있는 것이다.

Parent parent = new Child(); // 자동 타입 변환
Child child = (Child) parent; // 강제 타입 변환

즉 부모타입객체가 사실은 자식 객체를 가리키고 있을 때만 가능하다는 것이다.

  • Parent.java
public class Parent {
    public String field1;

    public void method1() {
        System.out.println("Parent-method1");
    }

    public void method2() {
        System.out.println("Parent-method2");
    }
}
  • Child.java
public class Child extends Parent {
    public String field2;
    
    public void method3() {
        System.out.println("Child-method3");
    }
}
  • Main.java
public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        // 자동 타입 변환
        Parent parent = child;

        parent.field1 = "data";
        parent.method1();
        parent.method2();
        
        /*
        parent.field2 = "data2"; // 불가능
        parent.method3(); // 불가능
        * */
        
        // 강제 타입 변환
        Child child1 = (Child) parent;
        child1.field2 = "data2";
        child1.method3();

    }
}

위의 예시를 보면 child라는 Child 객체를 만들고 Parent 클래스를 가진 parent에 넣어주어 자동 타입 변환이 발생하도록 하였다.

이후에 Child 클래스 타입을 가진 child1 변수를 만들고, parent 변수를 강제 타입 변환해주어 child 객체를 전달해준 것이다.

다형성

다형성은 사용 방법은 동일하지만, 실행 결과가 다양하게 나오는 성질을 말한다. 이는 객체의 사용방법(method)가 동일한데, 그 내부의 객체가 무엇이냐에 따라 결과가 다르게 나오는 것을 말한다.

이러한 특성을 가능하게 해주는 것이 바로 '메서드 오버라이딩'이다. 부모 클래스를 상속받은 자식 클래스에서 부모 클래스 메서드를 오버라이딩 한다음에 부모 클래스 변수 타입에 넣어주면, 부모 클래스 변수 타입은 내부적으로 메서드 오버라이딩을 완료한 자식 객체를 참조하므로, 자식 객체의 오버라이딩된 메서드를 호출하게 된다.

따라서, 다형성이라는 것은 '자동 타입 변환' + '메서드 오버라이딩' 으로 구성되었다고 할 수 있다.

다형성 = 자동 타입 변환 + 메서드 오버라이딩

이는 메서드 뿐만 아니라 field 역시도 동일한데, 부모 타입에서 호출하고 있는 field는 동일하지만 자식 객체가 내부적으로 어떤 것이냐에 따라 다른 값이 들어갈 수 있다.

  • Car.java
public class Car {
    public Tire tire;

    public void run() {
        tire.roll();
    }
}

Car 클레스는 field로 어떤 tire 객체를 가지고 있냐에 따라서 run의 결과가 달라게 된다. 이것이 바로 field 다형성이다. 예제를 통해서 자세히 확인해보도록 하자.

  • Tire.java
public class Tire {
    public void roll() {
        System.out.println("회전");
    }
}
  • BasicTire.java
public class BasicTire extends Tire{
    @Override
    public void roll() {
        System.out.println("basic tire");
    }
}
  • SnowTire.java
public class SnowTire extends Tire{
    @Override
    public void roll() {
        System.out.println("snow tire");
    }
}
  • Main.java
public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();

        myCar.tire = new Tire();
        myCar.run();

        myCar.tire = new BasicTire();
        myCar.run();

        myCar.tire = new SnowTire();
        myCar.run();
    }
}

myCar 객체의 tire field가 어떤 tire 객체이냐에 따라서 결과가 달라지는 것을 볼 수 있다. 반면에 myCar에서 사용하고 있는 tirerun은 달라진 결과에 달리 동일한 사용방법을 쓰고 있다. 이것이 바로 다형성이다.

다음과 같이 field말고도 매개변수를 통해서도 다형성을 쓸 수 있다. 메서드를 호출할 때 특정 매개변수의 타입이 클래스라면 해당 클래스의 자식 클래스를 매개변수로 넘겨 동일한 메서드를 실행해도 다른 결과를 내도록 할 수 있다.

  • Driver.java
public class Driver {
    public void drive(Vehicle vehicle) {
        vehicle.run();
    }
}

Driver의 매개변수로 Vehicle 클래스 타입의 객체가 필요한데, Vehicle뿐만 아니라, Vehicle을 상속한 자식 객체들도 넘겨줄 수 있다. 가령 Vehicle을 상속한 Bus라는 클래스를 만들어보자.

  • Vehicle.java
public class Vehicle {
    public void run() {
        System.out.println("차량이 달린다.");
    }
}
  • Bus.java
public class Bus extends Vehicle {
    public void run() {
        System.out.println("Bus가 달린다.");
    }
}

Bus 클래스는 Vehicle을 상속했기 때문에 Vehicle 타입의 변수에 자동 타입 변환이 가능하다.

  • Main.java
public class Main {
    public static void main(String[] args) {
        Driver driver = new Driver();

        Bus bus = new Bus();
        driver.drive(bus); // Bus가 달린다.
    }
}

객체 타입 확인

다형성을 활용하다보면 실제 변수에서 사용하고 있는 객체의 타입이 무엇인지 알고 싶을 때가 있다. 이때 사용하는 것이 instanceof 이다.

boolean res = 객체 instanceof 타입;

결과가 restrue or false로 나온다. 참고로, 부모, 자식 상속관계와 상관없이 딱 객체가 타입이랑 일치하는 지 본다. 즉, 부모 타입 변수가 자식 객체를 참조하고 있다면, 내부의 자식 객체의 타입만이 true로 나온다.

가장 많이 사용되는 곳이 강제 타입 변환하기 전에 사용되는데, '변수'가 Child 타입인지 여부를 instanceof로 확인하여 강제 타입 변환을 실행하는 것이다.

public void method(Parent parent) {
    if (parent instanceof Child) {
        Child child = (Child) parent;
        child.method3(); // Child 만의 메서드
    }
}

위의 코드가 계속해서 관행으로 쓰여서, java 12부터는 instanceof연산의 결과가 true일 경우 다음과 같이 간단히 child 객체를 사용할 수 있게 되었다.

if (parent instanceof Child child) {
    child.method3(); //Child 만의 메서드
}

맨 우측에 if문 블럭 안에 쓸 child 변수를 선언하는 것이다. 단, true일 때만 쓸 수 있다.

추상 클래스

'추상(abstract)'은 실체 간에 공통되는 특성을 추출한 것을 말한다. 가령, 새, 곤충, 물고기 등의 공통점은 동물이라는 것이다. 동물은 실체들의 공통되는 특성을 가지고 있는 추상적인 것이라고 볼 수 있다.

객체를 생성할 수 있는 클래스를 '실체 클래스'라고 한다. 이 클래스들의 공통적인 필드나 메서드를 추출해서 선언한 클래스를 '추상 클래스'라고 한다. '추상 클래스'는 '실체 클래스'의 부모 역할을 한다. 따라서, '실체 클래스'는 '추상 클래스'를 상속해서 공통적인 필드나 메서드를 물려받을 수 있다.

          Animal.class (추상 클래스)
              ^ (상속)
              |
  -----------------------------
  |           |               |
Bird.class  Insect.class  Fish.class (실체 클래스)

추상 클래스는 실체 클래스의 공통되는 필드와 메서드를 추출해서 만들었기 때문에 new 연산자를 사용해서 객체를 직접 생성할 수 없다.

Animal animal = new Animal(); // error

따라서, 추상 클래스는 실체 클래스를 만들기 위한 부모 클래스로만 사용된다. 즉, 추상 클래스는 extends 뒤에만 올 수 있다.

class Fish extends Animal {
    ...
}

추상 클래스 선언은 클래스 선언에 abstract를 붙이면 된다.

public abstract class 클래스명 {
    //field
    //생성자
    //method
}

추상 클래스도 메서드, 필드를 선언할 수 있다. 그리고 자식 객체가 생성될 때 super를 통해서 추상 클래스의 생성자가 호출되기 때문에 생성자도 반드시 있어야 한다. 다음의 예시를 보도록 하자.

  • Phone.java
public abstract class Phone {
    String owner;

    Phone(String owner) {
        this.owner = owner;
    }

    void turnOn() {
        System.out.println("ON");
    }

    void turnOff() {
        System.out.println("OFF");
    }
}

Phone은 공통된 필드와 메서드를 정의해두었다. 자식 클래스는 추상 클래스인 Phone을 상속하여 공통된 필드와 메서드를 사용할 수 있다.

  • SmartPhone.java
public class SmartPhone extends Phone{
    public SmartPhone(String owner) {
        super(owner);
    }

    void internetSearch() {
        System.out.println("Internet");
    }
}

Phone을 상속받고 super를 통해서 Phone 추상 클래스의 생성자를 호출하고 있다.

public class Main {
    public static void main(String[] args) {
        // Phone phone = new Phone(); // 불가
        SmartPhone smartPhone = new SmartPhone("홍길동");

        smartPhone.turnOn(); // ON
        smartPhone.internetSearch(); // Internet
        smartPhone.turnOff(); // OFF
    }
}

추상 클래스 Phone의 field와 method에 접근이 가능한 것을 볼 수 있다.

추상 클래스는 공통된 부분들만 모아놓은 특수한 클래스라고 했다. 이러한 추상 클래스는 상속으로만 사용할 수 있는데, 일부 추상 클래스의 메서드들은 자식 클래스에서 상속하여 오버라이딩을 해야만 하는 경우도 있다. 이는 공통된 특징 중 몇몇 부분들은 각 자식마다 다르게 실행될 수 있는 부분들이 있기 때문이다.

가령, 동물은 소리를 내지만, 소리를 내는 방식과 소리는 동물마다 다 다르다. 따라서, 동물이 소리를 낸다는 메서드는 있지만, 그 구현은 자식에게 맡기는 것이다. 이렇게 자식이 공통된 특질을 구체적으로 구현하도록 강제하는 것이 바로 '추상 메서드'이다.

추상 메서드는 추상 클래스에서만 사용할 수 있고, 자식에게서 오버라이딩을 강제한다. 다음과 같이 일반적인 메서드에 abstract를 붙이기만 하면 된다.

abstract 리턴타입 메서드명(매개변수, ...);

추상 메서드는 자식 클래스의 공통 메서드라는 것만 정의할 뿐, 실행 내용을 가지지 않는다(실행 블럭이 없다.). 다음은 Animal 추상 클래스에서 sound 추상 메서드를 선언한 것이다.

public abstract class Animal {
    abstract void sound();
}

세부 메서드의 내용은 추상 클래스를 상속한 자식 클래스에서 반드시 오버라이딩하여 재정의해야하는 것이다.

            Animal
      (abstract sound())
              ^
              |
              |
     ---------------------
     |                   |
   Dog                  Cat
 (sound()) 멍멍       (sound()) 야옹
  • Animal.java
public abstract class Animal {
    public void breathe() {
        System.out.println("숨을 쉰다.");
    }

    public abstract void sound();
}

추상 메서드인 sound는 반드시 자식 클래스에서 오버라이딩해야한다.

  • Dog.java
public class Dog extends Animal {
    @Override
    public void sound() { // 오버라이딩을 안하면 에러가 발생
        System.out.println("멍멍");
    }
}

추상 클래스인 Animal을 상속하고 추상 메서드인 sound을 오버라이딩했다. 참고로 sound 오버라이딩 안하면 에러가 발생한다.

  • Cat.java
public class Cat extends Animal {
    @Override
    public void sound() { // 오버라이딩을 안하면 에러가 발생
        System.out.println("야옹");
    }
}
  • Main.java
public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.sound(); // 멍멍

        Cat cat = new Cat();
        cat.sound(); // 야옹

        animalSound(new Dog()); // 숨을 쉰다. 멍멍
        animalSound(new Cat()); // 숨을 쉰다. 야옹
    }

    public static void animalSound(Animal animal) {
        animal.breathe();
        animal.sound();
    }
}

추상 클래스 덕분에 자식 클래스에서 공통된 특질인 breathe를 가지게 되었고, 추상 메서드인 sound를 강제로 자식 클래스가 오버라이딩하도록 하여 다형성을 구성할 수 있도록 해주었다.

봉인된(sealed) 클래스

기본적으로 final 클래스를 제외한 모든 클래스는 부모 클래스가 될 수 있다. 그러나 java 15부터는 무분별한 자식 클래스 생성을 방지하기 위해서 봉인된(sealed) 클래스가 도입되었다.

다음과 같이 Person 자식 클래스는 EmployeeManager만 가능하고, 그 이외는 자식 클래스가 될 수 없도록 Person을 봉인된 클래스로 선언할 수 있다.

public sealed class Person permits Employee, Manager {}

sealed 키워드를 사용하면 permits 키워드 위에 상속 가능한 자식 클래스들을 지정해야한다. 봉인된 Person 클래스를 상속하는 EmployeeManagerfinal또는 non-sealed 키워드로 다음과 같이 선언하거나, sealed 키워드를 사용해서 또 다른 봉인 클래스로 선언해야한다.

왜냐하면 기껏 Person을 봉인된 클래스로 만들어놨더니, EmployeeManager가 상속 가능하면 Person을 간접 상속하는 것과 동일하기 때문이다. 따라서 봉인된 클래스를 상속하는 자식 클래스들도 상속이 안되거나, 상속이 제한되거나 해야하는 것이다. 만약 이러한 제약조건을 풀고싶다면 non-sealed를 붙이는 것이다.

public final class Employee extends Person {}
public non-sealed class Manager extends Person {}

final로 된 Employee는 더이상 상속이 불가능하다. non-sealed로 된 Manager는 다음과 같이 자식 클래스를 만들 수 있다.

public class Director extends Manager {}

정리하면 봉인된 클래스를 상속하는 자식 클래스는 다음 3가지 중 하나를 해야한다.
1. 상속을 불가능하게 만들어 봉인된 클래스의 후손을 만들지 못하도록 하기: final
2. 상속을 제한하게 만들어 봉인된 클래스의 후손을 제한하기: sealed ~ permits
3. 자식 클래스에서는 봉인된 클래스의 제약을 해소하기: non-sealed

0개의 댓글