読み込み中...

Javaでの抽象メソッドの実装とその詳細な使い方の10選

Java言語での抽象メソッドの実装のイラスト Java
この記事は約35分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Javaで抽象メソッドを扱う結論は、共通の呼び出し方だけを親側に置き、具体的な処理を子クラスへ任せる設計を選ぶことにあります。初心者がつまずきやすいのは、abstractを付ける位置、@Overrideで守るべきシグネチャ、抽象クラスとインターフェースの使い分けです。

その理解が進むと、Javaの抽象メソッドは単なる構文ではなく、実装の差し替えや拡張の入口として整理できます。上級者向けの設計でも、extendsimplementspublicprotectedthrowsなどの細部を崩さないことが保守性につながりますが、これは押さえたい点です。

公式ドキュメントによれば、抽象クラスや抽象メソッドは継承先での具体化を前提にした仕組みです。詳細はOracle Java TutorialsのAbstract Methods and Classesと、言語仕様のJava Language Specification Chapter 8で確認できます。

動作確認環境
  • Java SE 21 / javac 21
  • 標準ライブラリのみ使用
📖 この記事で学べること
  • Javaの抽象メソッドと通常メソッドの違い
  • 抽象クラスでの実装、引数、返り値、例外の扱い
  • 継承、オーバーライド、インターフェース連携の詳細
  • 初心者が遭遇しやすいコンパイルエラーの対処
  • 上級者が設計時に確認したいカスタマイズの観点

Javaと抽象メソッドの基本

Javaはクラスを中心にプログラムを組み立てる言語で、共通の性質を親クラスに寄せ、差分を子クラスに置く書き方と相性があります。そのため、抽象メソッドを使う場面では、処理内容よりも「どのメソッドを必ず用意させるか」を先に決める考え方になります。

これにより、呼び出し側はAnimalDataOperationのような共通型を見ればよく、実際の処理がDogFileDataOperationQuickSortのどれかを細かく知る必要が減りますし、これが一つの目安です。関連する基礎として、コレクション設計はJava List型完全ガイド、メタ情報の付け方はJavaアノテーションの解説も参考になります。

一般に、抽象メソッドはメソッド本体を持たず、末尾を;で閉じます。通常メソッドは{}の中に処理を書きますが、抽象メソッドではreturnSystem.out.println、計算処理などを子クラス側の実装へ移するのが目安です。

ただし、抽象クラスは通常メソッドやフィールドを同時に持てます。共通処理をshowInfo()に置き、個別処理だけをsetPrice(int price)へ分離する形にすると、初心者にも役割の境界が読み取りやすくなります。

観点主な構文役割注意点関連例
抽象クラスabstract class共通処理と契約をまとめるnewで直接生成できないAbstractClass
抽象メソッドabstract void子クラスへ実装を要求する本体を書かないabstractMethod()
継承extends親の型と処理を引き継ぐ単一継承になるConcreteClass
インターフェースinterface公開する操作を定義する状態共有は慎重に扱うAnimal
実装宣言implementsインターフェースの契約を満たす未実装ならエラーになるDog
オーバーライド@Override親のメソッドを子で具体化する引数と戻り値を合わせるsound()
戻り値String処理結果の型を契約に含める互換性のない型は使えないread()
引数int price外部データを受け取る型と順序を変えないsetPrice
例外throws Exception失敗の可能性を宣言する広い検査例外に変えないabstractMethod
可視性publicどこからでも呼べる公開範囲が広いpublicMethod
保護可視性protected継承先へ公開する同一パッケージにも見えるprotectedMethod
非公開privateクラス内部だけに閉じる抽象メソッドには使えないprivateMethod
コンストラクタpublic Dog()初期状態を作る抽象クラスも定義できるConcreteProduct
フィールドprivate String data状態を保持する公開しすぎないdata
配列int[]複数値を扱う破壊的変更に注意するsort
出力System.out.println学習用に動きを確認する業務ロジックへ直書きしないmain
エントリポイントpublic static void mainサンプルの起点になるクラス名とファイル名を合わせるMain.java
戻り値なしvoid副作用中心の処理を表す結果が必要なら型を返すdelete()
文字列Stringテキストを扱うnullに注意するname
整数int価格や件数を扱う小数は扱えないprice
オブジェクトObject幅広い値を受ける型情報が粗くなるevent
代入this.dataインスタンス状態を更新する共有状態の変更に注意するupdate
削除表現null参照がない状態を表す呼び出し前に確認するdelete
戦略切替setStrategy処理方式を差し替える共通型へ依存するContext
生成new具象クラスのインスタンスを作る抽象クラスは生成できないnew Dog()
呼び出しobj.method()実装済み処理を動かす参照型と実体型を区別するdog.greet()
コンパイルjavac構文と型を検査する未実装は検出されるjavac Main.java
実行javaクラスを起動するクラスパスを確認するjava Main
パッケージpackage名前空間を分ける配置と宣言を合わせるcom.example
詳細設計final変更不能な要素を示す抽象メソッドとは併用できないfinal class

