0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaの継承についてまとめました

Last updated at Posted at 2024-12-13

はじめに

現在、私が進行しているJavaの勉強会でまとめた内容です。
内容に不正確な点があれば、ご指摘いただけるとありがたいです。

  • 韓国人として、日本語とコンピュータの勉強を同時に行うために、ここに文章を書いています
  • 翻訳ツールの助けを借りて書いた文章なので、誤りがあるかもしれません

目次

  • Java 継承の特徴
  • super キーワード
  • メソッドオーバーライド
  • ダイナミックメソッドディスパッチ (Dynamic Method Dispatch)
  • 抽象クラス
  • final キーワード
  • Object クラス

Javaの継承の特徴

継承とは?

言葉の意味そのままに親クラスの変数とメソッドを子クラスが受け継ぐことを言います。

継承を使用する理由

The idea of inheritance is simple but powerful: When you want to create a new class and there is already a class that includes some of the code that you want, you can derive your new class from the existing class. In doing this, you can reuse the fields and methods of the existing class without having to write (and debug!) them yourself.

継承の概念は簡単ですが強力です。新しいクラスを作成しようとする際に、すでに必要なコードの一部を含むクラスがあれば、そのクラスから新しいクラスを派生させることができます。これにより、既存のクラスのフィールドとメソッドを自分で記述(そしてデバッグ!)することなく再利用できます。
まとめると以下のようになります。

コードの再利用性を通じてコードの簡潔化を確保するため。

継承の方法

  • extends
    • extends は宣言されたクラスの直接スーパークラス(direct superclass)を指定します
{アクセス修飾子} class A extends ClassType

直接スーパークラス (Direct Superclass)

  • 直接継承されたクラス
  • クラス宣言の extends で指定されたクラスが該当します

推移的閉包 (Transitive Closure)

  • 継承の継承、つまり親クラスの親クラスもスーパークラスとみなします
class A { }          // A は最上位のスーパークラス  
class B extends A { } // B は A の直接サブクラス、A は B の直接スーパークラス  
class C extends B { } // C は B の直接サブクラス、B は C の直接スーパークラス  

A は C のスーパークラス(間接的な継承関係)

  • Object クラスの定義では extends を使用することはできません

    • これに違反するとコンパイルエラーが発生します
    • なぜなら、Object は基本クラス(primordial class)であり、直接スーパークラスを持たないからです
  • ClassType はアクセス可能なクラスでなければなりません

クラス継承のアクセス修飾子(演算子)

  • sealed クラスを継承する場合、許可されたサブクラス(permitted subclass)として明示されている必要があります
  • sealed クラスは必ず以下のキーワードのいずれかで宣言されなければなりません:
    • sealed: 他の制限付き継承を許可
    • non-sealed: 制限なく継承を許可
    • final: これ以上の継承を禁止

image.png

  • non-sealed は継承を許可します

image.png

  • final クラスは継承できません
    image.png

さまざまなクラスの種類

ENUM

  • enum クラスは列挙型のみ継承可能です。
    • enumfinal クラスであるため、他のクラスから拡張することはできません。
    • java.lang.Enum クラスを継承しています

image.png

enum に関しては次回、別途取り上げることにします

record

  • record も継承できません

image.png

Java の継承の使い方と注意点

  • 多重継承は禁止
    • 複数のクラスから同じフィールドを継承する場合に発生する可能性がある曖昧性や複雑性を防ぐため

ダイヤモンド問題(Diamond Problem)

class A {
    void greet() {
        System.out.println("Hello from A");
    }
}

class B extends A {
    @Override
    void greet() {
        System.out.println("Hello from B");
    }
}

class C extends A {
    @Override
    void greet() {
        System.out.println("Hello from C");
    }
}

// 多重継承を仮定: D が B と C を同時に継承
class D extends B, C {
    // D は greet() メソッドを使用したい
}

このような場合、ランタイムで D を作成する際に、B と A を生成すべきか、C と A を生成すべきかが曖昧になります。

これを「ダイヤモンド問題」と呼びます。

ダイヤモンド問題(多重継承禁止)に関する公式チュートリアル

SUPER

  • super というキーワードを使用して、サブクラスからスーパークラスにアクセスできます
  • super は参照変数です
  • 子クラスのコンストラクタでは、必ず最初の行に配置する必要があります
