본문 바로가기
Study/Live-Study

[Live-Study] 상속

by 검프 2021. 2. 25.

목표

자바의 상속에 대해 학습하세요.

학습할 것 (필수)

  • 자바 상속의 특징
  • super 키워드
  • 메소드 오버라이딩
  • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
    • 더블 디스패치
  • 추상 클래스
  • final 키워드
  • Object 클래스

이제야 반을 온 것 같습니다. (사실 아직 반도 안왔지만)

자주쓰는 상속의 개념인데 제대로 한번 정리해봣나 싶긴 합니다 ㅎㅎ

이번 기회에 자바의 상속에 대해 한번 제대로 알아보겠습니다.


 

자바 상속의 특징

상속(Inheritance)

부모가 소유하고 있는 재산의 일부를 자식이 물려받는 것을 생각해보면, 부모가 가진 것을 자식이 마음대로 쓰거나 그대로 쓰거나 선택할 수 잇습니다.

즉, 자식 클래스가 부모 클래스의 변수와 메소드를 물려 받아 쓰는 것입니다.

상속의 장점

코드의 재사용을 통해 코드의 간결성을 확보할 수 있습니다.

상속의 단점

부모 클래스 기능에 버그가 생기거나 기능의 추가/변경 등으로 변화가 생겼을 때, 자식 클래스가 정상적으로 작동할 수 있을지에 대한 예측이 힘듭니다.

자식 클래스가 동작하기 위해선 부모 클래스에 의존할 수 밖에 없습니다.

즉, 부모 클래스와의 결합도가 높아집니다.(캡슐화 원칙에 위반합니다).

상속의 특징

자식 클래스는 단 하나의 부모 클래스로부터 상속 받을 수 있습니다. (다중 상속 금지)

인터페이스는 다중 상속 가능합니다. (헷갈려서 한번더 정의하고 가겠습니다.)

상속받는 자식 클래스가 다른 클래스의 부모 클래스가 될 수 있습니다.

런타임 때, 자식클래스의 오버라이딩 한 변수나 메소드가 우선적으로 사용됩니다.
접근제어자를 통해 상속의 범위를 조정할 수 있습니다.

사용법

# 나쁜 예
class 자식클래스 extends 부모클래스, 다른부모클래스 {
}

# 좋은 예
class 자식클래스 extends 부모클래스 {
}

 

Super 키워드

super 키워드는 부모 클래스의 생성자, 멤버 변수, 메소드에 접근할 수 있는 참조 변수 인데요.

간단히 말하면, 부모 클래스를 엑세스 할 수 있는 키워드입니다.

생성자에서 super 키워드

class Animal {
    public Animal() {
        System.out.println("나는 동물이야! ");
    }
}

class Dog extends Animal {
    public Dog() {
        System.out.println("(사실 조금더 자세히 말하면 멍멍이야.)");
    }
}

public class SuperKeyword {
    public static void main(String[] args) {
        Dog dog = new Dog();
    }
}

출력문은

나는 동물이야! 
(사실 조금더 자세히 말하면 멍멍이야.)

인데요.

그 이유는 생성자 안에 super() 가 자동적으로 실행되기 때문입니다.

즉, new Dog()에서 new 키워드는 Animal 클래스의 생성자를 먼저 호출하고 Dog 클래스의 생성자를 호출합니다.

아래와 같이 명시적으로 작성할 수 있습니다.

주의! 생성자의 첫쨰 줄에 작성되야 합니다!

package whiteship.week6;

class Animal {
    public Animal(String name) {
        System.out.println(String.format("나는 %s 이라는 동물이야! ", name));
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}

public class SuperKeyword {
    public static void main(String[] args) {
        Dog dog = new Dog("멍멍이");
    }
}

출력문은

나는 멍멍이 이라는 동물이야!

입니다.

부모의 멤버변수를 지정하는 super 키워드

class Animal {
    protected String name = "나는 동물이야!";
}

class Dog extends Animal {
    private String name = "나는 멍멍이야!";