Javaの抽象メソッドを読む前提

基本的に、抽象メソッドは「処理の名前、引数、戻り値を先に決める道具」と考えると理解しやすくなります。処理本体を空にするのではなく、構文として本体を書かないため、abstract void run();のように宣言だけで終わる点が通常メソッドとの違いです。

一方、抽象メソッドを含む抽象クラスは、共通処理を持てる点でインターフェースだけの設計と異なります。たとえばJavaのオーバーライド解説で扱う再定義のルールを理解しておくと、抽象メソッドの実装ミスを減らせますが、覚えておくと役立つでしょう。

一方、抽象メソッドを導入する前に、通常メソッドで十分かどうかを確認する視点も欠かせません。すべての子クラスで同じ処理になるなら通常メソッドのほうが読みやすく、子クラスごとに差が出る処理だけを抽象メソッドにします。

その判断では、将来の拡張予定よりも現在の責務を優先します。まだ具体的な差分が見えていない段階で抽象化を広げると、実装クラスが空のメソッドを抱え、初心者にも上級者にも意図が読み取りにくい構造になるのがポイントです。

具体的には、入力値の検証、ログ出力、共通の前処理は抽象クラス側の通常メソッドに置き、保存形式や計算方式のように変化する部分を抽象メソッドへ分けます。こうした境界を先に決めると、サンプルコードから実用的な設計へ移すときも迷いが減ります。

ただし、抽象クラスに状態を持たせる場合は、子クラスがその状態をどう変更するかまで考える必要があるのが一般的です。protectedフィールドで直接触らせる方法は短く書けますが、詳細な制御が必要ならgettersetterを通して変更範囲を狭めます。

これらの判断をチームで共有する場合は、抽象メソッドを作る理由を名前に反映させます。process()のように広すぎる名前は詳細が読みにくいため、createreadonEventsortのように目的が伝わる動詞を選びますし、ここを基本と考えるとよいでしょう。

その結果、実装クラスの責務も自然に絞られます。Javaの抽象メソッドは継承先へ作業を押し付けるための構文ではなく、呼び出し側と実装側の約束を明文化するための構文と整理できます。

具体的には、抽象メソッド名だけで処理の目的が伝わる状態を目指するのが現実的です。名前が曖昧なまま実装を増やすと、呼び出し側の意図と子クラス側の詳細がずれやすくなります。この確認は長期保守にも効きますし、ここがポイントです。

抽象メソッドの正確な実装

Javaの抽象メソッドを正確に実装するには、親側のabstract宣言、子側の@Override、呼び出し側の具象クラス生成を分けて確認します。最小構成は次のサンプルコードで、抽象クラスに宣言したメソッドを子クラスで具体化していると整理できます。

基本的な実装方法

具体的には、抽象クラスにabstractMethod()を宣言し、具象クラスで同じシグネチャのメソッドを書きます。このときアクセス修飾子を狭くしたり、戻り値を別型にしたりすると、コンパイル時の検査で弾かれます。

サンプルコード1:基本的な抽象メソッドの実装

// 抽象クラスの作成
abstract class AbstractClass {
    // 抽象メソッドの宣言
    abstract void abstractMethod();
}

// サブクラスで抽象メソッドをオーバーライド
class ConcreteClass extends AbstractClass {
    // 抽象メソッドの具体的な実装
    void abstractMethod() {
        System.out.println("抽象メソッドの具体的な実装");
    }
}

