本ページはプロモーションが含まれています

Java入門

Java抽象クラス(abstract)を理解!インターフェースとの違いも解説

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

Javaの学習を進めていると、「抽象クラス」という言葉に出会うと思います。

「普通のクラスと何が違うの?」

「インターフェースと似ているけど、どう使い分けるの?」

「そもそも、なぜ抽象クラスなんてものが必要なの?」

私自身、Javaを学び始めたころは同じ疑問を抱えていました。参考書を読んでも、いまいちピンと来なかった経験があります。しかし、実務でコード設計に携わるようになって初めて、抽象クラスがコードの再利用性を高め、チーム開発を円滑にするための強力な武器であることに気づいたのです。

この記事は、過去の私と同じようにJavaの抽象クラスについて知りたい、理解を深めたいと考えている方に向けて書きました。

この記事を読み終える頃には、あなたは以下の状態になっているでしょう。

  • Javaの抽象クラスの基本的な役割と仕組みを説明できる
  • インターフェースとの明確な違いを理解し、適切に使い分けられる
  • 具体的なサンプルコードを元に、抽象クラスを自信を持って実装できる

Javaの設計力を一段階レベルアップさせるために、一緒に学んでいきましょう。

抽象クラスとは?まず基本の仕組みを理解しよう

Javaの抽象クラスは、一言でいうと「未完成な設計図」のようなものです。具体的な処理が決まっていない部分(抽象メソッド)と、共通で使える処理(具象メソッド)を両方持つことができる特別なクラスを指します。

抽象クラスの定義と役割

抽象クラスの主な役割は、複数のクラスに共通する機能や性質をまとめ、それらを子クラスに継承させることです。

例えば、「動物」という抽象クラスを考えてみましょう。動物には「食べる」という共通の行動がありますが、「鳴く」という行動は動物の種類によって異なります。犬は「ワン」、猫は「ニャー」と鳴きますよね。

このように、共通化できる処理は抽象クラスに実装し、各クラスで異なる処理は「こういう機能が必要だよ」という宣言だけをしておき、具体的な実装は子クラスに任せる。これが抽象クラスの基本的な考え方です。

「abstract」キーワードの意味

Javaでは、クラスやメソッドの前にabstractというキーワードを付けることで、それが抽象的であることを宣言します。

  • abstract class: このクラスは抽象クラスです。インスタンス化(new)できません。
  • abstract void cry(): このメソッドは抽象メソッドです。処理内容は書かず、名前と引数だけを定義します。

このabstractキーワードがあるおかげで、プログラマーもコンピューターも「これは未完成な設計図なのだな」と認識できるわけです。

クラスとメソッドの違い(通常クラスとの比較)

では、通常のクラスとJavaの抽象クラスは具体的に何が違うのでしょうか。大きな違いは2つです。

比較項目通常クラス抽象クラス
インスタンス化 (new)できるできない
抽象メソッドの有無持てない持てる(必須ではない)

抽象クラスはあくまで「設計図」なので、単体でインスタンスを生成することはできません。必ず、その設計図を元に具体的な実装をした子クラス(サブクラス)を作成し、そちらをインスタンス化して利用します。

抽象クラスが必要とされる理由

なぜわざわざ「未完成な」クラスを作る必要があるのでしょうか。それには、プログラム全体の品質と開発効率を高めるための、ちゃんとした理由があります。

共通処理をまとめて再利用性を高める

最大のメリットは、コードの再利用性が向上する点です。複数のクラスで同じようなコードを何度も書いていると、修正があった場合にすべての箇所を直さなければならず、非常に手間がかかります。また、修正漏れによるバグの原因にもなりかねません。

共通する処理を抽象クラスに一元管理することで、修正は抽象クラスの1箇所だけで済みます。これにより、コードがすっきりと見やすくなり、メンテナンス性も格段に向上するのです。

チーム開発でのコード設計をシンプルにする

チームで開発を進める際、各メンバーがバラバラのルールでコードを書くと、統一感のない読みにくいプログラムが出来上がってしまいます。

抽象クラスを使えば、「このクラスを継承したら、必ずこのメソッドを実装してください」というルールを強制できます。

抽象メソッドを実装し忘れるとコンパイルエラーになるため、実装漏れを未然に防げるのです。これにより、チーム全体のコード品質を一定に保ち、設計の意図を明確に伝えられます。

インターフェースとの使い分けポイント

「ルールを強制するならインターフェースでも良いのでは?」と思った方もいるかもしれません。その通りです。抽象クラスとインターフェースは似た役割を持ちますが、明確な使い分けの指針があります。

  • 抽象クラス: 親子関係が明確で、共通の機能や状態(フィールド)を持たせたい場合 (is-a の関係)
  • インターフェース: クラスに特定の「機能」を追加したい場合 (can-do の関係)