class Outer {
    int secret = 5;
    class Inner {
        int getSecret() { return secret; }
        void setSecret(int s) { secret = s; }
    }
}

class ChildOfInner extends Outer.Inner {
    ChildOfInner(Outer x) {
        x.super(); // Outer インスタンスを渡す
    }
}

public class Test {
    public static void main(String[] args) {
        Outer x = new Outer();
        ChildOfInner a = new ChildOfInner(x);
        ChildOfInner b = new ChildOfInner(x);

        System.out.println(b.getSecret()); // 出力: 5
        a.setSecret(6);
        System.out.println(b.getSecret()); // 出力: 6
    }
}

メソッドオーバーライド

  • スーパークラスのメソッドをサブクラスが新しいロジックで定義したい場合に使用します

    • 再定義が目的であるため、定義しなくても問題ありません
  • @Override は省略可能です

    • @Override を明示すると、コンパイラがそのメソッドが親クラスまたはインターフェースのメソッドを正確にオーバーライドしているかを検査します。(開発者のミスを減らす)
    • 可読性向上(チーム作業や意図の把握が容易になります)
  • 親クラスのメソッドのアクセス修飾子を狭めることはできません

親クラスのメソッド 子クラスのメソッド
public public
protected protected, public
default(パッケージ専用) default, protected, public
class Parent {
    void greet() {} // default
}

class Child extends Parent {
    @Override
    private void greet() {} // コンパイルエラー: アクセス範囲を狭めることはできません
}

@Override を使用すべき理由

class Animal{
    public void sayHello(){}

}

class Dog extends Animal{
    @Override
    public void sayHello(){
        System.out.println("wow bowl");
    }
}  

通常、上記のコードのようにメソッドを定義することができます。

しかし、@Override を削除すると、どのような問題が発生するのでしょうか?

class Animal{
    public void sayHello(){}

}

class Dog extends Animal {
    public void sayHello1(){
        System.out.println("wow bowl");
    }
} 

次のように間違ったメソッドを定義すると、このメソッドは sayHello1 となり、AnimalsayHello にはなりません。

そのため、親クラスのメソッドを再定義していることをコンパイラに伝えるために @Override を使用します。これにより、コンパイル時に検証することができます。

また、他の開発者がコードを見たときに、このロジックが親クラスのメソッドを再定義したものだと一目で理解できます。

以上の理由から、@Override を使用します。

ダイナミックディスパッチ(Dynamic Dispatch)とは?

  • 実行時にメソッド呼び出しを決定する仕組み
  • ポリモーフィズムの核心概念であり、親クラスの参照変数が子クラスのオブジェクトを参照する場合、呼び出されるメソッドは参照変数の型ではなく、実際のオブジェクトの型に基づいて決まります。

スタティックディスパッチ(Static Dispatch)との違い

  • スタティックディスパッチ: コンパイル時に呼び出すメソッドが決定される。
    • メソッドオーバーロード(Method Overloading)で使用される。
  • ダイナミックディスパッチ: 実行時に呼び出すメソッドが決定される。
    • メソッドオーバーライド(Method Overriding)で使用される。
特徴 オーバーロード (Overloading) オーバーライド (Overriding)
決定時点 コンパイル時 実行時
ダイナミックディスパッチ適用 適用されない 適用される
基準 メソッドの引数シグネチャ オブジェクトの実際の型
class Parent {
    void greet() {
        System.out.println("Hello from Parent");
    }
}

class Child extends Parent {
    @Override
    void greet() {
        System.out.println("Hello from Child");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child(); // 親クラス型の参照変数で子クラスのオブジェクトを参照

        parent.greet(); // 出力: Hello from Parent
        child.greet();  // 出力: Hello from Child (ダイナミックディスパッチ)
    }
}
  • 次のように、ランタイムでどの実装クラスを使用するかが判断されます

ダブルディスパッチ (Double Dispatch)

  • Java は基本的にシングルディスパッチのみをサポートします
  • しかし、メソッド呼び出しが2回にわたりオブジェクトの型に応じて動的に決定される仕組みも存在します
  • これをダブルディスパッチと呼びます

ここで例を挙げてみます。

私(Haroya)と学生Aは、勉強した科目の試験を受けるつもりです。

public class StudentA {
    public void exam(String subject) {
        if (subject.equals("science")) {
            System.out.println("A is taking the science exam.");
        } else if (subject.equals("math")) {
            System.out.println("A is taking the math exam.");
        } else {
            System.out.println("A is not taking exam.");
        }
    }
}