public class Main {
    public static void main(String[] args) {
        // インスタンスの作成と抽象メソッドの呼び出し
        ConcreteClass obj = new ConcreteClass();
        obj.abstractMethod();
    }
}

結果: 期待される出力は「抽象メソッドの具体的な実装」です。

このコードでは、AbstractClassが処理名だけを定め、ConcreteClassが具体的な表示処理を持ちます。そのため、呼び出し側はnew ConcreteClass()で作ったインスタンスに対して、実装済みのabstractMethod()を呼び出せます。

ただし、同じファイルにpublic class Mainを書く場合、ファイル名は一般的にMain.javaに合わせますし、ここがポイントです。初心者は抽象メソッドそのものより、ファイル名、クラス名、publicの組み合わせで迷うことがあるため、エラー文を切り分けて読むとよいでしょう。

引数を持つ抽象メソッド

抽象メソッドには引数を含められます。引数を契約に入れると、どの具象クラスでも同じ形式でデータを受け取り、内部の実装だけを切り替えられます。

その代表例として、商品価格を設定するsetPrice(int price)を抽象メソッドにすると理解できます。価格の保存先や表示方法は具象クラスに任せつつ、呼び出し側の書き方は同じ形に保てます。

サンプルコード2:引数を持つ抽象メソッドの実装

// 商品を表す抽象クラス
public abstract class Product {
    // 商品の名前
    protected String name;

    // 商品の価格を設定する抽象メソッド(引数を持つ)
    public abstract void setPrice(int price);

    // 商品の名前を取得するメソッド
    public String getName() {
        return name;
    }

    // 商品の情報を表示するメソッド
    public void showInfo() {
        System.out.println("商品名: " + name);
    }
}

結果: この断片は抽象クラスの定義であり、単体では画面出力を行いません。

この定義では、Productnameという共通フィールドとshowInfo()という通常メソッドを持ちます。一方で、価格設定の詳細はsetPrice(int price)として子クラスへ移しているため、商品種別ごとに異なる処理へ分岐できます。

// Productクラスを継承した具象クラス
public class ConcreteProduct extends Product {

    // setPriceメソッドをオーバーライドして具体的な実装を提供
    @Override
    public void setPrice(int price) {
        System.out.println("商品の価格を設定: " + price + "円");
    }

    // コンストラクタで商品の名前を設定
    public ConcreteProduct(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        // ConcreteProductクラスのインスタンスを作成
        ConcreteProduct product = new ConcreteProduct("テスト商品");

        // setPriceメソッドを呼び出し
        product.setPrice(1000);

        // showInfoメソッドを呼び出し
        product.showInfo();
    }
}

結果: 期待される出力は、価格設定の行と商品名の行です。

商品の価格を設定: 1000円
商品名: テスト商品

結果: 期待される出力例として、価格と商品名がこの順序で並びます。

この流れでは、ConcreteProductsetPriceを実装し、Productから継承したshowInfoも利用しています。そのため、抽象メソッドで差分を強制しながら、共通処理を再利用する構成になると覚えるとよいでしょう。

返り値を持つ抽象メソッド

返り値を持つ抽象メソッドでは、子クラスが返す値の型まで契約に含まれます。たとえばString sound()と宣言した場合、具象クラスは文字列を返す実装を用意します。

一方、voidの抽象メソッドは戻り値を持たないため、出力、保存、状態更新などの副作用が中心になると考えられます。返り値を設計に含めると、呼び出し側で結果を受け取り、別の処理へつなげられます。

サンプルコード3:返り値を持つ抽象メソッドの実装

// 抽象クラスを定義します
abstract class Animal {
    // 抽象メソッドを定義します。このメソッドは返り値としてString型のデータを返す契約を作成すると言えるでしょう。
    abstract String sound();
}

// 抽象クラスを継承した具体的なクラスを定義します
class Dog extends Animal {
    // 抽象メソッドを具体的に実装するのが基本です。この例では「ワンワン」という文字列を返します。
    @Override
    String sound() {
        return "ワンワン";
    }
}

public class Main {
    public static void main(String[] args) {
        // Dogクラスのインスタンスを作成し、soundメソッドを呼び出します
        Dog dog = new Dog();
        System.out.println(dog.sound()); // 出力: ワンワン
    }
}

結果: 期待される出力は「ワンワン」です。

