본문 바로가기
자바/스터디

[자바 스터디] 6주차 과제 - 상속

by jeonghaemin 2020. 12. 25.
728x90

본 게시글은 백기선 님의 live-study 과제를 수행하면서 작성한 글입니다.

목표

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

학습할 것 (필수)

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

자바 상속의 특징

단일 상속

C++은 여러 조상 클래스로부터 상속받는 '다중 상속'을 허용하지만 자바에서는 단일 상속만 가능하다.

다중 상속을 허용하면 위 그림과 같이 서로 다른 클래스로부터 상속받은 멤버 간의 선언부가 같은 경우 구별할 수 없다는 단점을 가지고 있다. 이런 경우에 조상 클래스의 메서드를 사용하는 모든 클래스를 변경해야 되기 때문에 간단한 문제가 아니다.

자바에서는 다중 상속의 이러한 문제점 때문에 단일 상속만을 허용한다.

Object 클래스

자바에서는 모든 클래스의 최상위 조상 클래스는 Object 클래스이다.

다른 클래스로부터 상속받지 않은 모든 클래스들은 컴파일러가 자동으로 'extends Object'를 추가해준다.

만약 다른 클래스를 상속받는다고 해도 최상위 조상 클래스는 무조건 Object클래스이다.

toString()이나 equals(Object o) 메서드를 정의하지 않고도 사용할 수 있었던 것이 Object 클래스의 메서드이기 때문이다.

Super

인스턴스 자기 자신을 참조할 때 this를 사용했던 것과 마찬가지로 super는 조상 클래스의 멤버를 참조하는 데 사용된다.

class Parent {
    int n = 10;

        void method() {}
}

class Child extends Parent {
    int n = 20;

    void print() {
        System.out.println(this.n + ", " + super.n);
    }

        void method() {
            super.method(); //Parent 클래스의 method()
        }
}

예를 들어 위와 같이 조상 클래스와 자손 클래스에 같은 이름의 멤버가 사용되었을 때 super 키워드를 사용하여 구별할 수 있고, 메소드 오버라이딩의 경우에도 자손 메소드와 조상 메소드를 구별할 수 있다.

super()

super()는 조상 클래스의 생성자를 호출하는 데 사용된다. 자손 클래스의 인스턴스가 조상 클래스의 멤버들을 사용하려면 조상 클래스의 초기화 작업이 수행되어야 하기 때문에 자손 클래스 생성자의 첫 줄에서 조상 클래스의 생성자를 호출해줘야 한다.

Object 클래스를 제외한 모든 클래스의 생서자의 첫 줄에는 this() 또는 suepr()를 호출해야 한다. 그렇지 않으면 컴파일러가 자동으로 super()를 생성자 첫 줄에 삽입한다.

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

    public Parent(int a) {
        System.out.println();
    }
}

class Child extends Parent {
    public Child() {
        System.out.println("Child()");
    }
}

실행 결과

다음과 같이 명시적으로 초기화해줄 수 있다.

메소드 오버라이딩

메소드 오버라이딩이란?

메소드 오버라이딩이란 조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 말한다.

조상클래스로부터 상속받은 메소드를 자손 클래스에 맞게 변경해야 하는 경우가 많은데 예를 들어 다음과 같다.

class Point {
    int x, y;

    public String getLocation() {
        return "[" + x + "," + y + "]";
    }
}

class Point3D extends Point{
    int z;

    public String getLocation() {
        return "[" + x + "," + y + "," + z + "]";
    }
}

Point 클래스는 x,y 2차원 상의 좌표를 나타내는 클래스로, getLocation() 메소드를 이용하여 현재 좌표 x, y를 문자열로 반환한다.

Point3D 클래스는 3차원 상의 좌표를 표현하기 위해 Point 클래스를 상속받으며 z축 좌표가 추가되었다. 그렇기 때문에 상속받은 getLocation() 메소드를 3차원 좌표 문자열을 출력하도록 메소드 오버라이딩을 통해 수정하여야 한다.

