よしたろうブログ

駆動設計・アーキテクチャ・変更容易性とかの話が好きです。

『決定板:Javaの継承と抽象化~ 第4部 抽象クラスとインターフェイスの使い分け』

はじめに

本記事は、「継承」「抽象クラス」「インターフェイス」の各々について、それらの相互作用について学ぶことをテーマにした全4部構成記事の第4部「インターフェイスと抽象クラスの使い分け」について、です。

『継承・インターフェイス・抽象クラス』シリーズ

前部 「抽象クラス」

  1. Javaの継承』~型と実装の継承・合成と委譲・多重継承問題~
  2. Javaインターフェイス』~歴史・型の継承・ポリモーフィズム情報隠蔽・多重継承問題へのソリューション~
  3. Javaの抽象クラス』~抽象化とは・抽象クラスとサブクラスの粒度差・具体的な使用例(Template Methodパターン)~
  4. Javaの抽象クラスとインターフェイスの違い』~表・使い分け・組み合わせ爆発問題・抽象骨格実装 ~

以上の構成を予定しています。

  • 基本的な情報
  • 特徴・メリット・デメリット
  • 目的や注意点
  • オブジェクト指向の中での役割

などなど深堀していきます。

それぞれの特徴とメリット・デメリット、トレードオフ。登場した当時の歴史的背景や、何に対してのソリューションなのか?オブジェクト指向の目的である「変更容易性」のある設計に対してどの様な貢献をするのか?

など、統一的かつ網羅的な内容にしようと思っています。

言語が用意しているシステムが、何をしたいのか?何のためにあるのか?どんな背景や文脈の元で登場したのか?歴史の中でどう変化していったのか?

  • 再利用性が高い
  • 独立性の高い
  • 変更に対して影響が少ない
  • 変更の影響が特定しやすお
  • 機能追加・拡張が容易
  • 意図がわかる一意な名付け
  • データ抽象化・情報隠蔽
  • データと処理を一つの固まりとして扱える

ピンポイントな情報を見たい場合は目次を用いて抽出してください。長いです。

4. 抽象クラスとインターフェイスの違い

