abstract で抽象クラス

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

abstract で抽象クラス とは

Java の abstract キーワードで抽象クラスと抽象メソッドを定義し、Shape を継承した Triangle で三角形の面積を求めるコードを書いてみよう。

「形だけ決めて中身は子に任せる」抽象クラス

継承の章で extends を学んだとき、親クラスは「共通の部品をまとめる入れ物」として登場しました。たとえば動物アプリで Animal クラスに name フィールドや walk() メソッドをまとめておき、DogCatextends Animal で受け取る、というやつです。ここまでは「親クラスを単体で new してインスタンスにする」ことも普通にできていました。

ところが現実のシステムをモデリングしていくと、「親クラスそのものを実体化されると困る」という場面が出てきます。たとえば図形アプリで Shape (図形) という概念を作ったとして、new Shape() で「ただの図形」が生まれてもサイズも形も決まっていません。面積を求めようにも、三角形 なのか なのか分からないと計算式が書けないのです。

そこで Java には abstract という修飾子が用意されています。abstract class Shape { ... } と書くと、「Shape は設計図としては存在するけど、直接 new してインスタンスにすることはできない」という宣言になります。さらに、メソッド側に abstract を付けて abstract double area(); のように本体 ({ }) を書かなければ、「この area() の中身はサブクラスで必ず実装してね」という宿題を残せます。

抽象クラスは「半完成品の設計図」と言われます。共通部分は親に書いてしまうが、具体的な計算ロジックや表示処理など「種類によって変わる部分」だけは子クラスに丸投げする、という分業の道具です。

diagram (will load when visible)

この章では abstract の文法と、なぜそれが ポリモーフィズム の土台になるのか、そして抽象クラスと インターフェース の使い分けまでを一気に押さえます。

abstract classabstract method の文法

抽象クラスの基本構文は次のとおりです。クラス宣言とメソッド宣言の頭にそれぞれ abstract を付けます。

Java

public abstract class Shape { private String name; public Shape(String name) { this.name = name; } // 中身がない 「抽象メソッド」 はセミコロンで終わる public abstract double area(); // 中身がある 「具象メソッド」 は普通に書ける public String describe() { return name + " の面積は " + area(); } }

ポイントは、抽象メソッド public abstract double area(); の行末がいきなり ; で終わっていることです。普通のメソッドなら { } でブロックを書きますが、abstract メソッドは「中身はサブクラスが書く」と決まっているので、本体を書かずに型情報だけ残します。一方で、抽象クラスの中に普通のメソッド (describe()) を混ぜることもでき、共通実装をここにまとめてしまうのが定石です。

サブクラス側は次のようになります。

Java

public class Triangle extends Shape { private int base; private int height; public Triangle(int base, int height) { super("三角形"); this.base = base; this.height = height; } @Override public double area() { return base * height / 2.0; } }

@Override アノテーションは「親の抽象メソッドを実装するよ」というメモで、付けておくとコンパイラがメソッド名のタイポを検出してくれます。Trianglearea() を実装し忘れると、Triangle 自身が抽象クラス扱いになるか、コンパイルエラーになります。

abstract class Shapenew Shape("なにか") しようとすると、Shape is abstract; cannot be instantiated というエラーが出ます。これがまさに「親を直接インスタンス化されたくない」という意図を Java が守ってくれている証拠です。

ポリモーフィズムの土台になる

抽象クラスのすごいところは、Shape 型の変数で TriangleCircle をまとめて扱えるところです。これを ポリモーフィズム (多態性) と呼びます。

Java

Shape[] shapes = { new Triangle(4, 5), new Circle(3) }; for (Shape s : shapes) { System.out.println(s.describe()); }

配列の型は Shape[] ですが、中には TriangleCircle といった異なるサブクラスが入っています。s.describe() を呼ぶと、Shape の中で定義された共通実装が走り、そこから area() を呼ぶと「実際の型 (TriangleCircle) の area()」が動きます。呼び出し側は具体的な型を意識せず、「Shape として扱えれば OK」という発想でコードが書けます。

diagram (will load when visible)

この仕組みのおかげで、後から Rectangle (長方形) や Square (正方形) を追加しても、呼び出し側のループは一切変えずに済みます。新しい図形ごとに if (shape instanceof Triangle) { ... } を書き足す必要がないのが、オブジェクト指向の威力です。

抽象クラス vs インターフェース

ここで「抽象メソッドだけ並べるなら interface (インターフェース) でもよくない?」という疑問が出てきます。実際、両者は似ていますが、使い分けの基準は次のとおりです。

観点abstract classinterface
多重継承1 つだけ extends複数 implements 可能
フィールド普通に持てるpublic static final のみ
コンストラクタ持てる持てない
共通実装普通に書けるdefault メソッドで一部可
主な用途「is-a」関係 + 共通実装「できる/できない」契約

大まかには次のように考えます。フィールドや共通の処理を持たせたい / 「○○の一種である」と言いたい ときは abstract class役割や能力 (Comparable Runnable のような「○○できる」) を表したい ときは interface です。今回の Shapename フィールドと describe() の共通実装を持つので、抽象クラスがしっくりきます。

Java 8 以降は interface にも default メソッドが書けるようになり、両者の境界は曖昧になっています。それでも「フィールドが必要なら abstract class」というルールを覚えておくと、設計の迷いが減ります。

よくある間違い

抽象クラスは便利ですが、文法が独特なのでハマりやすい落とし穴があります。次の 3 つは特によく見る失敗例です。