메소드 오버라이딩 조건

  • 조상 클래스의 메소드와 선언부(이름, 반환 타입, 매개변수)가 같아야 한다.
  • 접근 제어자는 조상 클래스의 메소드보다 좁은 범위로 변경할 수 없다.
  • 조상 클래스의 메소드보다 많은 수의 예외를 선언할 수 없다.
  • 인스턴스 메서드를 static 메서드로, static메소드를 인스턴스 메소드로 변경할 수 없다.

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

디스패치(Dispatch)란 프로그램이 어떤 메소드를 호출할 것인가를 결정하고 실행하는 과정을 말한다.

디스패치에는 정적 디스패치, 동적 디스패치 두 가지가 있다.

정적 디스패치

  • 어떤 메소드가 실행될지 컴파일 타임에 결정되는 것.
  • 아래 코드와 같은 경우 컴파일 시점에 어떤 메소드 실행할지 결정된다.
public class Main {
    public static void main(String[] args) {
        Parent p = new Parent();
        Child c = new Child();
        p.method(); // Parent method()
        c.method(); // Child method()
    }
}

class Parent {
    void method() {
        System.out.println("Parent method()");
    }
}

class Child extends Parent {
    void method() {
        System.out.println("Child method()");
    }
}

동적 디스패치

  • 어떤 메소드가 실행될지 런타임에 결정되는 것.
  • 자바에서는 다형성을 통해 조상 타입의 변수로 자손 타입의 객체를 참조할 수 있다.
public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        p.method(); //Child method()
        System.out.println(p.n); // 0
    }
}

class Parent {
    int n = 0;

    void method() {
        System.out.println("Parent method()");
    }
}

class Child extends Parent {
    int n = 1;

    void method() {
        System.out.println("Child method()");
    }
}

위와 같이 멤버 변수의 경우는 변수의 타입에 따라 값이 결정되지만 메소드의 경우는 동적 디스패치에 의해 런타임에 참조 변수가 가리키는 객체의 실제 타입의 메소드를 실행된다.

추상 클래스

클래스를 설계도에 비유한다면 추상 클래스는 미완성 설계도에 비유할 수 있다. 추상 클래스는 추상 메소드를 포함하고 있는 클래스를 말하는 것으로 추상 메소드를 가지고 있다는 것을 제외하면 일반 클래스와 다를게 없다. 추상 클래스도 생성자를 가지며, 멤버 변수, 일반 메소드를 가질 수 있다. 추상 메소드는 메소드의 내용은 없고 선언만 되어있는 것을 말한다. 미완성 설계도로 제품을 만들 수 없듯이 추상클래스로 인스턴스를 생성할 수 없고, 상속을 통해 자손 클래스에서 추상 메소드를 구현해줘야 한다.

추상이란 여러 가지 사물이나 개념에서 공통되는 특성이나 속성 따위를 추출하여 파악하는 작용.

추상 클래스는 새로운 클래스를 작성하는 데 있어 서로 공통되는 틀을 잡아 준다. 예를 들어 리그 오브 레전드 게임에서 챔피언을 구현한다고 가정해보자. 모든 챔피언은 이동하는 기능을 가지고 있다. 하지만 이 기능은 각 챔프마다 차이가 있을 수 있다. 그렇기 때문에 추상 클래스를 사용하여 이동하는 기능을 구현해야 한다고만 알려주고, 이를 상속받은 각 챔피언 클래스에서 챔피언의 특징에 맞게 구현을 해주는 것이다.

abstract class Champion {
    int x, y;
    abstract void move(int x, int y);
}

class Caitlyn extends Champion {
    @Override
    void move(int x, int y) {
        // 케이틀린 챔피언 이동 코드
    }
}

class Kassadin extends Champion {
    @Override
    void move(int x, int y) {
        // 카사딘 챔피언 이동 코드
    }
}

위와 같이 챔피언 클래스 선언부에는 추상 메소드가 포함되어 있다고 abstract 키워드를 붙혀 알려주고, move 추상 메소드에도 abstract 키워드를 붙혀 추상 메소드 임을 알리고 메소드의 선언 부만 작성한다. 그리고 자손 클래스인 케이틀린, 카사딘 클래스에서 move 메소드를 각 챔피언에 맞게 구현한다.

