はじめに
現在、私が進行している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
: これ以上の継承を禁止
-
-
non-sealed
は継承を許可します
さまざまなクラスの種類
ENUM
-
enum
クラスは列挙型のみ継承可能です。-
enum
はfinal
クラスであるため、他のクラスから拡張することはできません。 -
java.lang.Enum
クラスを継承しています
-
enum
に関しては次回、別途取り上げることにします
record
-
record
も継承できません
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
となり、Animal
の sayHello
にはなりません。
そのため、親クラスのメソッドを再定義していることをコンパイラに伝えるために @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");
}
}
次のように実装すると、以下のような問題が発生します。
- 新しい科目が追加されるたびに、すべての学生クラスの
if
文を修正する必要があります - 変更に対してオープンで、拡張に対してクローズドなコードになります。(OCP 違反)
- ヒューマンエラーを引き起こすリスクがあります
- コンパイル時に型の安全性を保証できません
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回目のディスパッチが行われる
}
}
ダブルディスパッチの動作原理
-
最初のディスパッチ
-
scienceExam.takeExam(haroya)
を呼び出すとき - 実行時に実際の Subject 型(ScienceExam)に応じて適切な
takeExam
メソッドが選択されます
-
-
2回目のディスパッチ
-
student.takeScienceExam(this)
を呼び出すとき - 実行時に実際の Student 型(StudentHaroya)に応じて適切な
takeScienceExam
メソッドが選択されます
-
ダブルディスパッチの利点
-
型の安全性
- コンパイル時に型チェックが可能
- 実行時エラーの可能性が減少
-
拡張性
- 新しい科目や学生タイプの追加が容易
- 既存のコードを修正せず、新しいクラスを追加するだけで機能を拡張可能
-
保守性
- 条件文を排除し、コードの可読性が向上
- 各クラスの責任が明確になる
-
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 クラス内のすべてのメソッドは自動的に
-
final
メソッドとして宣言すると、サブクラスがそのメソッドをオーバーライドすることはできません(オーバーライド禁止)
Object
-
java.lang
に属しているため、import
しなくても自動的に含まれます
通常であれば、このように static
メソッドを呼び出す必要があります
しかし、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();
}
}
次のように定義されたクラスのバイトコードを見ると、
次のように 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) | 指定された時間とナノ秒の間、現在のスレッドを待機状態にします。 |
래퍼런스