Java

Java의 Dynamic Method Dispatch, Abstract class, final, Object class

donggi 2022. 9. 1. 14:28
백기선 자바 스터디 6주차 상속 주제에 대해 학습하면서 정리한 내용입니다.

Dynamic Method Dispatch


다이나믹 메서드 디스패치는 Overriding된 메서드에 대한 호출이 compile-time이 아닌 runtime에 결정되는 메커니즘입니다.

  • Overriding 메서드가 조상 클래스 참조를 통해 호출될 때 Java는 호출이 발생할 때 참조되는 Object의 유형에 따라 실행할 메서드의 버전을 결정합니다.
    • 이 결정은 runtime에 이루어집니다.

예시

public class Fruit {
    void m1() {
        System.out.println("Eat some Fruit");
    }
}

class Apple extends Fruit {
    void m1() {
        System.out.println("Eat some Apple");
    }
}

class Banana extends Fruit {
    void m1() {
        System.out.println("Eat some Banana");
    }
}

class Dispatch {
    public static void main(String args[]) {
        Fruit fruit = new Fruit();
        Apple apple = new Apple();
        Banana banana = new Banana();


        Fruit ref;
        ref = fruit;
        ref.m1();

        ref = apple;
        ref.m1();

        ref = banana;
        ref.m1();
    }
}
  • Apple, Banana 클래스는 Fruit 클래스를 상속 받고 있습니다.
  • 각각의 자손 클래스에서는 m1()를 재정의하고 있습니다.
  • Fruit 클래스 유형의 참조 변수를 선언합니다.
  • Apple, Banana 인스턴스 변수를 각각 ref 변수에 할당하여 m1() 메서드를 호출하면 재정의한 메서드가 호출되는 걸 볼 수 있습니다.
public class A {
    int x = 10;
}

class B extends A {
    int x = 20;
}

class Test {
    public static void main(String[] args) {
        A a = new B(); // object of type B

        // Data member of class A will be accessed
        System.out.println(a.x);
        // 10
    }
}
  • Java에서 메서드만 오버라이드가 가능합니다.
  • 변수는 오버라이드가 불가능한데 이것에 대한 예시 코드입니다.
  • B 클래스는 A 클래스를 상속 받고 있습니다.
    • 두 클래스 모두 x라는 멤버 변수를 갖고 있습니다.
    • A 클래스 유형인 참조 변수 a B 클래스 객체를 만듭니다.
    • 멤버 변수 x는 재정의되지 않기 때문에 runtime 시점에 결정되는 Dynamic Method Dispatch가 이루어지지 않고 조상 클래스 A의 멤버 변수 x의 값이 출력되게 됩니다

Dynamic Method Dispatch의 장점

  1. Dynamic Method Dispatch를 통해 Java는 runtime-polymorphism 핵심인 Override Method를 지원할 수 있습니다.
  2. 이를 통해 상속 관계를 통해 코드 중복을 줄일 수 있고, 자손 클래스에서 일부 메서드를 재정의할 수 있게 됩니다.

정적 바인딩과 동적 바인딩

  • 정적 바인딩은 compile-time 동안 수행되고 동적 바인딩은 runtime 중에 수행됩니다.
  • private, final, static method 변수들은 정적 바인딩을 사용하여 compiler에 의해 연결되는 반면 Override method는 runtime 객체 유형에 따라 runtime 시점에 결합하게 됩니다.

 

 

추상 클래스


추상화(abstraction) 는 특정 세부 사항을 숨기고 사용자에게 필수적인 정보만 보여줍니다. abstract class 혹은 interface를 통해 추상화 할 수 있습니다.

abstract 키워드를 이용해 추상 클래스를 만들 수 있습니다. 메서드에도 abstract 키워드를 이용하여 추상 메서드를 만들 수 있습니다.

  • abstract class : 추상 클래스는 인스턴스를 만들 수 없습니다.
  • abstract method : 추상 클래스에서만 사용할 수 있으며 본문은 작성하지 않습니다. 본문은 상속된 하위 클래스에서 작성되게 됩니다.