public class StudentHaroya {
    public void exam(String subject) {
        if (subject.equals("science")) {
            System.out.println("Haroya is taking the science exam.");
        } else if (subject.equals("math")) {
            System.out.println("Haroya is taking the math exam.");
        } else {
            System.out.println("Haroya is not taking exam.");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        StudentHaroya haroya = new StudentHaroya();
        StudentA a = new StudentA();

        haroya.exam("science");
        a.exam("math");
    }
}

次のように実装すると、以下のような問題が発生します。

  1. 新しい科目が追加されるたびに、すべての学生クラスの if 文を修正する必要があります
  2. 変更に対してオープンで、拡張に対してクローズドなコードになります。(OCP 違反)
  3. ヒューマンエラーを引き起こすリスクがあります
  4. コンパイル時に型の安全性を保証できません
public abstract class Subject {
    public abstract void takeExam(Student student);
}

public class ScienceExam extends Subject {
    @Override
    public void takeExam(Student student) {
        student.takeScienceExam(this);
    }
}

public class MathExam extends Subject {
    @Override
    public void takeExam(Student student) {
        student.takeMathExam(this);
    }
}

// 学生クラス
public abstract class Student {
    abstract void takeScienceExam(ScienceExam exam);
    abstract void takeMathExam(MathExam exam);
}

public class StudentA extends Student {
    @Override
    public void takeScienceExam(ScienceExam exam) {
        System.out.println("A is taking the science exam");
    }

    @Override
    public void takeMathExam(MathExam exam) {
        System.out.println("A is taking the math exam");
    }
}

public class StudentHaroya extends Student {
    @Override
    public void takeScienceExam(ScienceExam exam) {
        System.out.println("Haroya is taking the science exam");
    }

    @Override
    public void takeMathExam(MathExam exam) {
        System.out.println("Haroya is taking the math exam");
    }
}

public class Main {
    public static void main(String[] args) {
        Student haroya = new StudentHaroya();
        Student a = new StudentA();
        
        Subject scienceExam = new ScienceExam();
        Subject mathExam = new MathExam();
        
        scienceExam.takeExam(haroya);  // 1. scienceExam の実際の型に基づいて最初のディスパッチが行われる
        mathExam.takeExam(a);        // 2. student の実際の型に基づいて2回目のディスパッチが行われる
    }
}

ダブルディスパッチの動作原理

  1. 最初のディスパッチ

    • scienceExam.takeExam(haroya) を呼び出すとき
    • 実行時に実際の Subject 型(ScienceExam)に応じて適切な takeExam メソッドが選択されます
  2. 2回目のディスパッチ

    • student.takeScienceExam(this) を呼び出すとき
    • 実行時に実際の Student 型(StudentHaroya)に応じて適切な takeScienceExam メソッドが選択されます

ダブルディスパッチの利点

  1. 型の安全性

    • コンパイル時に型チェックが可能
    • 実行時エラーの可能性が減少
  2. 拡張性

    • 新しい科目や学生タイプの追加が容易
    • 既存のコードを修正せず、新しいクラスを追加するだけで機能を拡張可能
  3. 保守性

    • 条件文を排除し、コードの可読性が向上
    • 各クラスの責任が明確になる
  4. OCP(Open-Closed Principle)の遵守

    • 既存コードを修正することなく、新機能を追加可能

ダブルディスパッチはビジターパターン(Visitor Pattern)の核心メカニズムであり、複雑なオブジェクト構造において型に応じた処理を柔軟に実現することができます。

Visitor Pattern については、後日記述する予定です。

抽象クラス

  • インスタンスを生成できないクラス
    • 自体では存在できません
  • 抽象クラスは抽象メソッドを持つことができます
    • 子クラスで必ず実装する必要があります
abstract class Human {
    // 抽象メソッド(子クラスで実装する必要があります)
    abstract void old();

