스터디할래 - 6주차 과제: 상속

https://github.com/whiteship/live-study

🎄목표

 

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

 

 

🎄학습할 것 (필수)

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

 


 

📌 자바 상속의 특징

 

상속(Inheritence)

 

현실 세계에서 상속은 부모가 자식에게 재산을 물려주는 행위를 뜻한다. 객체지향 프로그래밍에서의 상속 또한 마찬가지

 

로 부모 클래스의 필드와 메서드를 자식 클래스에게 물려주는 상속이란 개념이 존재한다.

 

 

상속을 물려주는 클래스를 우리는 '부모 클래스(parent class)', '상위 클래스(super class)', '기본 클래스(base class)'라 부

 

르며, 상속을 받는 클래스를 '자식 클래스(child class)', '하위 클래스(sub class)', '파생 클래스(derived class)'라 한다.

 

 

상속의 장점

 

- 기존에 작성된 클래스 재사용 

 

- 공통적인 필드, 메서드를 작성해두고 상속을 통해 중복된 코드를 제거

 

 

자바에서의 상속의 특징

 

자바는 다른 언어(C++, 파이썬 등) 다중 상속을 지원하지 않으며, 대신 인터페이스를 활용하여 기능을 확장

 

시킬 수 있다.

 

 

 

상속 예제 코드

 

Parent.java
public class Parent {
    public int b;
    
    public Parent() {
        b = 10;
    }
    
    public void parentMethod() {
        System.out.println("parent Method");
    }
}

 

코드 설명 : Parent.class의 필드 b를 생성자를 통해 10으로 초기화 하며, Parent class는 출력 메서드 하나가

 

존재한다.

 

 

 

Child.class
public class Child extends Parent {
}

 

코드 설명 : Child class는 Parent class를 extends 키워드를 사용하여 상속 받는다.

 

 

 

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class MainTest {
    @Test
    @DisplayName("부모, 자식 클래스의 상속 관계 확인")
    void inheritance() {
        Parent parent = new Parent();
        Child child = new Child();

        assertAll(

                () -> assertThat(child).isInstanceOf(Parent.class),
                () -> assertThat(child).isInstanceOf(Child.class),
                () -> assertThat(parent).isInstanceOf(Parent.class),
                
                () -> assertThat(parent).isNotInstanceOf(Child.class)

        );
    }

    @Test
    @DisplayName("자식 클래스에서 부모 클래스의 필드 호출 확인")
    void inheritance_field_call() {
        Child child = new Child();

        assertThat(child.b).isEqualTo(10);
    }

    @Test
    @DisplayName("자식 클래스에서 부모 클래스의 메서드 호출 확인")
    void inheritance_method_call() {
        Child child = mock(Child.class);

        child.parentMethod();

        verify(child, times(1)).parentMethod();
    }
}

 

 

테스트 코드 실행 결과

 

 

코드 설명 : 해당 테스트 코드는 3가지를 확인한다. 

 

1) 첫번째 테스트는 부모 클래스와 자식 클래스의 상속 관계를 확인한다. 부모 클래스, 자식 클래스 자신

 

에 대한 instanceof 연산자는 테스트 통과 되며 부모-> 자식은 isInstanceOf로, 자식 -> 부모는

 

isNotInstanceOf로 확인하여 테스트하였다.

 

 

2)  두번째 테스트 자식 클래스에서 부모 클래스의 필드를 접근하여 호출이 가능한지에 대한 테스트로, 부

 

모 클래스의 생성자에서 필드의 값을 10으로 초기화 하였기 때문에 10과 같은지 확인하였다.

 

 

3) 세번째 테스트는 자식 클래스에서 부모 클래스로부터 물려 받은 메서드를 호출 가능한지에 대한 테스트

 

로, 메서드의 호출을 확인하기 위해 Mockito 프레임워크를 사용하였다.

 

child 객체를 목 객체로 생성해서 부모로부터 상속받은 메서드를 호출 하였는지 verify()를 통해 검증 하였다.

 

 

 

📌 super 키워드

 

현실에서 부모 없이 자식이 생겨날 수 없듯이 자바에서도 자식 객체를 생성하면, 부모가 먼저 생성된 후, 자

 

식 객체가 생성된다. 모든 객체는 클래스의 생성자를 호출해야 되는데, 자식 클래스의 생성자에는 super()

 

키워드를 사용하여 부모 생성자를 호출한다. 이를 명시 하지 않고 생략 할 수 있다.

 

Parent.java
public class Parent {
    public Parent() {
        System.out.println("Parent Constructor");
    }
}

Child.java
public class Child extends Parent {
    public Child() {
        //super(); 생략되어 있음
        System.out.println("Child constructor");
    }
}

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