この違いについては、後の章でさらに詳しく解説しますので、ここでは「似ているけど目的が違うんだな」と理解しておけば大丈夫です。

抽象クラスの書き方と実装例

ここからは、実際のコードを見ながらJavaの抽象クラスの書き方を学んでいきましょう。

基本的な構文(サンプルコード付き)

先ほどの「動物」の例をコードにしてみます。Animalという抽象クラスを定義し、「鳴く」という抽象メソッドと「食べる」という具象メソッドを持たせます。

// 動物を表す抽象クラス
abstract class Animal {

    // 処理内容が未定の抽象メソッド
    // サブクラスでの実装を強制する
    abstract void cry();

    // 共通で使える具象メソッド
    public void eat() {
        System.out.println("もぐもぐ食べる");
    }
}

abstract classでクラスを宣言し、abstractを付けたcry()メソッドには{}の処理ブロックがない点に注目してください。

抽象メソッドをオーバーライドする方法

次に、このAnimalクラスを継承して、具体的な動物クラスを作成します。子クラスでは、extendsキーワードで抽象クラスを継承し、@Overrideアノテーションを付けて抽象メソッドの具体的な処理を記述します。

// Animalクラスを継承したDogクラス
class Dog extends Animal {

    @Override
    void cry() {
        System.out.println("ワン!");
    }
}

// Animalクラスを継承したCatクラス
class Cat extends Animal {

    @Override
    void cry() {
        System.out.println("ニャー");
    }
}

もしcry()メソッドのオーバーライドを忘れると、「Dogは抽象メソッドcry()を実装していません」というコンパイルエラーが発生します。

継承を使った実践的な例(スーパークラスとサブクラス)

もう少し実践的な例を見てみましょう。従業員(Employee)というスーパークラス(親クラス)を抽象クラスとして定義し、正社員(FullTimeEmployee)とアルバイト(PartTimeEmployee)というサブクラス(子クラス)を作成するケースです。

給与計算の方法は雇用形態によって異なるため、抽象メソッドにします。

// 従業員を表す抽象クラス(スーパークラス)
abstract class Employee {
    String name;

    public Employee(String name) {
        this.name = name;
    }

    // 給与を計算する抽象メソッド
    abstract int calculateSalary();

    // 共通の情報を表示する具象メソッド
    public void printInfo() {
        System.out.println("名前: " + this.name);
        System.out.println("給与: " + calculateSalary() + "円");
    }
}

// 正社員クラス(サブクラス)
class FullTimeEmployee extends Employee {
    private int baseSalary; // 基本給

    public FullTimeEmployee(String name, int baseSalary) {
        super(name);
        this.baseSalary = baseSalary;
    }

    @Override
    int calculateSalary() {
        // 正社員の給与計算ロジック
        return this.baseSalary;
    }
}

// アルバイトクラス(サブクラス)
class PartTimeEmployee extends Employee {
    private int hourlyWage; // 時給
    private int hoursWorked; // 勤務時間

    public PartTimeEmployee(String name, int hourlyWage, int hoursWorked) {
        super(name);
        this.hourlyWage = hourlyWage;
        this.hoursWorked = hoursWorked;
    }

    @Override
    int calculateSalary() {
        // アルバイトの給与計算ロジック
        return this.hourlyWage * this.hoursWorked;
    }
}

// 実行クラス
public class Main {
    public static void main(String[] args) {
        Employee taro = new FullTimeEmployee("太郎", 300_000);
        Employee hanako = new PartTimeEmployee("花子", 1_200, 80);

        taro.printInfo();
        // 出力:
        // 名前: 太郎
        // 給与: 300000円

        hanako.printInfo();
        // 出力:
        // 名前: 花子
        // 給与: 96000円
    }
}

このように、printInfo()のような共通処理はスーパークラスにまとめ、calculateSalary()のような個別の処理はサブクラスに任せることで、柔軟で拡張性の高い設計が実現できます。

抽象クラスとインターフェースの違いを徹底比較

Javaの学習者にとって、抽象クラスとインターフェースの違いは最も混乱しやすいポイントの一つです。ここで両者の違いを明確にしておきましょう。

共通点と相違点を整理

以下の表に、両者の共通点と相違点をまとめました。