    // 通常のメソッド(子クラスで継承されます)
    void sleep() {
        System.out.println("zzz");
    }
}

インターフェースとの違い

特性 抽象クラス (Abstract Class) インターフェース (Interface)
インスタンス化 不可能(直接オブジェクトを作成できない) 不可能(直接オブジェクトを作成できない)
抽象メソッド 抽象メソッドを持つことが可能 すべてのメソッドはデフォルトで抽象メソッド(Java 8以降では default メソッドも可能)
多重継承 不可能(単一継承のみ可能) 可能(多重継承をサポート)
フィールド 変数(フィールド)を持つことが可能(ただし、static と final フィールドのみ) 変数(フィールド)**を持つことはできない(定数のみ可能、public static final のみ)
メソッド実装 通常のメソッドと抽象メソッドの両方を持つことが可能 基本的にすべてのメソッドは抽象メソッド(Java 8以降では default メソッドも可能)
コンストラクタ コンストラクタを持つことが可能 コンストラクタを持つことはできない
interface MyInterface {
    void doSomething();  // 抽象メソッド

    // default メソッド
    default void defaultMethod() {
        System.out.println("再定義");
    }
}
  • 再定義を通じて必ず実装しなくても、デフォルトの実装を提供することができます
  • static メソッドはオーバーライドできません
interface MyInterface {
    static void staticMethod() {
        System.out.println("This is a static method in the interface");
    }
}

class MyClass implements MyInterface {
    // staticMethod() は再定義(オーバーライド)することはできません。
    // 以下のコードはコンパイルエラーになります。
    // @Override
    // void staticMethod() { } // エラー発生: static メソッドはオーバーライドできません
}

public class Main {
    public static void main(String[] args) {
        MyInterface.staticMethod();  // インターフェースの static メソッドを呼び出し
    }
}
  • なぜ子クラスで static メソッドをオーバーライドできないのか?
    • 静的メソッド(static メソッド)はクラスメソッドであり、クラスレベルで呼び出されます。
    • インスタンスとは無関係に動作するため、インスタンスレベルの動作であるオーバーライドとは概念的に異なります

final

  • final は一度しか値を割り当てることができず、宣言時に値を初期化しない場合、空の final 変数(blank final variable)と見なされます(値の変更は禁止)
    • 空の初期化変数は必ず明示的に初期化する必要があります
    • もし final 変数が初期化されていない場合、コンパイルエラーが発生します

final の初期化方法

  • 宣言と同時に初期化する
final String name = "Haroya";
  • コンストラクタまたは初期化ブロックで初期化する
final String name;

Example() {
    name = "Haroya";
}
public class Example {
    final String name;

    {
        name = "Haroya";
    }
    
    public Example(){
    }
}
  • final クラスはサブクラスを持つことができません (継承が禁止されます)

    • final クラス内のすべてのメソッドは自動的に final となります
  • final メソッドとして宣言すると、サブクラスがそのメソッドをオーバーライドすることはできません(オーバーライド禁止)

Object

  • java.lang に属しているため、import しなくても自動的に含まれます

image.png

通常であれば、このように static メソッドを呼び出す必要があります

image.png

しかし、java.lang に属するクラスは次のように import なしで使用できます。

  • Object はすべてのクラスの最上位クラスです
    • 明示的に extend Object を記述しなくても使用されます
public class Main {

  static class Haroya{
    public void sayHello(){
      System.out.println("hello");
    }
  }

  public static void main(String[] args){
    Haroya haroya = new Haroya();
    haroya.sayHello();
  }
}

次のように定義されたクラスのバイトコードを見ると、

image.png

次のように Object のコンストラクタが呼び出されていることがわかります

主なメソッド

メソッド名 説明
clone() オブジェクトを複製して新しいオブジェクトを返します。(ただし、Cloneable インターフェースを実装する必要があります。)
equals(Object obj) 2つのオブジェクトが等しいかどうかを比較します。デフォルトでは参照を比較しますが、オーバーライドして値を比較できます。
finalize() オブジェクトが GC(ガベージコレクション) によって削除される前に呼び出されます。(推奨されません)
getClass() オブジェクトのランタイムクラス情報を返します。
hashCode() オブジェクトのハッシュコードを返します。equals() が true のオブジェクトは同じハッシュコードを持つべきです。
notify() 同じオブジェクトを待機しているスレッドのうち1つを起こします。
notifyAll() 同じオブジェクトを待機しているすべてのスレッドを起こします。
toString() オブジェクトの文字列表現を返します。デフォルト実装はクラス名とハッシュコードを返します。
wait() 現在のスレッドを待機状態にします。
wait(long timeout) 指定された時間、現在のスレッドを待機状態にします。
wait(long timeout, int nanos) 指定された時間とナノ秒の間、現在のスレッドを待機状態にします。

래퍼런스

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?