このサンプルコードでは、Animalsound()という返り値ありの抽象メソッドを定めています。Dogは同じ戻り値型でオーバーライドし、mainから呼び出した値をSystem.out.printlnに渡するのが基本です。

これを応用すると、計算結果を返すcalculate()、読み込み結果を返すread()、判定結果を返すisValid()のような契約も作れます。詳細な設計では、戻り値がnullになり得るか、例外で失敗を表すかも合わせて決めます。

同様に、抽象メソッドを使った設計では、テストコードから見た呼び出しやすさも考えますが、これは押さえたい点です。親型で扱える設計にしておくと、テスト用の簡単な実装クラスを作り、処理の境界だけを確認できます。

そのため、戻り値を返す抽象メソッドは、出力だけに依存する処理より検証しやすい場合があります。System.out.printlnで確認する学習用のサンプルコードから、戻り値を比較するテストへ移すと、詳細な動作の確認がしやすくなるのが目安です。

一方で、イベント通知やファイル更新のように副作用が中心になる処理では、voidの抽象メソッドも自然な選択です。戻り値の有無は好みではなく、呼び出し側が何を必要とするかで決めます。

抽象メソッドの詳細な使い方

抽象メソッドの詳細な使い方では、継承階層とオーバーライドの規則を分けて見る必要があります。Javaでは親型の変数に子クラスのインスタンスを代入できるため、抽象メソッドはポリモーフィズムと一緒に理解すると扱いやすくなるのがポイントです。

その考え方は、うるう年判定のように条件分岐を整理する場合にも通じます。条件の書き方はJavaでうるう年を判定する解説、文字列出力の扱いはJavaエスケープ処理の解説と合わせて読むと、サンプルコードの意図が追いやすくなります。

抽象メソッドの継承

抽象メソッドを持つ抽象クラスを継承した具象クラスは、未実装のメソッドをすべて具体化するのが一般的です。具体化しない場合、その子クラスもabstract classとして宣言しなければなりません。

このとき、親クラスの設計は「子クラスに必ず守らせる入口」を作る役割を持ちます。上級者が設計する場合でも、抽象メソッドを増やしすぎると実装側の負担が増えるため、変化しやすい処理だけに絞る判断が必要になります。

サンプルコード4:抽象メソッドの継承の実例

abstract class AbstractClass {
    abstract void abstractMethod();
}

結果: この断片は抽象クラスの定義であり、単体では出力を行いません。

これが親側の契約です。AbstractClassabstractMethod()の存在だけを示し、具体的な処理は何も持ちません。

class ConcreteClass extends AbstractClass {
    @Override
    void abstractMethod() {
        System.out.println("抽象メソッドのオーバーライド成功!");
    }
}

結果: この断片は具象クラスの定義であり、呼び出しコードと組み合わせると表示処理が使えます。

その子クラスでは、extends AbstractClassによって親の契約を引き継ぎ、@Override付きで処理を補います。@Overrideは必須構文ではありませんが、メソッド名や引数のずれを検出しやすくするのが現実的です。

public class Main {
    public static void main(String[] args) {
        ConcreteClass concreteClass = new ConcreteClass();
        concreteClass.abstractMethod();
    }
}

結果: 期待される出力は「抽象メソッドのオーバーライド成功!」です。

この呼び出し側では、具象クラスをnewで生成し、実装済みのメソッドを呼びます。抽象クラスそのものを生成しない点を押さえると、抽象メソッドの継承は自然に理解できます。

抽象メソッドのオーバーライド

抽象メソッドのオーバーライドでは、親のシグネチャを保ったまま、子クラスごとの処理だけを変えますし、これが一つの目安です。JavaはUnicode識別子を許容するため日本語名のクラスも書けますが、共同開発では英数字の命名規約にそろえることが一般的です。

ただし、学習用のサンプルコードとしては、日本語名により「動物」「犬」「猫」の関係が読み取りやすくなります。実務寄りのコードへ移すときは、AnimalDogCatのような命名に置き換えるとよいでしょう。

サンプルコード5:オーバーライドによる抽象メソッドの変更例

abstract class 動物 {
    abstract void 鳴く();
}

結果: この断片は抽象クラスの定義であり、単体では出力を行いません。

class 犬 extends 動物 {
    @Override
    void 鳴く() {
        System.out.println("ワンワン");
    }
}