주의할 점은 자손 클래스에서 추상 메소드를 구현하지 않았을 경우, 자손 클래스에도 abstract 키워드를 붙히고 자손 클래스의 자손 클래스에서 추상 메소드를 꼭 구현해줘야 한다. 즉, 추상 메소드는 상속의 과정에서 언젠가는 꼭 구현되어야 한다.

final 키워드

final : 마지막의, 변경할 수 없는

  • 클래스 : 변경될 수 없는 클래스, 상속될 수 없는 클래스. final 키워드가 붙은 클래스는 다른 클래스의 조상이 될 수 없다.
  • 메소드 : 변경될 수 없는 메소드, final 키워드가 붙은 메소드는 오버라이딩을 통해 재정의 될 수 없다.
  • 변수 : final 키워드가 붙은 변수는, 값을 변경할 수 없는 상수가 된다.
class FinalSample { //조상이 될 수 없는 클래스

    //상수명은 보통 모두 대문자로 써주며, 단어 사이에 _를 넣어 구분한다.
    final int MAX_SIZE = 10; //값을 변경할 수 없는 멤버 변수.
        final int MIN_SIZE;

        public FinalSample(int minSize) {
            MIN_SIZE = minSize; //생성자를 통해 상수 초기화 가능.
        }

    final int getMaxSize() { //오버라이딩 할 수 없는 메소드.
        final int RETURN_SIZE = MAX_SIZE; //값을 변경할 수 없는 지역 변수.

        return RETURN_SIZE;
    }
}

Object 클래스

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Object.html

모든 클래스의 조상 클래스인 Object 클래스는 멤버 변수 없이 11개의 메서드만을 가지고 있다.

protected Object clone()

  • 객체 자신의 복사본을 반환한다.
  • 기본적으로는 단순히 인스턴수 변수의 값만을 복사하기 때문에 참조 변수, 배열 등 깊은 복사가 필요할 경우 오버라이딩하여 사용한다.

public boolean equals(Object obj)

  • 객체 자신과 obj 객체가 같은 객체인지 알려준다.
  • 기본적으로는 객체의 주소 값을 비교, 필요시 적절히 오버라이딩하여 사용

protected void finalize()

  • 객체가 소멸될 때 가비지 컬렉터에 의해 자동 호출된다. 이때 수행되어야 할 코드가 있으면 오버라이딩한다.

public Class getClass()

  • 객체 자신의 멤버 변수, 메소드, 생성자 등 여러 가지 클래스 정보를 담고 있는 Class인스턴스를 반환한다.
  • Class.forName("패키지명.클래스명"), 클래스명.class 으로도 Class 인스턴스를 얻을 수 있다.

반환 값인 Class 객체는 클래스 파일에 클래스 로더에 의해 메모리에 올라갈 때 자동으로 생성된다. 클래스 로더는 필요한 실행 시에 필요한 클래스를 동적으로 메모리에 로드해준다.

  1. 기본에 생성된 객체가 메모리에 존재하는지 확인하고 있으면 객체의 참조를 반환
  2. 없으면 클래스 패스에 지정된 경로를 따라 클래스 파일을 찾는다.
  3. 찾으면 해당 클래스 파일을 읽어서 Class 객체로 변환, 못 찾으면 ClassNotFoundException 발생

public int hashCode()

  • 객체 자신의 해시 코드를 반환한다
  • equals() 메서드를 사용하여 다른 방식으로 객체의 같음을 비교하는 경우에 해당 메소드도 오버라이딩해야한다. 같은 객체라면 해시 코드도 같아야 하기 때문이다.

public String toString()

  • 객체 자신의 정보를 문자열로 반환한다.
  • 기본적으로 getClass().getName() + "@" + Integer.toHexString(hashCode())반환, 필요시 적절히 오버라이딩하여 사용

public void notify()

  • 객체 자신을 사용하려고 기다리는 쓰레드를 하나만 깨운다.

public void notifyAll()

  • 객체 자신을 사용하려고 기다리는 모든 쓰레드를 깨운다.

public void wait()

  • 다른 쓰레드가 notify()나 notifyAll()을 호출할 때까지 대기한다.

public void wait(long timeoutMillis [, int nanos])

댓글