    public void printMessage() {
        System.out.println(name);
        System.out.println(super.name);
    }
}

public class SuperKeyword {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.printMessage();
    }
}

출력문은

나는 멍멍이야!
나는 동물이야!

즉, 부모 클래스의 인스턴스 변수와 같은 타입, 변수명을 가진다면 기본적으로 자식 클래스가 우선순위를 가집니다. ( 위에서 설명한 런타임 때, 자식클래스의 오버라이딩 한 변수나 메소드가 우선적으로 사용됩니다. )

메소드에서 super 키워드

class Animal {
    protected String name = "나는 동물이야!";

    public void printMessage() {
        System.out.println(name);
    }
}

class Dog extends Animal {
    private String name = "나는 멍멍이야!";

    public Dog() {
        super.printMessage();
        printMessage();
    }

    @Override
    public void printMessage() {
        System.out.println(name);
    }
}

출력문은

나는 동물이야!
나는 멍멍이야!

이또한 멤버션수의 super와 같습니다.

 

메소드 오버라이딩

이부분은 [Live-study]클래스에서 다뤘던 부분으로 대체하겠습니다.

메소드 오버로딩(Method Overloading)

  • 컴파일 타임에 동작합니다. → 컴파일 타임 다형성
  • 반환 타입과 이름은 같게, 파라미터의 유형과 갯수를 다르게 하여 정의하여 사용합니다.
  • Static 메소드를 오버로딩 할 수 있습니다.
public class Calculator {
    public static void main(String args[]) {
        Calculator calculator = new Calculator();
        System.out.println("Add two numbers: " + calculator.add(20, 21));
        System.out.println("Add two long numbers: " + calculator.add(41L, 45L));
    }

    int add(int n1, int n2) {
        return n1 + n2;
    }

    int add(Long n1, Long n2) {
        return (int) (n1 + n2);
    }
}

Output:

Add two numbers: 41
Add two long numbers: 63

메소드 오버라이딩(Method OverrRiding)

  • 런타임에 동작합니다. → 런타임 다형성
  • 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용합니다.
  • Static 메소드는 재정의 할 수 없습니다(Static 메서드는 컴파일 타임에 메모리에 할당되기 때문).
//메소드 오버라이딩 예제
public class Exam03 {
    public static void main(String[] args) {
        Car suv = new Suv(20);
        suv.go();
        System.out.println("distance = " + suv.getDistance());
    }

    interface Car {
        void go();

        void back();

        int getDistance();
    }

    static class Suv implements Car {
        private int distance;

        Suv(int distance) {
            this.distance = distance;
        }

        @Override
        public void go() {
            distance += 2;
        }

        @Override
        public void back() {
            distance -= 2;
        }

        @Override
        public int getDistance() {
            return distance;
        }
    }
}

 

다이나믹 메소드 디스패치 (Dynamic Method Dispatch(발송?))

메소드 디스패치(method dispatch)는 호출할 메소드를 결정하여 실행시키는 과정을 말합니다. 이 과정은 static(정적)과 dynamic(동적)이 있습니다.

다이나믹 메소드 디스패치는 런타임 다형성(Runtime polymorphism)이라고도 불립니다. 이는 재정의된 메서드에 대한 호출이 컴파일 타임이 아닌 런타임에 이루어 지는 것을 뜻합니다.

즉, 메서드 재정의를 통해 다이나믹 메소드 디스패치를 얻을 수 있습니다.

https://user-images.githubusercontent.com/48986787/108785059-0564fe80-75b4-11eb-9e42-a596b74806c1.png

다이나믹 메소드 디스패치 과정에서 오버라이드 된 메서드는 부모 클래스의 참조 변수를 통해 호출됩니다.

( → 부모 클래스의 참조 변수로 자식 클래스의 Instance를 참조 할 수 있기 때문. )

호출 할 메서드가 결정되는 것은 참조 변수가 참조하는 객체를 기반으로 합니다.

코드로 보면,

class Parent{
    public void print() {
        System.out.println("나는 부모!");
    }
}