項目抽象クラスインターフェース
インスタンス化できないできない
抽象メソッド持てる持てる
具象メソッド持てるJava 8以降、defaultメソッドとして持てる
継承/実装のキーワードextendsimplements
多重継承/実装できない(1つしか継承できない)できる(複数実装できる)
フィールド(変数)通常のフィールドを持てるpublic static finalな定数のみ
主な目的is-a 関係(〇〇は△△の一種) 共通の実装と状態を引き継がせるcan-do 関係(〇〇は△△できる) クラスに機能を追加する

どちらを使うべきか判断する基準

理論的な違いは分かっても、実際にどちらを使うべきか迷う場面は多いでしょう。判断基準は以下の通りです。

抽象クラスを選ぶべきケース

  • クラス間に明確な親子関係 (is-a) がある場合。例: 「犬(Dog)」は「動物(Animal)」の一種である。
  • 複数のクラスで共通のフィールドや具象メソッドを共有したい場合。コードの重複を避け、実装を共通化したいときに有効です。
  • アクセス修飾子(public, protected, private)を使いたい場合。インターフェースのメソッドは原則としてpublicです。

インターフェースを選ぶべきケース

  • クラスに特定の機能 (can-do) を追加したい場合。例: 「飛べる(Flyable)」「泳げる(Swimmable)」といった機能。
  • クラスの親子関係とは無関係な機能を提供したい場合。
  • 複数の機能をクラスに持たせたい(多重継承のようなことをしたい)場合。Javaではクラスの多重継承はできませんが、インターフェースは複数実装できます。
  • 将来的に、全く異なる実装を持つクラスが出てくる可能性がある場合。

基本的には、インターフェースを優先的に検討し、共通の実装を持たせたいなど、抽象クラスでなければならない理由がある場合に抽象クラスを使う、という考え方がおすすめです。

Java 8以降での境界の変化(defaultメソッドなど)

Java 8から、インターフェースにdefaultメソッドとstaticメソッドが導入されました。defaultメソッドは、インターフェース内に具体的な処理を持つメソッドを記述できる機能です。

これにより、インターフェースでも具象メソッドが持てるようになり、両者の境界は以前より少し曖昧になりました。しかし、設計思想の根幹である「is-a」と「can-do」の違いは変わりません。

defaultメソッドは、あくまでインターフェースに後から機能を追加しやすくするための補助的な機能と捉えるのが良いでしょう。

抽象クラスを使う際の注意点とベストプラクティス

強力な機能である抽象クラスですが、使い方を誤ると逆にプログラムを複雑にしてしまう可能性もあります。ここでは、いくつかの注意点とコツを紹介します。

多重継承の制約に注意

Javaのクラスは、一度に1つのクラスしかextends(継承)できません。これを単一継承の原則といいます。

もし、あるクラスがすでに別のクラスを継承している場合、そのクラスはもう抽象クラスを継承することはできません。

この制約は、設計を考える上で重要なポイントとなります。機能を追加したいだけであれば、複数実装が可能なインターフェースの方が柔軟に対応できます。

設計を複雑にしすぎないためのコツ

抽象クラスを使って継承の階層を深くしすぎると、クラス間の関係が複雑になり、コード全体の可読性が低下することがあります。

設計のコツは、本当に共通している部分だけを抽象化することです。「将来使うかもしれない」という予測で過度な設計をするのではなく、現時点で必要な共通機能だけを抽象クラスにまとめるように心がけましょう。

抽象化のしすぎが生む落とし穴

抽象化はコードの再利用性を高めますが、度を超すと「何の処理をしているのか分からない」という事態を招きます。具体的な処理がどこに書かれているのかを追うために、いくつもの親クラスを遡らなければならなくなるからです。

抽象化は、あくまでコードを分かりやすく、メンテナンスしやすくするための手段です。目的と手段を履き違えないよう、バランス感覚を持つことが重要になります。

まとめ:抽象クラスを理解すれば設計力が変わる

今回は、Javaの抽象クラスについて、基本的な仕組みからインターフェースとの違い、実践的な使い方までを解説しました。

インターフェースと組み合わせて柔軟な設計へ

抽象クラスとインターフェースは、対立する概念ではありません。むしろ、両者を適切に組み合わせることで、より柔軟で堅牢なシステムを設計できます。例えば、Animal抽象クラスを継承したBirdクラスに、Flyable(飛べる)インターフェースを実装する、といった使い方が可能です。

実務で抽象クラスを使う判断基準

実務でどちらを使うか迷ったときは、以下の点を自問自答してみてください。

  • これから作るクラス群は、明確な「is-a(…は〜の一種である)」の関係か?
  • そのクラス群で、共通の変数(状態)や、共通の具体的な処理(メソッド)を持つべきか?

この2つの問いに「Yes」と答えられるなら、それは抽象クラスの出番です。

  • この記事を書いた人
  • 最新記事

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

-Java入門