예시

abstract class Base {

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

    abstract void fun();
}

class Derived extends Base {

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

    void fun() {
        System.out.println("Derived fun() called");
    }
}

class Main {

    public static void main(String[] args) {
        Base b = new Derived();
        b.fun();
        // Base class constructor
        // Derived class constructor
        // Derived fun() called
    }
}
  • abstract method의 본문은 자손 클래스에서 작성되었습니다.
  • 추상 클래스는 기본 생성자를 가질 수 있는데 이는 상속 받은 자손 클래스 인스턴스가 생성될 때 호출됩니다.
  • 추상 클래스는 추상 메서드 없이 작성할 수 있습니다. 추상 클래스는 인스턴스화 할 수 없고, 상속만 가능하게 됩니다.

그 외

  • 그 외에도 추상 클래스는 static 메서드를 정의할 수 있습니다.
  • 클래스에 추상 메서드가 포함되어 있으면 반드시 클래스를 abstract로 선언해야 합니다.
  • 추상 클래스를 상속 받는 자손 클래스가 추상 메서드를 구현할 수 없는 경우 다음 자식 클래스가 나머지 추상 클래스를 구현할 수 있도록 해당 자식 클래스를 추상 클래스로 선언해줘야 합니다.

정리

  • 추상 클래스는 기본 생성자를 가질 수 있고, 기본 생성자는 상속 관계의 자손 클래스의 인스턴스가 생성될 때 호출됩니다.
  • 추상 클래스는 추상 메서드 없이 작성 가능하고, 추상 메서드를 선언할 경우 본문은 상속 관계의 자손 클래스에서 작성하게 됩니다.
  • 추상 클래스는 인스턴스화 할 수 없고, 상속만 가능합니다.
  • 추상 클래스는 static 메서드를 정의할 수 있습니다.

 

Java의 final keyword


final 키워드는 클래스, 메서드, 변수에서 사용 할 수 있습니다.

  • final variable의 경우 변수를 상수로 만듭니다.
  • final method는 메서드가 재정의되는 걸 막습니다. (Override)
  • final class는 해당 클래스의 상속을 막습니다.

Final Variables

  • 변수에는 final 키워드를 사용할 수 있습니다. 변수에 final을 사용하게 되면 해당 변수는 재할당 할 수 없기에 상수가 됩니다.
  • final 키워드를 사용할 변수는 반드시 초기화해주어야 합니다.
  • final 키워드를 사용한 참조 변수는 다른 참조 객체를 재할당 할 수 없습니다.
    • 하지만 참조 변수가 가리키는 객체의 내부 상태는 변경될 수 있습니다.
    • 배열 혹은 collection에 요소를 추가하거나 제거할 수 있습니다.

final 변수의 선언과 초기화를 따로 할 수 있는 경우

일반적으로 final 키워드를 사용한 변수는 선언과 동시에 초기화를 해주어야한다. 그러지 않으면 compile-time 에러가 발생합니다.

특정한 상황에서 final 변수의 선언과 초기화를 따로 할 수 있습니다.

public class GFG {

    final int THRESHOLD = 5;
    final int CAPACITY;
    final int MINIMUM;
    static final double PI = 3.141592653589793;
    static final double EULERCONSTANT;

    {
        CAPACITY = 25;
    }

    // static initializer block for
    // initializing EULERCONSTANT
    static {
        EULERCONSTANT = 2.3;
    }

    public GFG() {
        MINIMUM = -1;
    }

    public static void main(String[] args) {
        final int a;
        a = 10;
        System.out.println(a);

        int[] arr = new int[]{1, 2, 3};
        for (final int i : arr) {
            System.out.println(i);
        }
    }
}