Output:
Parent Constructor
Child constructor

 

 

자식 클래스에서 부모 클래스의 메서드를 오버라이딩하면 부모 클래스의 메서드 대신 오버라이딩된 자식 

 

메서드만 사용된다. 그러나 자식 클래스의 메서드에서 오버라이딩된 부모 클래스의 메서드를 호출하고 싶

 

다면 super 키워드를 붙여서 부모 클래스의 메서드를 호출 할 수 있다. 예시는 아래와 같다.

 

Animal.java
public class Animal {
    void bark () {
        System.out.println("동물이 짖습니다");
    }
}

Dog.java
public class Dog extends Animal{
    @Override
    void bark() {
        System.out.println("멍멍");
        super.bark();
    }
}

Main.java
public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.bark();

    }
}

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

Output:
멍멍
동물이 짖습니다

 

부모 클래스 Animal의 bark()는 "동물이 짖습니다"를 출력한다. Animal을 상속받은 Dog 클래스의 bark()를

 

오버라이딩하여 "멍멍"을 출력하도록 하였고, 부모 클래스의 bark()도 super 키워드를 사용하여 호출하였다.

 

따라서 Dog 객체를 생성해서 bark()를 호출하면 오버라이딩 된 메서드가 호출되는 것을 확인 할 수 있다.

 

 

📌 메소드 오버라이딩

 

자바에서는 메서드 오버라이딩을 지원하는데, 메서드 오버라이딩이란 부모 클래스의 메서드를 자식 클래스

 

에서 재정의 하는 것을 말한다. 부모 클래스의 메서드가 자식 클래스에게 맞게 설계되지 않을 때 자식 클래

 

스에 맞게 재정의 한다고 보면 된다.

 

 

메서드 오버라이딩 규칙은 다음과 같다.

 

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

 

메서드 오버라이딩의 예제는 위의 bark()를 재정의한 코드를 참고하길 바란다.

 

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

 

메서드 오버라이딩은 Java가 런타임 다형성을 지원하는 방법 중 하나이다.  다이나믹 메서드 디스패치는 컴파일 타임이

 

아닌 런타임에 오버라이드 된 메서드에 대한 호출이 해결되는 메카니즘이다.

 

(다이나믹에 디스패치 등 어렵게 보이는 용어에 대한 위압감이 있었는데 막상 공부해보니 엄청 어려운 개념까지는 아닌 것 같았다. )

 

오버라이딩 된 메소드가 수퍼 클래스 참조에 의해 호출 될 때, 자바는 호출이 발생할 때 참조되는 객체의 유형에 따라 어

 

떤 메서드의 버전이 실행될 것인지를 결정한다.

 

수퍼 클래스 참조 변수는 서브 클래스 객체를 참조 할 수 있으며(upcasting) 자바는 이 사실을 이용하여 런타임에 오버라

 

이드 된 메소드에 대한 호출을 해결한다.

 

 

class A {
    void m1() { System.out.println("A의 m1 메서드"); }
}

class B extends A {
    @Override
    void m1(){ System.out.println("B의 m1 메서드"); }
}

class C extends A {
    @Override
    void m1(){ System.out.println("c의 m1 메서드"); }
}

public class Dispatch {
    public static void main(String args[]) {
        
        // 1단계
        A a = new A();
        B b = new B();
        C c = new C();

        // 2단계
        A ref;

        // 3단계
        ref = a;
        ref.m1();

        ref = b;
        ref.m1();

        ref = c;
        ref.m1();
    }
}

Output:
A의 m1 메서드
B의 m1 메서드
c의 m1 메서드

 

동작 과정 

 

수퍼 클래스 A를 생성하고, A를 상속 받은 B, C의 서브 클래스는 A의 메서드를 오버라이딩 하였다.

 

1. Dispatch 클래스의 메인 함수 내에 A, B, C 각각의 객체가 선언이 된다.

 

2. A의 참조 객체가 선언 되어(ref) null을 가리킨다.

 

3. 각 객체 타입에 대한 참조를 ref에 하나씩 할당하고 해당 참조를 사용하여 m1()을 호출한다. 어떠한 m1이 호출되는지

 

는 호출 시 참조되는 객체의 유형에 따라 결정 된다.

 

 

간단히 요약하자면 수퍼 클래스가 있고, 수퍼 클래스를 상속받은 둘 이상의 서브 클래스에서 수퍼 클래스의 메서드를 재

 

정의한 메서드를 상속 받은 객체에서 부모 클래스를 참조할 때, 어떠한 서브 클래스의 메서드가 호출 될지는 '런타임'에

 