class 猫 extends 動物 {
    @Override
    void 鳴く() {
        System.out.println("ニャーン");
    }
}

結果: この断片は具象クラスの定義であり、呼び出しコードと組み合わせると犬と猫の処理を切り替えられます。

これらのクラスは同じ鳴く()を持ちますが、出力する文字列が異なります。同じ呼び出し名で異なる処理へ分かれるため、抽象メソッドはポリモーフィズムの入口として機能すると整理できます。

public class Main {
    public static void main(String[] args) {
        動物 いぬ = new 犬();
        いぬ.鳴く(); // ワンワンと出力される

        動物 ねこ = new 猫();
        ねこ.鳴く(); // ニャーンと出力される
    }
}

結果: 期待される出力は「ワンワン」と「ニャーン」です。

この呼び出しでは、変数の型はどちらも動物ですが、実体はです。そのため、同じ鳴く()の呼び出しでも、実体側の実装が選ばれます。

💡 Tips: 抽象メソッドの理解では、変数の型とインスタンスの実体を分けて読むと混乱が減ります。動物 いぬ = new 犬();は、呼び出し口を親型にそろえ、処理内容を子型に任せる書き方です。

応用例とサンプルコード

応用例では、抽象メソッドを「差し替え可能な処理の入口」として使いると理解できます。データ操作、イベント処理、デザインパターンのように、呼び出し手順は似ているが内部処理が変わる場面でサンプルコードが役立ちます。

その構造を意識すると、初心者は文法の暗記から離れやすくなり、上級者は依存関係の整理や拡張時の影響範囲を考えやすくなります。抽象メソッドの詳細は、単独の構文ではなく周辺クラスとの関係で読み解くのが自然です。

データ操作の抽象化

データ操作では、作成、読み込み、更新、削除という流れを共通のメソッド名でそろえると、実装の差し替えがしやすくなると覚えるとよいでしょう。たとえばファイル、メモリ、データベースで保存先が変わっても、呼び出し側はcreatereadupdatedeleteを使えます。

一方、すべてを抽象化すればよいわけではありません。保存先ごとの制約が大きい場合は、共通化する範囲を最小限にし、個別処理の詳細を具象クラスへ閉じ込めます。

サンプルコード6:データ操作を抽象化する実例

abstract class DataOperation {
    abstract void create(String data);
    abstract String read();
    abstract void update(String data);
    abstract void delete();
}

class FileDataOperation extends DataOperation {
    private String data;

    @Override
    void create(String data) {
        this.data = data;
        System.out.println("データを作成しました:" + data);
    }

    @Override
    String read() {
        System.out.println("データを読み込みました:" + data);
        return data;
    }

    @Override
    void update(String data) {
        this.data = data;
        System.out.println("データを更新しました:" + data);
    }

    @Override
    void delete() {
        data = null;
        System.out.println("データを削除しました");
    }
}

public class Main {
    public static void main(String[] args) {
        DataOperation dataOperation = new FileDataOperation();

        dataOperation.create("初期データ");
        dataOperation.read();
        dataOperation.update("更新データ");
        dataOperation.delete();
    }
}

結果: 期待される出力は、作成、読み込み、更新、削除の順に表示される内容です。

データを作成しました:初期データ
データを読み込みました:初期データ
データを更新しました:更新データ
データを削除しました

結果: 期待される出力例として、各操作のメッセージが順番に並びます。

このサンプルコードでは、変数の型をDataOperationにしている点が要点です。実体はFileDataOperationですが、呼び出し側が抽象型に依存しているため、別の実装へ入れ替える余地が残ります。

イベント処理の抽象化

イベント処理では、「イベントを受け取ったら何をするか」という処理を抽象メソッドにできると考えられます。JavaのGUIやサーバーサイドの設計でも、通知を受ける側の処理を共通化する考え方がよく使われます。

ただし、イベントの型をObjectにすると受け取れる値は広がりますが、詳細な型情報は弱くなります。上級者向けの設計では、独自のイベントクラスやジェネリクスを使って、型安全性を高める選択肢もあると言えるでしょう。

サンプルコード7:イベント処理を抽象化する実例

abstract class AbstractEventListener {
    // イベントが発生した際に呼び出される抽象メソッド
    public abstract void onEvent(Object event);
}