class Child extends Parent{
    @Override
    public void print() {
        System.out.println("나는 자식!");
    }
}

public class DynamicMethodDispatch {
    public static void main(String[] args) {
        Parent child = new Child();    //**호출 할 메서드가 결정되는 것은 참조 변수가 참조하는 객체를 기반 여기서는 부모 클래스의 참조 변수가 자식 클래스의 instance를 참조** 
        child.print();    //**오버라이드 된 메서드는 부모 클래스의 참조 변수를 통해 호출**
    }
}

출력문은

나는 자식!

다이나믹 메소드 디스패치의 속성

  • 어떤 메소드가 실행될지 런타임에 결정됩니다.
  • 동적 바인딩(실행 시간에 성격이 결정)을 통해 동작이 이루어집니다.
  • 서로 다른 클래스간(인터페이스, 추상클래스, 상속 등)에 동작이 발생합니다.
  • 자식 클래스 객체가 부모 클래스 객체에게 할당되야 합니다.(위의 코드로 확인했다)
  • 상속과 관련이 있습니다.

 

추상 클래스

코드를 작성 중, 뼈대 작성만 먼저하고, 구현을 나중으로 미루고 싶을 때가 있습니다. 이는 추상 클래스를 통해 쉽게 만들 수 있습니다.

추상클래스의 속성

class 키워드 앞에 abstract 한정자를 붙여 정의할 수 있습니다.

abstract class Animal{

}

서브클래스화 될 수 있지만 인스턴스화는 할 수 없습니다.


public static void main(String[] args){
        Animal animal = new Animal(); // 이거 안됨
}

클래스에 하나 이상의 추상 메서드가 있는 경우, 클래스 자체가 추상 클래스가 되야합니다.

abstract class Animal{
        abstract void print();
}

추상 클래스는 추상 및 구체 메서드를 모두 선언할 수 잇습니다.

abstract class Animal{
                public String hello(){
                    retrun "hi";
                }
        abstract void print();
}

추상 클래스 예제

public class AbstractTest {
    @Test
    void abstarctClassTest() {
        Product timesTwo = new TimesTwo();
        Product timesWhat = new TimesWhat(3);

        Assertions.assertThat(timesTwo.getValue()).isEqualTo(2);
        Assertions.assertThat(timesWhat.getValue()).isEqualTo(3);
    }
}

abstract class Product {
    protected int value;
    public Product( int value) {
        this.value = value;
    }

    public abstract int multiply(int val);
    public abstract int getValue();
}

class TimesTwo extends Product {
    public TimesTwo() {
        super(2);
    }

    @Override
    public int multiply(int val) {
        return super.value * val;
    }

    @Override
    public int getValue() {
        return super.value;
    }
}

class TimesWhat extends Product {
    public TimesWhat(int what) {
        super(what);
    }

    @Override
    public int multiply(int val) {
        return super.value * val;
    }

    @Override
    public int getValue() {
        return super.value;
    }
}

언제 추상클래스를 사용하는가?

공통의 멤버변수, 공통의 메서드를 가져야할때

코드를 재사용 해야할때

추상화와 공통 구현의 캡슐화를 한 곳에서 할 때

 

final 키워드

상속을 통해 기존 코드를 재사용 할 수 있지만 때로는 확장성에 대한 제한을 설정해야합니다. 이때 Final 키워드가 필요합니다.

클래스, 메소드, 변수 등등 사용의 위치에 따라 그 의미가 조금 다른데요. 살펴보겠습니다.

클래스에서의 final 키워드

클래스에 final 키워드가 붙여있다면 상속해서 확장 될 수 없음을 의미합니다.

즉, 재사용 될 수 없음을 의미합니다.

자바에서 기본적으로 제공하는 String을 살펴보면, final로 선언된 것을 확인할 수 있습니다.

만약 String클래스를 재정의하고, 확장한다면 String에 대한 작업의 결과를 예측할 수 없겠죠?