  • abstract メソッドに本体を書いてしまうpublic abstract double area() { return 0; } のように { } を付けるとコンパイルエラーです。エラーメッセージは abstract methods cannot have a body などになります。本体を書きたいなら abstract を外し、サブクラスで @Override するスタイルにします
  • 抽象クラスを new しようとするnew Shape("hoge") はコンパイルエラーです。設計図を直接実体化しようとしているサインなので、TriangleCircle のような具象サブクラスを new し、変数の型だけを Shape にする (Shape s = new Triangle(4, 5);) のが正解です
  • finalabstract を同時に書くfinal は「これ以上継承させない」、abstract は「サブクラスで上書きさせる」という真逆の意味を持つので、同じクラス/メソッドに付けるとコンパイルエラーになります。どちらか一方だけにします

もう一つよくあるのは、サブクラスで抽象メソッドを実装し忘れるパターンです。Triangle extends Shape と書いたのに area() を書かないと、Triangle is not abstract and does not override abstract method area() in Shape というエラーが出ます。@Override を必ず付けておくと、シグネチャ違いも一緒に検知できるので安全です。

抽象メソッドを実装したくない (まだ書けない) ときは、サブクラス自身も abstract にしてしまう手があります。たとえば abstract class Polygon extends Shape と書けば、Polygon でも area() を残したまま、その子の Triangle Square で実装する、という多段構成にできます。

やってみよう

今回の課題は、Shape (抽象クラス) と Triangle (具象サブクラス) を組み合わせて、三角形の面積を整数で返す triangleArea メソッドを作ることです。

  1. ファイル内に abstract class Shape を定義し、abstract int area(); を宣言する
  2. Triangle extends Shape を定義し、コンストラクタで baseheight を受け取る
  3. Trianglearea()base * height / 2 を返す (整数除算でよい)
  4. Solution.triangleArea(int base, int height) の中で new Triangle(base, height) を作り、area() の結果を return する

テストは次の 3 ケースで判定します。triangleArea(4, 5)4 * 5 / 2 = 10triangleArea(0, 5)0 * 5 / 2 = 0triangleArea(7, 3)7 * 3 / 2 = 10 (整数除算なので 21 / 2 = 10) です。int で割り算しているので小数点以下は自動で切り捨てになります。double でキャストしたくなる気持ちをぐっと我慢して、まずは整数のまま素直に返してみてください。

ヒントとしては、Triangle クラスを Solution と同じファイルに書く場合、public を付けられるのは Solution 1 つだけです。class Triangle extends Shape { ... } のように修飾子なし (パッケージプライベート) にすれば、同じファイルにいくつでもクラスを並べられます。abstract class Shape も同様です。実行環境のクラス名は Solution で固定なので、そこだけ間違えないように気をつけましょう。

慣れてきたら、Circle クラスも追加して Math.PI * radius * radius を返す area() を書いてみたり、Shape[] shapes = { new Triangle(3, 4), new Circle(2) };for-each で回して、ポリモーフィズムの感触をつかんでみてください。次のレッスンでは、この abstract の親戚にあたる interface を取り上げ、「契約だけ並べる」スタイルとの違いをさらに深掘りしていきます。

よくある質問

Q. BST はなぜ高速ですか?

A. 平衡している場合、検索・挿入・削除がいずれも O(log n) です。各ノードで「左<自分<右」を満たすため、半分ずつ探索範囲が絞れます。ただし偏ると O(n) に劣化するため、実用では赤黒木・AVL 木のような自動平衡化された実装を使います。

Q. in-order 走査で何が得られますか?

A. BST を in-order で巡回するとソート済みの順序で要素が得られます。これが BST と他のツリーの大きな違いで、ソート済みデータの逐次処理に向いています。範囲検索(lo 以上 hi 以下)も簡潔に書けます。

Q. BST に重複値を入れるとどうなりますか?

A. 実装次第ですが、通常は「等しい場合は右に入れる」「カウントを持って同じノードに記録する」のいずれかです。重複が多いユースケースなら TreeMap<Key, Integer> のように出現数を値に持たせる方が探索効率が上がります。

次のレッスン

次は interface で契約を定義 で、interface で契約を定義 を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. abstract の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. abstract とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. abstract class Shape を定義し、abstract int area(); を宣言すること (本体 { } は付けない)
  2. Triangle extends Shape を定義し、@Overridearea() を実装して base * height / 2 を返すこと
  3. Solution.triangleArea の中で new Triangle(base, height) を作り、その area() の戻り値を返すこと

入出力例

test-cases.txt

triangleArea(4, 5)10 triangleArea(0, 5)0 triangleArea(7, 3)10 triangleArea(10, 10)50 triangleArea(1, 1)0

ヒント

main.java
main.java
学習モード

メモ

abstract で抽象クラス

⌘S で保存