class CustomEventListener extends AbstractEventListener {
    // イベントが発生した際の具体的な処理を実装
    public void onEvent(Object event) {
        System.out.println("イベントが発生しました: " + event.toString());
    }
}

public class Main {
    public static void main(String[] args) {
        // イベントリスナーのインスタンスを生成
        CustomEventListener listener = new CustomEventListener();

        // イベントをシミュレート
        listener.onEvent("カスタムイベント");
    }
}

結果: 期待される出力は「イベントが発生しました: カスタムイベント」です。

この例では、AbstractEventListenerがイベント受信の入口を定め、CustomEventListenerが具体的な反応を実装しています。そのため、別のイベント処理を追加したい場合は、新しい子クラスでonEventの中身を変えられます。

⚠️ 注意: event.toString()eventnullの場合に例外を起こするのが基本です。実装ではnullを受け取る可能性を先に決め、必要なら条件分岐を追加します。

デザインパターンとの関連

デザインパターンでは、変化する処理を抽象メソッドとして切り出す場面があります。Strategyパターンでは、アルゴリズムを共通型として扱い、実行時に処理方式を差し替えますが、覚えておくと役立つでしょう。

これにより、呼び出し側のContextは、具体的なソート方法を細かく知る必要がありません。実装の詳細をBubbleSortQuickSortへ閉じ込めることで、コードの見通しを保ちやすくなります。

サンプルコード8:デザインパターンにおける抽象メソッドの活用例

// 抽象ストラテジークラス
abstract class SortingStrategy {
    public abstract void sort(int[] array);
}

// 具体ストラテジークラス1
class BubbleSort extends SortingStrategy {
    public void sort(int[] array) {
        // バブルソートのアルゴリズムをここに実装
    }
}

// 具体ストラテジークラス2
class QuickSort extends SortingStrategy {
    public void sort(int[] array) {
        // クイックソートのアルゴリズムをここに実装
    }
}

// コンテキストクラス
class Context {
    private SortingStrategy strategy;

    public Context(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void executeStrategy(int[] array) {
        strategy.sort(array);
    }
}

結果: この断片はStrategyパターンの構造例であり、ソート処理本体が空のため出力はありません。

このサンプルコードでは、SortingStrategysort(int[] array)という入口を定めています。ContextSortingStrategy型だけを保持するため、setStrategyで戦略を切り替えられます。

一方、学習用のコードではアルゴリズム本体を省略しているのが目安です。実装を完成させる場合は、BubbleSortQuickSortの中に配列を並べ替える処理を入れ、テストで順序を確認します。

抽象メソッドの注意点と対処法

抽象メソッドの注意点は、ほとんどがコンパイル時に見つかります。未実装、シグネチャ不一致、例外宣言の拡大、アクセス修飾子の縮小は、初心者が遭遇しやすい代表的なエラーです。

そのため、エラー文を読むときは、親クラスの宣言と子クラスの実装を横に並べて確認するのがポイントです。詳細な原因を探る際は、メソッド名、引数の型と順序、戻り値、throws、可視性の順に見ると切り分けやすくなります。

実装を忘れてしまった時のエラーと対処

抽象クラスを継承した具象クラスが抽象メソッドを実装しない場合、具象クラスとして成立しません。対処は、メソッドを実装するか、そのクラスもabstractとして宣言するかのどちらかです。

ただし、具象クラスとしてインスタンス化したいなら、未実装を残さない方法を選びます。IDEの補完機能で抽象メソッドを生成すると、シグネチャの写し間違いも減らせますし、ここを基本と考えるとよいでしょう。

// 抽象クラスの定義
abstract class AbstractClass {
    abstract void abstractMethod();
}

// 抽象メソッドの実装を忘れたクラス
class ConcreteClass extends AbstractClass {
    // ここでabstractMethod()の実装を忘れているため、エラーが発生します。
}

// 解決法
// 抽象メソッドを実装することでエラーを解消できます。
class CorrectConcreteClass extends AbstractClass {
    @Override
    void abstractMethod() {
        System.out.println("抽象メソッドの実装");
    }
}

結果: ConcreteClassはコンパイルエラーが期待され、CorrectConcreteClassは呼び出しコードを追加すると「抽象メソッドの実装」を出力できるのが一般的です。

このコードは、間違いと修正を同じ断片に置いた説明用の例です。実際にコンパイルする場合は、エラーになるConcreteClassを削除するかabstractに変更し、正しいクラスだけを残します。

オーバーライドのルール違反時のエラーとその対処

オーバーライドでは、親より広い検査例外を子クラス側で投げる宣言にできません。親がthrows Exceptionなら、子はExceptionかそのサブクラスに抑える必要があります。

一方、非検査例外であるRuntimeExceptionは扱いが異なるのが現実的です。詳細な例外設計では、呼び出し側に処理を強制したい失敗か、プログラム上の誤用として扱う失敗かを分けます。

// 抽象クラスの定義
abstract class AbstractClassWithException {
    abstract void abstractMethod() throws Exception;
}

// 抽象メソッドのオーバーライドで例外のルールを違反したクラス
class WrongConcreteClass extends AbstractClassWithException {
    @Override
    // ここでExceptionより上位の例外Throwableをスローしているため、エラーが発生します。
    void abstractMethod() throws Throwable {
        throw new Throwable("エラー");
    }
}

// 解決法
// 例外のルールを守って抽象メソッドをオーバーライドする
class CorrectConcreteClassWithException extends AbstractClassWithException {
    @Override
    void abstractMethod() throws Exception {
        throw new Exception("エラー");
    }
}

結果: WrongConcreteClassはコンパイルエラーが期待され、CorrectConcreteClassWithExceptionは親の例外宣言と整合すると整理できます。

この例では、ThrowableExceptionより広い型である点が問題になります。対処として、親と同じExceptionにするか、より具体的な例外型にします。

ℹ️ 補足: 抽象メソッドのエラーは、実装側だけでなく親側の契約にも原因がある場合があると理解できます。メソッド名、戻り値、引数、例外、アクセス修飾子を一組として確認します。

抽象メソッドの利用上の実践的な考え方

抽象メソッドを使うときは、先に変化する処理と変化しにくい処理を分けます。共通の処理は抽象クラスの通常メソッドに残し、変化する処理だけを抽象メソッドへ切り出すと、実装クラスの負担が小さくなると覚えるとよいでしょう。