クラス・抽象クラス・インターフェースの三つの違いからまずは見て行きたいと思います。 まずここまでのおさらいとして、それぞれの役割とか定義を簡単にまとめます。

  • クラス

    • ①生成機の役割(インスタンス
    • ②実装(構造やコード)・仕様(型・振る舞い)としての役割
    • ③再利用の役割として実装継承(extends)を可能にする
      • これにかんしては、これまで説明した通り委譲を用いるべきです。再利用という観点からみると、クラスという単位は大きすぎる単位なのです。
    • ④データ・状態(不変オブジェクトの場合は禁止)と関数のカプセル化
  • 抽象クラス

    • 具体的なオブジェクト(もの)を抽象化し、共通処理としてスーパークラスカプセル化し、具象クラスに個別の実装を延期(extends)させる
  • インターフェイス

    • オブジェクト(もの)ではなく振る舞い(What:なに)を抽象化。〜として扱うという性質をクラス階層に関係なく特定のクラスに付与できる
    • 多重継承(型のみ ※defult実装では実装の継承も可能に)を可能にしクラス階層を無視した型継承(implements)を実現
    • インターフェイスは抽象クラスより完全なデータ抽象化で具象クラスのカプセル化を実現し、ポリモーフィズムも可能になる

4-1. インターフェイスと抽象クラスはどう使い分けるのか

参考記事:ORACLE Java ドキュメンテーションJava™ チュートリアル

抽象クラス

  • ①密接に関連する複数のクラス間で共通する性質をコードとして共有、その関係性を表現したい
  • ②public 以外のアクセス修飾子 (protected や private など) が必要
    • 4-2-4. 「アクセス修飾子」 にて解説
  • ③メンバ変数に「static」「final」以外のメンバ変数が必要。
    • 4-2-5. 「メンバ変数」と「状態(データ)」 ※重要 にて解説
      • フィールドによってオブジェクトを包含し状態にアクセスしたい(状態を保持する)
      • 状態を変更したい場合・変更を反映してほしい場合

インターフェイス

  • ①クラス階層・クラス間の関係性を無視して、振る舞い・性質をオブジェクトに付与・定義したい
    • 4-2-6. 「クラスツリー内の立ち位置」と「型継承時の制限」 にて解説
  • ②ひとつのオブジェクトに複数の振る舞い・性質を付与するために、多重継承(型)を行いたい
    • 4-2-2. 「多重継承」 にて解説
    • 4-2-5. 「メンバ変数」と「状態(データ)」 にて解説
      • メンバ変数に「static」「final」な定数しか持てないため、状態を継承させない
  • ③呼び出し側から具象クラスをカプセル化したい場合(多態性の前提)
    • 4-2-7. 「パッケージ間などでの結合度」と「不変・可変の分離へのアプローチ」 にて解説

抽象クラスはスーパークラスとして抽出された概念・性質であり、インターフェースはオブジェクトに性質を与えます。双方とも、サブクラスに性質の具象化(実装)を強制し機能を実現します。契約の関係と表現できます。

抽象クラスで定義された型を実装するには、クラスはその抽象クラスのサブクラスでなければないけませんし、それが目的の一つですね。インターフェースの場合には、ルールに従って要求されるメソッドを全て定義しているクラスであれば、クラス階層のどこに位置していても、インターフェースを実装することが許されています。

それぞれについての具体定期な説明を、以下の一覧表の解説と共に説明していきたいと思います。

4-2. クラス・抽象クラス・インターフェイスの違い 一覧表

                                                                                                                                                                                                                                                                                                                                                                     
クラス・抽象クラス・インターフェースの違い(赤文字は重要部分)
クラス抽象クラスインターフェイス
予約語classabstractinterface
継承宣言extendsextendsimplements /
extends(サブインターフェイス
継承の違い実装・型(仕様)を継承実装・型(仕様)を継承型(仕様)を継承
多重継承単一継承のみ単一継承のみ可能(型の継承のみだが、
Java8からのdefaultメソッドに
よって一部実装の継承もサポート
ただし、抽象クラスとは全く別の用途)
クラス・インターフェイス
へのfinal宣言
可能不可
継承しないと実装できない
不可
継承しないと実装できない
インスタンス生成可能不可
(未定義な実装があるから)
不可
(未定義な実装があるから)
(Java8 からはSAMであれば
ラムダ式インスタンス化可能)
コンストラクタ記述可能可能
(具象メソッドで使用する
動的な変数の初期化が必要)
不可
インスタンス生成時の
初期化は必要ない
抽象メソッド不要必須
(定義しなければ意味が無い)
必須
(定義しなければ意味が無い)
スーパークラスでの
具象メソッドの記述
可能可能
抽象と具象は混在する
具象メソッドで共通の性質を共有する
不可
メソッドの型定義のみ可
暗黙的にpublic abstract修飾子付与
(※ Java8からdefaultメソッド、staticメソッド
といった具象メソッドも定義できる様になった
ただし、抽象クラスとは考え方が異なる)
(Java9 から private な具象メソッドの実装可能に)
サブクラスへの
抽象メソッドの実装
(Override)
/強制
未定義でコンパイルエラー
強制
未定義でコンパイルエラー
アクセス修飾子public /
private /
protected
他省略
public /
protected
public
メンバ変数制限なく定義可能final でも static でもない
フィールドの宣言ができる
インスタンスフィールドなどのデータ(状態)も保持できる
public static finalがついた定数のみ宣言可能
省略した場合は暗黙的にpublic static finalが付与
宣言と同時に値を代入し、初期化が必要
インターフェイスは状態を持たない
状態(データ)持つ持つ持たない
継承の目的多態性 / 再利用 / 差分プログラム
実装・型(仕様)の継承
多態性 / 再利用 / 抽象化 / カプセル化(共通実装のみ)/ データ抽象化
実装・型(仕様)の継承>
多態性 / 再利用 / 抽象化 / カプセル化 / データ抽象化 / 情報隠蔽 / 型(仕様)の継承
クラスツリー内
の立ち位置
構成要素の一部構成要素の一部独立
型継承時の制限直下のサブクラス直下のサブクラス階層位置に縛られない
パッケージ間などでの
結合度
不変・可変の分離
へのアプローチ
不変部分を抽出し
通化させる
インターフェイス
クラスの両方の性質を持つ
可変部分を抽出し抽象化させる
抽象を具象化し、具象クラスは不変として扱う
誰向けか
/
内部向き
設計者・開発者
(どちらかといえば)
外部向き
コンポーネント外か
クラス外か、世界か
文脈による
スパークラスと
サブクラスの
関係性
is - a
リスコフ置換原則に則る
*(抽象クラス・インターフェイスも同様)*
①抽象クラスは複数のオブジェクトから、抽象化された本質・性質・共通点を持つ
②実装クラスのインスタンスに機能(実装)を延期する
①抽象(インターフェイス)依存・具象(実装)非依存
②仕様(What)と実装(How)の分離
③実装クラスのインスタンスに機能(実装)を強制させる

キャスト
 
継承関係が明確な場合のみ可能継承関係が明確な場合のみ可能具象クラスがインタフェースを
実装していればどんなキャストも可能

※SAM とは、Single Abstract Method のことです。Java8 からは一つだけabstract(抽象) なメソッドを含んでいる interface を functionalInterface(関数型インターフェイス) と呼び、無名クラスによるインスタンス生成がラムダ式 でできるようになりました。

4-2. 一覧表解説とまとめ

ここではこれまでのシリーズの中で説明してきたことを端的に一覧で表しました。

それぞれについて簡単に説明をすると共に、これまでのシリーズの中で学んだことのまとめや補足をしていきたいと思います。

4-2-1. 「継承宣言」

extendsimplements がありそれぞれ意味が異なります。 一般的には継承とは extends を、インターフェイスの実装とは implements を指します。

ただ本シリーズでは、どちらも継承として捉えています。

  • extends とは実装・型の継承
  • implements は型のみの継承

実装とは、コード・状態(データ)・How

型とは、仕様・振る舞い・性質・What

などと表現してきました。

4-2-2. 「多重継承」

Java においての多重継承は「実装の多重継承は禁止するが型の多重継承は許可する」という形をとっています。多重継承が引き起こす諸々の問題は実装の多重継承が引き起こすためですね。

インターフェイスでは元々は実装の継承ができませんでした。default メソッドの登場で実装の多重継承にも注意しなくてはならなくなりましたが、元々の思想的にインターフェイスの具象メソッドの存在意義は実装の多重継承ではなく、公開されたAPIとしてのインターフェイスの互換性を保つものでした。

型の多重継承のみを許可すれば、基本的には多重継承の問題を避けれるという形で擬似的に多重継承を実現しました。ですが実装の継承ができないということは同じ様な実装内容を複数箇所に記述しなくてはいけないとう問題を新たに生じることになります。型の多重継承では、この問題を解決できていません。

これに対して、 default メソッドを使用できるという考え方もあります。元々の登場背景としては使用意図としてはし自然な考え方のように感じます。ただ、元々は抽象クラスとインターフェイスを用いた抽象骨格実装という考え方もあります。どちらの方法でも、基本的な実装をデフォルトで用意したいという要求に応える事ができます。

4-2-3. 「スーパークラスでの具象メソッドの記述」

抽象クラスにおいて、具象メソッドは複数のオブジェクトが共通して持っている性質を表します。例えば、音楽再生・動画再生で共通する性質は「何かを再生する」というものです。共通する概念をスーパークラスとサブクラス間で共有するもので、決して機能の再利用ではありません。

インターフェイスは元々、具象クラスの存在は許可されていませんでした。インターフェイスの最大のデメリットは、一度公開してしまったインターフェースにメンバーを追加すると、そのインターフェースを継承しているクラスに破壊的な影響が生じることでした。

設計・公開時に想定できなかった修正や機能拡張に際して柔軟性や互換性を持たせたいという場面で default メソッドの実装が許可されました(Java8)。インターフェイスは後からの変更が非常に困難なため、慎重な設計が必要でしたが、具象メソッドである default メソッドの追加により柔軟に対処できる様になったんですね。

4-2-4. 「アクセス修飾子」

抽象クラスは、クラス間で密接に関係していることを表現しそれを継承(extends)で実現します。継承を大前提としているため protected は使用できるが private は使用できません。サブクラスから継承できる様に出来なくてはいけません。protected はサブクラスからしかアクセスできない状態です。

インターフェイスはクラス階層から独立し、特定のクラスに振る舞いを付与することを目的とし、継承(implements)前提のため public 以外ありません。どこからでも implements 出来なくてはいけないのです。publicをつけなかった場合(アクセス修飾子なし)、同パッケージ内からの利用だけに制限されることになります。

4-2-5. 「メンバ変数」と「状態(データ)」 ※重要

抽象クラスとインターフェイスでは表の通り、保持できるメンバ変数が異なります。細かいルールは説明しませんが、何を実現したいかというと、状態を持つか持たないか?です。 公開インターフェイスので互換性問題の件で、インターフェイスが具象メソッドを保持できる様になったため、抽象クラスとの使い分けや存在意義がぼやけた様に思いますが、インターフェイスが一貫して変わらないのは『状態を持たない』という点です。これを言い換えると以下の様に表せます。

  • クラス・抽象クラス:状態を持てる代わりに単一継承しかできない
  • インターフェース: 状態を持てない代わりに多重継承できる

「状態を持つ」というのは、メンバ変数としてインスタンスフィールドを持てるかどうかです。つまりデータを持てるかどうかです。インターフェイスはメンバ変数として、静的最終な定数しか保持することを許可されていません。インスタンスフィールドは、あるオブジェクトを参照していますが、そのオブジェクトにはデータ、つまり変更されるであろう状態が内包されています。インターフェイスがその様なメンバ変数を保持することは禁止されています。状態を保持させないためです。インターフェースは、状態を持たない「純粋な振る舞い」でなければならないということですね。

参考記事:インターフェースを「契約」として見たときの問題点 ― C#への「インターフェースのデフォルト実装」の導入(前編)

4-2-6. 「クラスツリー内の立ち位置」と「型継承時の制限」

インターフェイスは型階層から独立している

引用元:Effective java

インターフェースは、非階層的な型を構築するためのフレームワークを可能にします。

インターフェースは対象のクラスが、クラス階層のどこに属していても実装を割り込ませる事が可能です。抽象クラスもどの階層においても実装できますが、Javaでは『実装の継承』は単一継承しか許されていない為、『型(仕様)の継承』、つまり型定義をする為に抽象クラスを使用するのは非常に不便です。extendsによる継承では「実装の継承」「型(仕様)の継承」がセットになるからです。

例えば、2つの異なるクラスB, Xに対して、抽象クラスAを実装させたいとする。この2つの異なるクラスB, Xはクラス階層が異なるバラバラの位置に存在すると思ってください。その上で、BとXの祖先がAになるように実装しなければならないとします。ここで問題になるのは、B及びXの間にある関係のないクラスまでAを実装する必要が出て来てしまうことです。

extendsによる継承は「実装の継承」「型(仕様)の継承」がセットだからですね。

これに対して、インターフェイスはクラス階層外から実装を割り込ませることが可能です。クラスツリーから独立しているからです。全てのクラスは暗黙的に Object クラスを継承しています。クラスは Object クラスを root としいたツリー階層になっているので、抽象クラスはクラスツリーの一部を構成しています。。

もしインターフェイスがない場合で、全部継承(extends)で行わなければいけないとしたら実装に必要な階層まで継承(extends)を続けるとなると、無関係のクラスが不必要な実装の継承を拒めません。現実的には、関係クラスを洗い出し全部の上位クラスとなる位置にスーパークラスを定義する必要があります。

  • クラスはObjectクラスをrootとしいたツリー階層になっている
    • インターフェース:クラスツリーから独立
    • 抽象クラス:クラスツリーの一部を構成
  • インターフェース
    • 型階層から独立しているので特定の機能を割り込ませることができる
    • スーパークラスによらず新たなインターフェースを実装可能
    • 多重継承可能
  • 抽象クラスの場合
    • 関係クラスを洗い出し全部の上位クラスとなる位置に入れる必要
    • 途中に必要ない機能があっても継承を拒否できない

4-2-7. 「パッケージ間などでの結合度」と「不変・可変の分離へのアプローチ」

具象に依存するのではなく、抽象に依存することで、疎結合にすることができますよということですね。

大雑把に言うと、具象に依存すると一部の変更が全体に影響するかもだけど、抽象依存するとめっちゃ限定できる様になるかもよ、っていう話です。

また、クラス指向なオブジェクト指向言語では『不変を軸に、可変であろう箇所をクラスに抽出する』を想定しています。

詳細は「第二部 〜インターフェイス〜」2-5-2. インターフェイスで可変を抽出する をご覧ください。

4-2-8. 「スーパークラスとサブクラスの関係性」

is - a やリスコフの置換原則を守るのは前提として、抽象クラスとインターフェイスでは行いたいことや思想が異なる点に注目すべきです。

抽象クラス

  • 抽象クラスは複数のオブジェクトから、抽象化された本質・性質・共通点を持つ
  • 実装クラスのインスタンスに機能(実装)を延期する

インターフェイス

抽象クラスと具象クラスはほとんど同じ「もの」を表現し、サブクラスにて具体的なもの・オブジェクトを表現します。例えば、抽象クラス「色」の具象クラスは青・赤・黄、といった様にどちらも色であることは「同じ」です。ですが、色というのは概念や性質であり、黄色とは色の具体例です。色という抽象的な概念を、具象化し赤というクラスを表現する場合、抽象クラス・具象クラスは密接に関連すべきものである事がわかり、それを抽象クラスを継承(extends)することで実現します。これにたいして、インターフェイスはそのような関係性は必要ありません。スーパータイプとサブタイプは同じオブジェクト・ものではありません。

インターフェイスは「振る舞い・性質」を表します。何をするのか?何ができるのか?このような振る舞い・性質を、継承(implements)する具象クラスに付与します。抽象クラスとサブクラスは同じものであると表現しましたが、インターフェイスを継承(implements)した具象クラスは「同じものではないが、同じ振る舞いをするという共通点を持ったオブジェクト」として扱う事ができる様になります。 「空を飛ぶ」「食べる」という性質は、カラス・コウモリ・モモンガを一つのグループにひとまとめにできます。それぞれの生き物は、flyable interface と eatable interface を継承(implements)しているオブジェクトであると表現できます。 また、インターフェイスの多重継承により一つのオブジェクトが多面性を獲得する事ができます。

抽象クラス・インターフェイスインスタンス化できないのは、概念や性質は実際には存在しないものだからです。このような抽象的な部類を表すための表現が抽象クラスとインターフェイスであり『抽象型』と呼ばれます。抽象とは「実体がない」という意味です。

また、以下の引用の様にインターフェイスや抽象クラスは、クラスに対する契約であるという性質もあります。

参考記事:インターフェースを「契約」として見たときの問題点 ― C#への「インターフェースのデフォルト実装」の導入(前編)

契約と実装の分離  当初、インターフェースに関数実体の定義を認めなかったのは、契約と実装を明確に分離すべきという思想的な判断である。ここでいう契約や実装というのは、図11に示すような意味合いである。

  • 契約(contract): ある型がどういう関数を持っているべきかという対外的な約束事
  • 実装(implementation): その関数が具体的にどう実装されるか

図11: 契約と実装

 確かに、「インターフェースとは、契約のみを与える型である」とした方が、役割が明瞭ではある。その一方で、インターフェースが実装を持てないことで、明瞭さというメリット以上にデメリットを受けているのではないかという疑念もある。

※抽象クラスとインターフェイスインスタンス化できませんが、抽象クラスは参照型変数は作れます。

    // 参照型変数
    AbstractClass abstractRef;

参照型変数はあくまで「参照の入れ物」だからです。インスタンスやメソッドとは関係ないので作ることができるというわけです。

4-2-9. 「キャスト」

キャストの自由度が異なります。 クラスは「継承関係が明確な場合」のみキャストが可能ですが、インタフェースはどんなキャストをしてもコンパイルエラーになりません。ただし、実行時にキャストして、インスタンスがインタフェースを実装していなかったら例外が投げられます。 インタフェース型の変数には、具象クラス型インスタンスの参照を代入できます。

参考記事:10.3 インタフェースのキャストは自由!

4-3. 抽象骨格(スケルトン)実装

インターフェイスによって、擬似的な多重継承を実現した Java ですがこのやり方には欠点がありました。実装の多重継承ができない事です。

インターフェイスに多数の抽象メソッドが定義されている場合や、ある特定のインターフェイスを実装する複数のサブクラスで共通する実装が存在する場合、実装者からすると毎回同じコードが出現したり、コード量が多くなってしまい不便です。

そのようなもの中で予めデフォルトとなる実装を提供しておくことができるものがあれば、インターフェイスの実装を効率良く行うことができます。このような形でインターフェイスの実装補助を行うことを『抽象骨格実装』と呼びます。

抽象骨格実装を実現する手段として以下の手段があります。

  1. 抽象クラスでインターフェイスを実装する
  2. インターフェイスでデフォルトメソッドを実装する

本記事では『抽象クラスでインターフェイスを実装する』方法を紹介します。

4-3-1. 抽象クラスでインターフェイスを実装する

インタフェースの大まかな実装を抽象クラスで実装しておき、その抽象クラスもインタフェースとあわせて提供するという方法です。

インタフェースを実装する具象クラスはインタフェースの抽象メソッドをすべて実する必要があります(契約)しかし、インターフェースを継承(implements)し抽象クラスを宣言すれば、インターフェイスのメソッドをすべて実装しないクラスを定義することも可能です。

abstract class X implements Y {
  // Yの1つのメソッドを除く全てのメソッドを実装する
}

class XX extends X {
  // Y の残りのメソッドを実装する
}

この場合、クラスXはYを完全に実装していないので抽象クラスでなければなりませんが、クラスXXは実際にYを実装しています。

以下に具体例を載せます。コレクションでは非常に多く使用されています。

参考記事:Collections Framework の概要の注釈

抽象実装 - カスタム実装を容易にする、コレクションインタフェースの骨格実装

  • AbstractCollection - セットでもリストでもない (「バッグ」やマルチセットのような) Collection の骨格実装
  • AbstractSet - Set の骨格実装
  • AbstractList - ランダムアクセスデータの格納 (配列など) を基とする List の骨格実装
  • AbstractSequentialList - シーケンシャルアクセスデータの格納 (リンク設定されたリストなど) を基とする List の骨格実装
  • AbstractQueue - Queue の骨格実装
  • AbstractMap - Map の骨格実装

java/util/List.java (単純なインターフェース)

public interface List<E> extends Collection<E> {
  E get(int index);
  void clear();
}

java/util/AbstractList.java (骨格実装)

public abstract class AbstractList<E> extends AbstractCollection<E> 
  abstract public E get(int index);
  
  public void clear() {
    removeRange(0, size());
  }
  
  protected void removeRange(int fromIndex, int toIndex) {
    ListIterator<E> it = listIterator(fromIndex);
    for (int i=0, n=toIndex-fromIndex; i<n; i++) {
        it.next();
        it.remove();
    }
  }
}

java/util/ArrayList.java (骨格実装を利用する実装クラス)

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  
  public E get(int index) {
    rangeCheck(index);
    return elementData(index);
  }
  
  private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
  }

  public void clear() {
    modCount++;
    // clear to let GC do its work
    for (int i = 0; i < size; i++)
      elementData[i] = null;
      size = 0;
  }

}

一ヶ月間以上かけてこのシリーズを書いてきましたが、めちゃくちゃ疲れました!笑 構文やルール・機能だけが記載されてるだけのありふれた記事からは得られる事が少なくて「なんでこんな浅い記事しかないんだ!ないなら作ってやる!!」という気持ちで始めましたが、後悔しました........笑

一年目なので間違いや理解不足も多々あるかと思いますがその際はご指摘いただくと泣いて喜びます!お待ちしております!