결정이 되기 때문에 Dynamic method dispatch라고 불리우는 것이다.

 

 

📌 추상 클래스

 

추상 클래스는 실체 클래스가 공통으로 가져야 할 메소드와 필드들을 정의해 놓은 추상적인 클래스이다.

 

실체 클래스의 필드와 메서드를 통일 시키는데에 목적이 있다.

 

추상 클래스에서 추상 메서드를 정의 할 수 있는데 추상 메서드는 메서드의 선언만 있고 구현은 없는 메서드를 말한다.

 

추상 클래스를 설계할 때, 하위 클래스가 반드시 구현부를 채우도록 강요하고 싶은 메서드에 대해 abstract 키워드를 붙

 

여 추상 메서드로 선언하면 자식 클래스는 추상 메서드를 재정의 해서 구현해야 한다. 

추상 메서드의 body를 구현하려 하면 에러가 발생한다.

 

추상 메서드 선언
추상 메서드를 자식 클래스에서 재정의

 

 

📌 final 키워드

 

final 키워드는 클래스, 필드, 메서드 선언 시에 사용할 수 있으며 final 키워드는 해당 선언이 최종(말그대로

 

final) 상태이고 수정될 수 없음을 뜻한다. 

 

 

final 클래스

클래스를 선언할 때 final 키워드를 클래스 앞에 붙이면 이 클래스는 최종적인 클래스임을 나타내는데, 이 말인 즉슨 상속

 

할 수 없는 클래스가 된다고 보면 된다. 해당 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없게 된다.

 

자바 표준 API의 String 클래스는 final 클래스의 대표적인 예시라고 볼 수 있다.

public final class String {}

 

String.java

 

final 클래스를 상속 받으려고 하면 아래와 같은 에러 메시지를 출력하는 것을 볼 수 있다.

 

 

final 필드 및 변수

 

final이 클래스 내의 필드에서 사용 될 시 값을 변경할 수 없는 상수가 됨을 뜻하며, 초기 값을 설정하지 않을 경우 에러

 

발생한다.

 

 

 

final 이 붙은 변수는 다음과 같이 초기 값 없이 선언 할 수 있지만, 한 번 값을 설정하면 이후에는 값을 변경 할 수 없으

 

며, final 변수는 의례적으로 대문자[필수]와 언더바(_)[선택]를 조합하여 사용한다.

 

 

 

 

final 메서드

 

메서드에 final 키워드를 붙이게 되면 해당 메서드는 최종적인 메서드란 의미가 되므로 오버라이딩이 불가

 

능 하게 되며, 상속 받은 클래스에서 해당 메서드를 오버라이딩이 될 수 없다.

 

 

📌 Object 클래스

 

Object 클래스는 자바에서 모든 클래스의 최상위 클래스이다. 즉, 모든 자바 클래스들은 Object 클래스로부터 상속을 받

 

으며, 다양한 정적 메서드들을 가지고 있다. Object 클래스의 메서드는 아래와 같다.

 

메서드 설명
protected Object clone() 해당 객체의 복제본을 생성하여 반환
boolean equals(Object obj) 해당 객체와 전달받은 객체가 같은지 여부를 반환
protected void finalize() 해당 객체를 더는 아무도 참조하지 않아 가비지 컬렉터가 객체의 리소스를 정리하기 위해 호출
Class<T> getClass() 해당 객체의 클래스 타입을 반환
int hashCode() 해당 객체의 해시 코드값을 반환
void notify() 해당 객체의 대기(wait)하고 있는 하나의 스레드를 다시 실행할 때 호출
void notifyAll() 해당 객체의 대기(wait)하고 있는 모든 스레드를 다시 실행할 때 호출
String toString() 해당 객체의 정보를 문자열로 반환
void wait() 해당 객체의 다른 스레드가 notify()나 notifyAll() 메소드를 실행할 때까지 현재 스레드를 일시적으로 대기(wait)시킬 때 호출
void wait(long timeout) 해당 객체의 다른 스레드가 notify()나 notifyAll() 메소드를 실행하거나 전달받은 시간이 지날 때까지 현재 스레드를 일시적으로 대기(wait)시킬 때 호출
void wait(long timeout, int nanos) 해당 객체의 다른 스레드가 notify()나 notifyAll() 메소드를 실행하거나 전달받은 시간이 지나거나 다른 스레드가 현재 스레드를 인터럽트(interrupt) 할 때까지 현재 스레드를 일시적으로 대기(wait)시킬 때 호출

표 출처: www.tcpschool.com/java/java_api_object

 

 

 

📖 Reference

이것이 자바다 ( 한빛미디어, 신용권 저)

다이나믹 메소드 디스패치

다중 상속

TCP School