코드 출처 : https://www.geeksforgeeks.org/final-keyword-in-java/

  • final 키워드를 사용한 인스턴스 변수는 인스턴스 블럭에서 변수를 초기화할 수 있습니다.
  • final 키워드를 사용한 static 변수 또한 static 블럭에서 변수를 초기화 할 수 있습니다.
  • 또한 생성자에서 final 키워드를 사용한 인스턴스 변수를 초기화 할 수 있습니다.
  • 지역 변수에 final 키워드를 사용하는 경우 선언과 초기화를 따로 할 수 있습니다.

 

Object Class


모든 Java 클래스는 직접적, 간접적으로 Object 클래스에서 파생됩니다. 클래스가 다른 클래스를 상속 받지 않으면 Object의 직접적 자손 클래스이고 다른 클래스를 상속 받게 되면 간접적으로 파생되는 자손 클래스입니다.

Object 클래스의 메서드

1. toString()

toString() 메서드는 객체의 String 표현을 제공하며 객체를 String으로 변환하는데 사용됩니다.

  • 기본적인 toString() 메서드는 객체가 인스턴스인 클래스의 이름, @, 16진수 해시코드를 String으로 반환합니다
  • 일반적으로 toString() 메서드는 재정의하여 사용합니다.

2. hashCode()

  • JVM은 모든 객체에 대해 해시 코드라는 고유한 번호를 생성합니다.
  • hashCode() 메서드는 객체의 주소를 반환하는 것이 아닌 알고리즘을 사용하여 객체의 내부 주소를 정수로 변환합니다.
  • hashCode() 메서드는 Java를 이용해 객체의 주소를 찾는 게 불가능하기 때문에, 객체의 주소를 찾기 위해 C/C++과 같은 네이티브 언어를 사용합니다.

3. equals(Object obj)

  • 주어진 객체를 호출되는 객체와 비교합니다.
  • equals(Object obj) 메서드를 재정의할 땐 hashCode() 메서드 또한 재정의하는 것이 좋습니다.
    • 그렇지 않으면 동일한 객체가 서로 다른 해시 값을 얻을 수 있으며 Hash(HashMap, HashSet, Hashtable) 기반 컬렉션이 제대로 작동하지 않습니다.

4. getClass()

  • getClass() 메서드는 final 메서드이므로 재정의할 수 없습니다.
  • this 객체의 클래스 객체를 반환하며 객체의 실제 런타임 클래스를 가져오는데 사용됩니다.
  • 클래스의 메타데이터를 가져오는 데에도 사용할 수 있습니다.
  • runtime 시점에 JVM heap 영역에 java.lang.Class 객체를 생성합니다. 이 클래스 객체를 사용하여 getClass() 메서드는 클래스 수준 정보를 얻을 수 있습니다.

5. finalize()

  • finalize() 메서드는 객체가 Garbage Collector에 의해 수집되기 직전에 호출됩니다.
  • 시스템의 자원을 삭제하고 정리 작업을 수행하여 메모리 누수를 최소화하려면 finalize() 메서드를 재정의하여 사용해야합니다.

6. clone()

  • 해당 객체와 동일한 새 객체를 반환합니다.
  • 객체를 복사할 땐 얕은 복사 (Shallow Copy) 와 깊은 복사 (Deep Copy)가 있습니다. Java clone()

 

 

References


https://www.geeksforgeeks.org/dynamic-method-dispatch-runtime-polymorphism-java/

https://www.w3schools.com/java/java_abstract.asp

https://www.geeksforgeeks.org/abstract-classes-in-java/

https://www.geeksforgeeks.org/final-keyword-in-java/

https://www.geeksforgeeks.org/object-class-in-java/

'Java' 카테고리의 다른 글

멀티스레드 프로그래밍  (2) 2022.09.27
예외 처리  (0) 2022.09.15
Java Overriding  (0) 2022.08.30
Java의 상속과 super 키워드  (0) 2022.08.29
5주차 클래스  (0) 2022.08.18