그렇기 때문에 다른 프로그래머가 확장하고, 재사용 할 수 없기 막아둔 것입니다.

(안좋은 의미로는 확장성을 막는 방법입니다. )

예제를 보면

public final class Cat {

    private int weight;

    // standard getter and setter
}

아래와 같이 사용한다고 하면

public class BlackCat extends Cat {
}

아래와 같은 에러 메세지를 볼 수 있습니다.

The type BlackCat cannot subclass the final class Cat

주의. class의 final은 불변을 의미하지 않습니다.

클래스를 상속헤사 확장할 수 없다는 것이지, 변경할 수 없음을 보장하는 것은 아니기 때문입니다.

Cat cat = new Cat();
cat.setWeight(1);

assertEquals(1, cat.getWeight());

메소드에서 final 키워드

메소드에 final 키워드가 붙어있다면 재정의(오버라이딩)가 불가함을 의미합니다.

클래스 상속(extends)을 완전히 금지할 필요는 없지만 일부 메서드의 재정의만 막을 필요가 있을 때 사용합니다.

이는 Thread 클래스에서 잘 사용되어 있습니다. ( isAlive() 메소드)

네이티브 메도이기 때문에, final로 재정의를 막았습니다.

/**
     * Tests if this thread is alive. A thread is alive if it has
     * been started and has not yet died.
     *
     * @return  <code>true</code> if this thread is alive;
     *          <code>false</code> otherwise.
     */
    public final native boolean isAlive();

예제를 보면

public class Dog {
    public final void sound() {
        // ...
    }
}

상속을 하고, 메서드를 재정의 하려하면

public class BlackDog extends Dog {
    public void sound() {
    }
}

아래와 같은 컴파일 에러를 확인할 수 있습니다.

overrides
- Cannot override the final method from Dog
sound() method is final and can’t be overridden

또한, 생성자가 다른 메서드를 호출하는 경우 호출되는 메서드를 final로 선언해야합니다. → side effect를 막기 위해 (validate가 변경될 수 도 있다.)

변수에서 final 키워드

변수에 final 키워드가 붙어있다면 재할당이 불가함을 의미 합니다.

즉, 한번 초기화되면 변경될 수 없음을 의미합니다.

1. 프리미티브 타입에서 final 키워드

public void whenFinalVariableAssign_thenOnlyOnce() {
    final int i = 1;
    //...
    i=2;
}

이렇게 사용할 경우 컴파일 에러가 발생합니다.

The final local variable i may already have been assigned

2. 래퍼런스 타입에서 final 키워드

래퍼런스 변수에 final이 붙으면 재 할당이 불가능합니다. 하지만 이는 객체 자체가 불변임을 보장하진 않습니다. ( 참조하는 주소값을 변경할 수 없지, 주소값이 참조하고 있는 값은 변경할 수 있음)

예제를 보면,

final Cat cat = new Cat();
cat = new Cat();

위와같이 선언 시 컴파일 에러가 발생합니다.

The final local variable cat cannot be assigned. It must be blank and not using a compound assignment

하지만 아래와 같이 속성은 변경할 수 있습니다.

cat.setWeight(5);

assertEquals(5, cat.getWeight());

3. 멤버 변수(필드)에서 final 키워드

상수 사용시

선언과 동시에 초기화 되야합니다.

static final int MAX_MONEY = 10_000_000;

변수에 사용시

초기화 블럭, 생성자에서 초기화가 무조건 진행되야 합니다.

class Money{
    private final int money;

        public Money(final int money){
            this.money = money;
        }
}

파라미터에서 final 키워드

매소드의 파라미터에 final은 변수에서 사용되는 final과 같은 의미를 가집니다.

예제를 보면,

public void methodWithFinalArguments(final int x) {
    x=1;
}

위와 같은 경우 컴파일 에러가 발생합니다.

The final local variable x cannot be assigned. It must be blank and not using a compound assignment

사용하는 위치에 따라 그 의미가 달라지니 명확히 알고 가야할 것 같습니다.

 

Refer

댓글