  • 共通処理は通常メソッド、差分は抽象メソッドに分ける
  • @Overrideを付け、シグネチャのずれを早く見つける
  • 抽象メソッドを増やす前に、インターフェースや委譲で表せないか確認する

こうした分け方を守ると、初心者でもコードの責務を読み取りやすくなります。上級者の場合は、テストしやすさ、依存方向、公開APIとして固定してよいかまで含めて判断します。

実際の開発では、抽象メソッドを公開APIに含めると、そのシグネチャ変更が継承先へ広く影響すると考えられます。メソッド名、引数、戻り値を後から変える可能性が高い場合は、抽象化の範囲を小さく保つほうが扱いやすくなります。

そのため、抽象メソッドの追加は、既存の実装クラスがすべて同じ契約を自然に満たせるかを確認してから行います。無理に共通化すると、使わない引数や空の処理が増え、詳細なコードレビューでも意図を説明しにくくなると言えるでしょう。

抽象メソッドのカスタマイズ方法

抽象メソッドのカスタマイズでは、アクセス修飾子、インターフェース連携、戻り値、例外、命名規約を調整します。Javaでは構文として許される範囲と、設計として読みやすい範囲が必ずしも一致しないため、利用者から見た呼び出しやすさも考えます。

その中でもアクセス修飾子は、どこまで公開するかを決める直接的な手段です。抽象メソッドは継承先で実装される必要があるため、private abstractのような組み合わせは意味が矛盾し、コンパイルできません。

アクセス修飾子を用いた制限

アクセス修飾子を選ぶときは、外部APIとして公開するならpublic、継承先だけに使わせたいならprotectedを検討するのが基本です。パッケージ内に閉じる場合は修飾子なしも候補になりますが、パッケージ設計との整合が必要です。

逆に、privateはサブクラスから見えないため、抽象メソッドには使えません。詳細な設計では、公開範囲を狭くしたい意図と、子クラスに実装させる意図が衝突していないか確認します。

サンプルコード9:アクセス修飾子を活用したカスタマイズ例

// 抽象クラスAbstractClassを作成
public abstract class AbstractClass {

    // publicな抽象メソッドを定義
    public abstract void publicMethod();

    // protectedな抽象メソッドを定義
    protected abstract void protectedMethod();

    // privateな抽象メソッドは作成できません(コンパイルエラーとなります)
    // private abstract void privateMethod();
}

結果: publicMethodprotectedMethodは子クラスでの実装が期待され、コメントを外したprivate abstractはコンパイルエラーになります。

このサンプルコードは、抽象メソッドの可視性を比較するためのものです。publicは外部からの呼び出しも想定し、protectedは継承先での利用を中心に考えます。

インターフェースとの連携

Javaのインターフェースに宣言する通常のメソッドは、暗黙的に抽象メソッドとして扱われますし、ここがポイントです。抽象クラスが共通処理や状態を持てるのに対し、インターフェースは型としての契約を広く共有しやすい点に特徴があります。

ただし、現在のJavaではインターフェースにdefaultメソッドやstaticメソッドも書けます。そのため、すべてが抽象メソッドになるわけではなく、メソッド種別を見分ける必要があるのが目安です。

サンプルコード10:インターフェースと抽象メソッドを組み合わせた実装例

// Animalというインターフェースを作成します
public interface Animal {
    // greetという抽象メソッドを定義します
    void greet();
}

結果: この断片はインターフェースの定義であり、単体では出力を行いません。

この定義では、greet()の本体を書いていません。インターフェースを実装するクラスは、このメソッドに具体的な処理を用意します。

// Dogクラスを作成し、Animalインターフェースを実装します
public class Dog implements Animal {
    // greetメソッドをオーバーライドして具体的な実装を行います
    @Override
    public void greet() {
        // 犬特有のあいさつを行う
        System.out.println("ワンワン");
    }
}

結果: この断片はDogクラスの定義であり、呼び出しコードと組み合わせると「ワンワン」を出力できます。

この実装では、implements Animalによってgreet()の契約を満たするのがポイントです。インターフェースのメソッドは外部公開を前提にするため、実装側のgreet()publicにします。

public class Main {
    public static void main(String[] args) {
        // Dogクラスのインスタンスを生成します
        Dog dog = new Dog();

        // greetメソッドを呼び出します
        dog.greet(); // 出力: ワンワン
    }
}

結果: 期待される出力は「ワンワン」です。

この呼び出しコードではDog型で変数を持っていますが、設計によってはAnimal dog = new Dog();とも書けます。インターフェース型で受けると、別の実装クラスへ差し替えやすくなります。

ただし、抽象クラスとインターフェースを同時に使う場合、責務が重複しないようにするのが一般的です。状態と共通処理は抽象クラス、複数の型へ横断的に持たせる契約はインターフェースに寄せると整理しやすくなります。

まとめ

Javaでの抽象メソッドの実装は、親側で契約を作り、子側で処理を具体化する考え方に集約できます。抽象クラスは共通処理と差分処理を同じ階層で扱えるため、継承関係が自然に表せる場面で力を発揮するのが現実的です。

その一方で、契約だけを広く共有したい場合はインターフェースも選択肢になります。初心者はabstract classabstractメソッド、@Overrideの対応関係を押さえ、上級者は公開範囲、例外、依存方向、テスト容易性まで含めて設計するとよいでしょう。

具体的なサンプルコードを読むときは、親の宣言、子の実装、呼び出し側の型を分けて追います。詳細なエラー対処では、未実装、シグネチャ不一致、例外宣言、アクセス修飾子の順に確認すると、原因を絞り込みやすくなると整理できます。

抽象メソッドは、すべての設計に使う道具ではありません。変化する処理を明確に分離したい場面で採用し、共通化しすぎて実装クラスが窮屈にならないように調整することが、読みやすいJavaコードにつながります。

関連記事

著者: Japanシーモア編集部

Japanシーモアは、Web/IoT/APP/SYS 分野のプログラミング情報を体系的に提供するメディアです。本記事は編集部による執筆とAI支援を組み合わせて制作し、公開前に編集部が校正しています。誤りや改善案がございましたらお問い合わせよりご連絡ください。

※本記事は実在のエンジニア複数名で構成される Japanシーモア編集部が、AI支援を活用して作成・校正・公開しています。