初めに
本記事は、私のブログにある『継承・インターフェイス・抽象クラス』シリーズ』を一つの記事にまとめたものです。全部で 106,227文字ありました...。この記事では 25,000文字くらいでお話しできればと思います。
なるべく簡潔にまとめたいと思います。詳細を知りたい方は以下のブログを読んでいただければと思います。
デザインパターンを勉強している時に、「は?インターフェイス?抽象クラス?何が違うの?」とか「継承ってなんか解ったような解らんような感じなんよな〜」って感じました。調べてみると、ほんとに期待した記事は出ませんでした。
- interface を implements すると、クラスにメソッドの実装を強制できる
よく見ます。「で??だからなんなん??いつ使うんや!」という気持ちでした。構文とかルールを公式ドキュメントから切り貼りしただけでしかない記事ばかりで結局は使い所がわからず....。便利な道具っぽいけど、メリットデメリットわからないまま使うなんて気持ち悪い。せやったら、自分が書くしかない!思って書き始めましたが、非常に後悔しました。むず過ぎました。なんとか網羅的に本質的にまとめれたかな?と思いますので、同じ気持ちで苦しんでる方の助けになれば幸い。
『決定版:Javaの継承と抽象化 ~ 第1部 継承 ~ 』
23745文字
『改訂版:Javaの継承と抽象化 ~ 第2部 インターフェイス ~』
40249文字
『決定版:Javaの継承と抽象化 ~ 第3部 抽象クラス ~ 』
19421文字
『決定板:Javaの継承と抽象化~ 第4部 抽象クラスとインターフェイスの使い分け』
22812文字
再度言いますが、構文とかどうでもいいんです。例えば、インターフェイスは抽象化や多重継承問題の解決のためにあります。その結果として、構文やルールができています。誕生した背景には必ず問題があり、そのソリューションとして道具が登場します。その道具は、その問題を解決するための機能以外のことはできない方がいいのです。本来の目的以外の余計な役割を持たせれば、他の道具と目的が重複するかもしれません。無駄であり、無用な混乱の元です。そういった事が起きないためのルールであり構文です。ルールや構文などは二次的なものに過ぎません。
重要なのは、
- 「なんのためにあるのか?」
- 「何ができて、何ができないのか」
- 「何であり、何でないのか」
を理解する事だと思います。ではまず、結論からお話しします。
1. 結論
ここでは結論だけ端的に述べます。詳しくは後述しますので詳細はそちらをご覧ください。さらに詳細を知りたい方は、上記で紹介した過去記事をご覧ください。
❐ 継承
『継承よりコンポジション(合成)、もしくは継承よりデリゲーション(委譲)』
❐ 抽象クラス
『適切な抽象クラスは具象クラスとほぼ同じであり、サブクラスをスーパークラスと同一視したい場合に抽象クラスを用いる』
❐ インターフェイス
『多重継承を行いたい場合』
『インターフェイスはサブクラスに振る舞い・性質を付与する』
『クライアントとプログラム(クラス・モジュール・APIなど)の仲介役』
❐ 抽象クラスとインターフェイスの使い分け
参考記事:ORACLE Java ドキュメンテーションJava™ チュートリアル
抽象クラス
- 密接に関連する複数のクラス間で共通する性質をコードとして共有、その関係性を表現したい
- public 以外のアクセス修飾子 (protected や private など) が必要
- メンバ変数に「static」「final」以外のメンバ変数が必要。
インターフェイス
- クラス階層・クラス間の関係性を無視して、振る舞い・性質をオブジェクトに付与・定義したい
- クラスに複数の振る舞い・性質を付与するために、多重継承(型)を行いたい
- メンバ変数に「static」「final」な定数しか持てないため、状態を継承させない
- 呼び出し側から具象クラスをカプセル化(情報隠蔽)したい場合(多態性の前提)
2. 前提知識
ここでは、上記の結論を理解するのために必要な前提知識を紹介します。
- 継承は二種類ある!「実装」と「型」の継承
- クラス・抽象クラス・インターフェイスの違い
- オブジェクト指向の設計原則「抽象化」
- 設計原則「SOLID」
- カプセル化・データ隠蔽・情報隠蔽とは?
❐ 継承は二種類ある!「実装」と「型」の継承
Java における『継承』には2種類の目的があります。
型(仕様)の継承:implements
どのようなメソッドを持っているか、どのように振る舞うかを継承。型・メソッド名・引数と引数の方・戻り値の型、といったシグネチャ。
実装(コード)の継承:extends
どのようなデータ構造を使い、どのようなアルゴリズムで処理するかを継承。
仕様を実現するための具体的なメソッド・実装の詳細、コードのことです。
- 型(仕様)継承は『implements』(インターフェース)
- 実装継承・型(仕様)継承は『extends』(クラス・抽象クラス)
Javaでは、二種類に継承を分けて、型(仕様)の継承のみ多重継承できるようにしています。これはデータ構造の衝突やクラス階層の複雑化などを回避するのが目的です。また、extends による継承は「型(仕様)の継承」「実装の継承」両方とも行われます。
これらは『実装の多重継承』が引き起こす問題に対する言語毎の対応の結果、という歴史の背景があります。『実装の多重継承』はメリットがある一方で大きなデメリットを生み出す可能性もあるので、Javaでは『型』と『実装』を同時に多重継承することを禁止する、という方法で安全を確保しています。
言語毎で『実装の多重継承問題』に対する対応は異なります。実装の多重継承が許可されている言語(C++やPerlやRuby)では、Mix-in(ミックスイン)と呼ばれるテクニックがあります。
❐ クラス・抽象クラス・インターフェイスの違い
-
クラス
- ①生成機の役割(インスタンス)
- ②実装(構造やコード)・仕様(型・振る舞い)としての役割
- ③再利用の役割として実装継承(extends)を可能にする
- これにかんしては、これまで説明した通り委譲を用いるべきです。再利用という観点からみると、クラスという単位は大きすぎる単位なのです。
- ④データ・状態(不変オブジェクトの場合は禁止)と関数のカプセル化
-
抽象クラス
- 具体的なオブジェクト(もの)を抽象化し、共通処理としてスーパークラスにカプセル化し、具象クラスに個別の実装を延期(extends)させる
-
インターフェイス
- オブジェクト(もの)ではなく振る舞い(What:なに)を抽象化。〜として扱うという性質をクラス階層に関係なく特定のクラスに付与できる
- 多重継承(型のみ ※defult実装では実装の継承も可能に)を可能にしクラス階層を無視した型継承(implements)を実現
- インターフェイスは抽象クラスより完全なデータ抽象化で具象クラスのカプセル化を実現し、ポリモーフィズムも可能になる
❐ オブジェクト指向の設計原則「抽象化」
よく言われる
- 継承
- ポリモーフィズム
- カプセル化
が有名です。ただ、カプセル化は別にオブジェクト指向特有の考え方ではありません。上記に加えて幾つかの設計原則が存在しますが、ここで紹介するのは『抽象化』です。
抽象化とは、さまざまな詳細の中から本質的な特徴を抽出しそれ以外を削り落とすことです。決して共通の機能を取り出すわけではありません。抽象化を行うと抽象化されたものは、小さく・少なく・短く、なります。
人間の思考能力は限られていて複雑な構造をそのまま理解することは非常に困難です。人間はイメージや図などでの理解が一番認知的負荷が低いです。
複雑なメソッドでも、優れた命名であれば短く端的に表現され、その名前だけで概要を理解できますね。
「抽象化」とは、対象の本質をみつけ、簡潔に表現することです。
Java では、抽象化は抽象クラスまたはインターフェースを使用して行われます。2つの主な違いは、抽象クラスは部分的な抽象化も提供できるのに対し、インターフェイスは常に完全な抽象化を提供すること(Java8からは default メソッドにてインターフェイスでも部分的な抽象化が行えますが、思想が異なります)、そして状態を継承するかしないかがコアな違いになります。
抽象化を用いる事で「ポリモーフィズム」や「カプセル化」「情報隠蔽」「本質的な再利用」が直接敵・関節的にも実現できます。
❐ 設計原則「SOLID」
引用元:Wiki ~SOLID~
SOLID(ソリッド)は、ソフトウェア工学の用語であり、特にオブジェクト指向で用いられる五つの原則の頭字語である。ソフトウェア設計をより平易かつ柔軟にして保守しやすくすることを目的にしている。その特徴はインターフェースを仲介にしての機能の使用と、インターフェースによる機能の注入である。
SOLIDは以下の五つの原則です。
- 単一責任の原則 (SRP Single Responsibility Principle)
- オープンクローズドの原則(OCP: Open-Closed Principle)
- リスコフの置換原則 (LSP: Liskov Substitution Principle)
- 依存関係逆転の原則 (DIP: Dependency Inversion Principle)
- インタフェース分離の原則 (ISP: Interface Segregation Principle)
引用元:アジャイルソフトウェア開発の奥義
これらの原則は、何十年にも及ぶ先人の経験の蓄積から生まれたソフトウェア工学の成果である。 これらの成果は一人の頭脳から生まれたものではない。非常に多くのソフトウェア開発者や研究者の 思索や論文を集大成したものなのだ。ここではオブジェクト指向設計の原則として取り上げている が、実はこれらはソフトウェア工学の世界で長年に渡って議論されてきた原則の特別なケースなので ある。
単一責任の原則 (SRP Single Responsibility Principle)
『クラスを変更する理由は1つ以上存在してはならない』
2つの役割を別々のクラスに分けるのは非常に重要です。役割が複数あれば、 その1つ1つが変更理由になってしまうからです。仕様要求が変わると、クラスの役割が変化するので、どのように変化したのかを見れば、変更部分が浮き彫りになります。しかし、クラスが複数の役割を背負ってしまうと、クラスを変更する理由も複数になってしまい、変更部分がぼやけてしまいます。
また、クラスが複数の役割を背負っているような場合、それらの役割は結合してしまい、その結果、ある役割が変更を受けると、そのクラスが担っている他の役割も影響を受け、不具合が生じる可能性が生じます。こういった結合は、ある部分が変更を受けると、予想もしない形で壊れてしまうような、もろい設計を生み出してしまいす。
関数やメソッドといった単位のものは、単一の機能のみを提供し、適切な命名が行われるべきです。
機能が増えれば増えるほどバグの生じる可能性が増え、改修の人的時間的コストが増大し、適切な抽象化・命名をしづらくなり、信頼できるドキュメントを残しづらくなるからです。
オープンクローズドの原則(OCP: Open-Closed Principle)
クラス、モジュール、 関数などは拡張に対して開いて(オーブン:Open) いて、修正に対して閉じて (クローズド:Closed) いなければならない。
OCPをうまく適用してシステムをリファクタリングできれば、あとで似たような変更が出てきたときに新しいコードを追加するといった方法で対処できるため、すでに動いている既存のコードには一切修正を加える必要がありません。
-
拡張に対して開かれている (オープン : Open )
これは、モジュールの振る舞いを拡張できるという意味。アプリケーションの仕様要求が変更されても、モジュールに新たな振る舞いを追加することでその変更に対処できる。つまり、そのモジュールの処理内容を変更できるということです。 -
修正に対して閉じている (クローズド: Closed)
モジュールの振る舞いを拡張しても、そのモジュールのソースコードやバイナリコードはまったく影響を受けません。
リスコフの置換原則 (LSP: Liskov Substitution Principle)
派生型はその基本型と置換可能でなければならない。
「サブクラスはスーパークラスの代わりとして振舞えなければならない」という原則です。
SがTの派生型であれば T型のオブジェクトが使われている箇所は全て S型のオブジェクトで置換可能になります。
引用元:多相性とジェネリクス
T型の引数を取ったりT型の戻り値を返したりする関数があったとき、型Tに対して互換性がある別の型Uを持ってきたら、「T型の引数を取ったりT型の戻り値を返したりする関数」と「U型の引数を取ったりU型の戻り値を返したりする関数」に互換性があるかどうか、といったことを考える時の話です。
リスコフの置換原則は[開放/閉鎖原則]を有効にする主要な役割を果たす原則の一つです。
たとえば、WriteRead という抽象クラスがあります。こちらは名前の通り、何かを書き込みし読み込みを行うことを表現しています。この WriteRead を継承したサブクラス Write クラスを作成します。このクラスは名前の通り、書き込みのみを行うクラスです。その場合、スーパークラスではできた読み込みがサブクラスでは出来なくなってしまいます。
これはスーパークラスと派生クラスの『置換不可』であり、サブクラスでの『機能の退化』を意味しているので、リスコフの置換原則違反となります。
その他、スーパークラスでは実装されているのに、サブクラスでは何もしないメソッドなど。
依存関係逆転の原則 (DIP: Dependency Inversion Principle)
上位のモジュールは下位のモジュールに依存してはならない。 どちらのモジュール も「抽象」に依存すべきである。
「抽象」は実装の詳細に依存してはならない。 実装の詳細が「抽象」に依存すべき である。
上位モジュールとは継承関係で例えるとスーパークラスに該当し、下位モジュールとはサブクラスが該当します。
アプリケーションの方針に基づく重要な判断やビジネスモデルを含み、アプリケーションの存在理由を決定づけているのは上位のモジュールです。それにもかかわらず、上位モジュールが下位モジュールに依存すると、下位モジュールの変更が直接上位モジュールに影響を与え、上位モジュールまで変更を強制されることになります。
本来であれば、アプリケーションの方針を決めている上位レベルのモジュールが、実装の詳細を担当している下位レベルのモジュールに影響を与えるのが筋です。
ビジネスルールを担当する上位レベルのモジュールは、実装の詳細を担当している下位のモジュールとは独立し、立場も上であるべきです。
ただ、Robert Cecil Martin、通称「ボブおじさん」いわく以下の様な前提があるようです。
引用元:Robert C. Martin, C++ Report, May 1996
このコラムでは、OCPとLSPの構造的な意味について説明します。これらの原則を厳密に使用することで得られる構造は、それ自体で一つの原則に一般化することができます。私はこれを「依存関係逆転原理」(DIP)と呼んでいます。
インタフェース分離の原則 (ISP: Interface Segregation Principle)
クライアントに、クライアントが利用しないメソッドへの依存を強制してはならない。
利用しないメソッドに依存してしまうと、クライアントはそういったメソッドの変更の影響を受けやすくなります。インターフェイスで定義された抽象メソッドをクライアントが使用しない場合、クライアントは無関係のはずですが、それらに依存していると、必要のない影響を受けることになります。
そういった不必要で不本意な結合を避けるために、すべてのインタフェースを1つのクラスに押し込めてしまうのではなく、関連性を持ったインタフェースはグループ化し、抽象型としての基本クラスとして分けて利用すべきです。
引用元:Interface-Segregation Principle (ISP) - Principles of Object-Oriented Class Design
- 「多くのクライアント固有のインターフェースは、1 つの汎用インターフェースよりも優れています」
- 「あるクラスから別のクラスへの依存関係は、可能な限り最小のインターフェイスに依存する必要があります」
- 「クライアント固有のきめの細かいインターフェイスを作成します。」
- 「クライアントは、使用しないインターフェースに依存することを強いられるべきではありません。この原則は、ファット インターフェイスの欠点を扱います。ファット インターフェイスはまとまりがありません。言い換えれば、クラスのインターフェースは、メンバー関数のグループに分割する必要があります。」
❐ カプセル化・データ隠蔽・情報隠蔽とは?
-
カプセル化
関連するデータとそれを必要とする処理のみをひとまとめにして、単一の振る舞いを行うまとまりにする。 -
データ隠蔽
フィールドの外部からの可視性や操作を制限する。アクセス修飾子をクラス、メソッド、フィールドに付与。 -
情報隠蔽
具体的な実装を外部から隠蔽し、外部からは公開された手続き(変数.メソッド()のような)でしか操作ができないようにすることで部分の独立性を高めること。インターフェイスによる抽象化で実現され、非公開のプログラムと公開されているプログラムを分けたり、仲介したりする。
3. 継承
継承を用いた設計は相当制限があるし、あるべきだし、結構危ないし、注意点・落とし穴は多いし、正しく運用するのは大変。基本的に
『 継承よりコンポジション(合成)、もしくは継承よりデリゲーション(委譲)』
がいい。
継承を用いる場合は以下が前提、もいしくは条件
- サブクラスがスーパークラスと同一視できる場合
- is - a であり、リスコフの置換原則違反ではない
- 階層が深くならない
- 多段階継承の様な差分プログラミングによる実装の再利用ではない
- 継承が前提の場合
- Abstract Pattern / Template Method / java.util.AbstractList など
- クラス単位での再利用が単一責任原則に違反しない
- 継承のために設計および文書化する、でなければ継承を禁止する
- クラスはOverride可能なメソッドの自己利用を文書化(JavaDoc)しなければならない
- 最低でも三つのサブクラスを作成。そのうちの一つか二つは、スーパークラスの作成者以外に書いてもらい、テストする。
5〜7は 名著:Effectiv Java で語られています。実際にできるかどうかは別として、継承を使うって実はめっちゃめんどくさくて大ごとなんだ....って事が伝わりますね。基本的に使わん方がいいと思いました。Rust と Go は継承という概念が消えたぐらいですからね。Go はクラスもないです。構造体とメソッドがあります。クラス単位の再利用を行う継承ですが、再利用の単位でかすぎるんでしょうね。
参考:The Go Programming Language Specification
あと、 Java の開発者である James Gosling さんはもしできるなら、継承はいつも避けた方がいいと明言しています。正確には『実装の継承』です。この考え方は前述した「前提知識」の項で説明しています。
参考:Why extends is evil; Improve your code by replacing concrete base classes with interfaces
4. 抽象クラス
『適切な抽象クラスは具象クラスとほぼ同じであり、サブクラスをスーパークラスと同一視したい場合に抽象クラスを用いる』
例えば、「色」という概念を表現するクラスが「抽象クラス」で、それらの概念を具体化するのがサブクラスです。「青」「赤」「黄」などです。スーパーもサブも全く同じものですが、具体性が異なります。概念が抽象クラスになります。具体的には「Abstract Pattern」「Template Method」が例になります。
抽象クラスは、クラスとインタフェースの中間に位置するもので、型を定義して(クラスと同じように)具体的な実装を含めることができますが、具体的な実装のない抽象メソッドを持つこともできます。抽象クラスは実装の詳細が未定義であり、継承されたサブクラスで埋める必要があります。部分的に実装されたクラスと考えることもできます。
抽象クラスでは、複数のオブジェクトで共通する概念を表現するコードを抽出し、それをスーパークラスの具象メソッドとして定義します。簡単にいえば共通部分を括り出して重複を削除しています。大事なのは、その括り出される対象のオブジェクト達の選び方です。
- LIP:リスコフの置換原則が定める「サブクラスはスーパクラスと置換可能」であること。
- 括り出される対象のオブジェクト達であるサブクラスと、括り出されたコードの抽出先であるスーパクラスとが「is - a」の関係であること。
これらが守られている必要があります。
つまり、括り出したコード部分はスーパークラス・サブクラスの根幹的な性質である必要がります。これはクラスの継承でも同様のことが言えます。
この「LIP」と「is - a」が成立しないのであれば抽象クラスとしてまとめるべきでもなければ、それを継承してもいけないのです。継承は機能の受け継ぎとして用いられるイメージがつよいかもしれません。その側面もあると思いますが、それのみで継承を行うのは間違いです。また、「LIP」 と 「is - a」 の関係は抽象クラスのみならず、継承全般に適用される考え方です。クラスの継承(extends)・抽象クラスの継承(extends)・インターフェイスの継承(implements)全てに当てはまります。
5. インターフェイス
『多重継承を行いたい場合』
Java においての多重継承は「実装の多重継承は禁止するが型の多重継承は許可する」という形をとっています。多重継承が引き起こす諸々の問題は実装の多重継承が引き起こすためですね。
インターフェイスでは元々は実装の継承ができませんでした。default メソッドの登場で実装の多重継承にも注意しなくてはならなくなりましたが、元々の思想的にインターフェイスの具象メソッドの存在意義は実装の多重継承ではなく、公開されたAPIとしてのインターフェイスの互換性を保つものでした。Java は後方互換性を重視する言語です。
型の多重継承のみを許可すれば、基本的には多重継承の問題を避けれるという形で擬似的に多重継承を実現しました。ですが実装の継承ができないということは同じ様な実装内容を複数箇所に記述しなくてはいけないとう問題を新たに生じることになります。型の多重継承では、この問題を解決できていません。
これに対して、 default メソッドを使用できるという考え方もあります。元々の登場背景としては使用意図としてはし自然な考え方のように感じます。ただ、元々は抽象クラスとインターフェイスを用いた抽象骨格実装という考え方もあります。どちらの方法でも、基本的な実装をデフォルトで用意したいという要求に応える事ができます。
『インターフェイスはサブクラスに振る舞い・性質を付与する』
インターフェイスは『What:なにをするか(できるか)?』と、それを行うのに必要・必須な抽象メソッドが定義されています。ですが、『How:どうやって実現するか?』は定義されていません。その定義を行うためには具象クラスでは、継承した抽象メソッドの具体的な内容を定義する必要があります。
Javaの歴史の経緯的には「多重継承問題」を解決するために導入され、状態を保持せず完全な抽象化を行なっておりました。Java8からデフォルト実装の導入より、具象メソッドを持つ事が可能になったため抽象クラスと同様に具体的な実装も持つ事ができるようになりました。
そのため、抽象クラスとインターフェイスの境目や違いが曖昧になりましたが、依然として変わらない大きな違いは「状態を保持しない」点です。
『クライアントとプログラム(クラス・モジュール・APIなど)の仲介役』
インターフェイスには、二者間の結びつきを弱くする効果があります。いわゆる疎結合です。その他、翻訳・読み替え・差分吸収・形が合わないものを統合・信用できないしたくないライブラリを隔離、など様々な効果があります。
クラスやAPIの情報隠蔽を行う際に、使い側と情報隠蔽された側での接続部分・仲介役にもなります。パッケージやクラスのアクセス修飾子で非公開・公開の設定を制御する情報隠蔽を行う場合、使う側と非公開なプログラムと繋ぐ接点としての公開部分でもあります。
非公開(外部APIやライブラリ) - 用意された手続き(インターフェイス)- クライアント(呼び出し側。ユーザーやプログラム)
の様な形です。
また、内部の詳細を隠して機能を表示する「データの抽象化」も行います。内部のコードを読むことを許容しつつ、内部に依存したプログラムは書かないようにすることが可能です。
6. インターフェイスと抽象クラスの使い分け
抽象クラスはスーパークラスとして抽出された概念・性質であり、インターフェースはオブジェクトに性質を与えます。双方とも、サブクラスに性質の具象化(実装)を強制し機能を実現します。契約の関係と表現できます。
抽象クラスで定義された型を実装するには、クラスはその抽象クラスのサブクラスでなければないけませんし、それが目的の一つですね。インターフェースの場合には、ルールに従って要求されるメソッドを全て定義しているクラスであれば、クラス階層のどこに位置していても、インターフェースを実装することが許されています。
以下はクラス・抽象クラス・インターフェイスの違いを一覧にまとめたものです。
❐ クラス・抽象クラス・インターフェイスの違い 一覧表
クラス・抽象クラス・インターフェースの違い(赤文字は重要部分) | ||||
---|---|---|---|---|
クラス | 抽象クラス | インターフェイス | ||
予約語 | class | abstract | interface | |
継承宣言 | extends | extends | implements /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(関数型インターフェイス) と呼び、無名クラスによるインスタンス生成がラムダ式 でできるようになりました。
参考記事:ORACLE Java ドキュメンテーションJava™ チュートリアル
それでは結論の項で紹介した以下の事項について解説していきます。
抽象クラス
- 密接に関連する複数のクラス間で共通する性質をコードとして共有、その関係性を表現したい
- public 以外のアクセス修飾子 (protected や private など) が必要
- メンバ変数に「static」「final」以外のメンバ変数が必要。
インターフェイス
- クラス階層・クラス間の関係性を無視して、振る舞い・性質をオブジェクトに付与・定義したい
- クラスに複数の振る舞い・性質を付与するために、多重継承(型)を行いたい
- メンバ変数に「static」「final」な定数しか持てないため、状態を継承させない
- 呼び出し側から具象クラスをカプセル化(情報隠蔽)したい場合(多態性の前提)
抽象クラス
❐ 密接に関連する複数のクラス間で共通する性質をコードとして共有、その関係性を表現したい
「スーパークラスでの具象メソッドの記述」
抽象クラスにおいて、具象メソッドは複数のオブジェクトが共通して持っている性質を表します。例えば、音楽再生・動画再生で共通する性質は「何かを再生する」というものです。共通する概念をスーパークラスとサブクラス間で共有するもので、決して機能の再利用ではありません。
インターフェイスは元々、具象クラスの存在は許可されていませんでした。インターフェイスの最大のデメリットは、一度公開してしまったインターフェースにメンバーを追加すると、そのインターフェースを継承しているクラスに破壊的な影響が生じることでした。
設計・公開時に想定できなかった修正や機能拡張に際して柔軟性や互換性を持たせたいという場面で default メソッドの実装が許可されました(Java8)。インターフェイスは後からの変更が非常に困難なため、慎重な設計が必要でしたが、具象メソッドである default メソッドの追加により柔軟に対処できる様になったんですね。
「スーパークラスとサブクラスの関係性」
is - a やリスコフの置換原則を守るのは前提として、抽象クラスとインターフェイスでは行いたいことや思想が異なる点に注目すべきです。
抽象クラス
- 抽象クラスは複数のオブジェクトから、抽象化された本質・性質・共通点を持つ
- 実装クラスのインスタンスに機能(実装)を延期する
インターフェイス
- 抽象(インターフェイス)依存・具象(実装)非依存
- 仕様(What)と実装(How)の分離
- 実装クラスのインスタンスに機能(実装)を強制させる
抽象クラスと具象クラスはほとんど同じ「もの」を表現し、サブクラスにて具体的なもの・オブジェクトを表現します。例えば、抽象クラス「色」の具象クラスは青・赤・黄、といった様にどちらも色であることは「同じ」です。ですが、色というのは概念や性質であり、黄色とは色の具体例です。色という抽象的な概念を、具象化し赤というクラスを表現する場合、抽象クラス・具象クラスは密接に関連すべきものである事がわかり、それを抽象クラスを継承(extends)することで実現します。これにたいして、インターフェイスはそのような関係性は必要ありません。スーパータイプとサブタイプは同じオブジェクト・ものではありません。
インターフェイスは「振る舞い・性質」を表します。何をするのか?何ができるのか?このような振る舞い・性質を、継承(implements)する具象クラスに付与します。抽象クラスとサブクラスは同じものであると表現しましたが、インターフェイスを継承(implements)した具象クラスは「同じものではないが、同じ振る舞いをするという共通点を持ったオブジェクト」として扱う事ができる様になります。
「空を飛ぶ」「食べる」という性質は、カラス・コウモリ・モモンガを一つのグループにひとまとめにできます。それぞれの生き物は、flyable interface と eatable interface を継承(implements)しているオブジェクトであると表現できます。
また、インターフェイスの多重継承により一つのオブジェクトが多面性を獲得する事ができます。
抽象クラス・インターフェイスをインスタンス化できないのは、概念や性質は実際には存在しないものだからです。このような抽象的な部類を表すための表現が抽象クラスとインターフェイスであり『抽象型』と呼ばれます。抽象とは「実体がない」という意味です。
❐ public 以外のアクセス修飾子 (protected や private など) が必要
「アクセス修飾子」
抽象クラスは、クラス間で密接に関係していることを表現しそれを継承(extends)で実現します。継承を大前提としているため protected は使用できるが private は使用できません。サブクラスから継承できる様に出来なくてはいけません。protected はサブクラスからしかアクセスできない状態です。
インターフェイスはクラス階層から独立し、特定のクラスに振る舞いを付与することを目的とし、継承(implements)前提のため public 以外ありません。どこからでも implements 出来なくてはいけないのです。publicをつけなかった場合(アクセス修飾子なし)、同パッケージ内からの利用だけに制限されることになります。
❐ メンバ変数に「static」「final」以外のメンバ変数が必要。
「メンバ変数」と「状態(データ)」
抽象クラスとインターフェイスでは表の通り、保持できるメンバ変数が異なります。細かいルールは説明しませんが、何を実現したいかというと、状態を持つか持たないか?です。
公開インターフェイスので互換性問題の件で、インターフェイスが具象メソッドを保持できる様になったため、抽象クラスとの使い分けや存在意義がぼやけた様に思いますが、インターフェイスが一貫して変わらないのは『状態を持たない』という点です。これを言い換えると以下の様に表せます。
- クラス・抽象クラス:状態を持てる代わりに単一継承しかできない
- インターフェース: 状態を持てない代わりに多重継承できる
「状態を持つ」というのは、メンバ変数としてインスタンスフィールドを持てるかどうかです。つまりデータを持てるかどうかです。インターフェイスはメンバ変数として、静的最終な定数しか保持することを許可されていません。インスタンスフィールドは、あるオブジェクトを参照していますが、そのオブジェクトにはデータ、つまり変更されるであろう状態が内包されています。インターフェイスがその様なメンバ変数を保持することは禁止されています。状態を保持させないためです。インターフェースは、状態を持たない「純粋な振る舞い」でなければならないということですね。
参考記事:インターフェースを「契約」として見たときの問題点 ― C#への「インターフェースのデフォルト実装」の導入(前編)
インターフェイス
❐ クラス階層・クラス間の関係性を無視して、振る舞い・性質をオブジェクトに付与・定義したい
「クラスツリー内の立ち位置」と「型継承時の制限」
インターフェイスは型階層から独立している
引用元:Effective java
インターフェースは、非階層的な型を構築するためのフレームワークを可能にします。
インターフェースは対象のクラスが、クラス階層のどこに属していても実装を割り込ませる事が可能です。抽象クラスもどの階層においても実装できますが、Javaでは『実装の継承』は単一継承しか許されていない為、『型(仕様)の継承』、つまり型定義をする為に抽象クラスを使用するのは非常に不便です。extendsによる継承では「実装の継承」「型(仕様)の継承」がセットになるからです。
これに対して、インターフェイスはクラス階層外から実装を割り込ませることが可能です。クラスツリーから独立しているからです。全てのクラスは暗黙的に Object クラスを継承しています。クラスは Object クラスを root としいたツリー階層になっているので、抽象クラスはクラスツリーの一部を構成しています。
もしインターフェイスがない場合で、全部継承(extends)で行わなければいけないとしたら実装に必要な階層まで継承(extends)を続けるとなると、無関係のクラスが不必要な実装の継承を拒めません。現実的には、関係クラスを洗い出し全部の上位クラスとなる位置にスーパークラスを定義する必要があります。
❐ クラスに複数の振る舞い・性質を付与するために、多重継承(型)を行いたい
「多重継承」
型(仕様)継承による解決
多重継承の問題はすべて、メソッドの実装とフィールドといった実装継承に関係しているのが解ります。実装の多重継承は厄介ですが、型の多重継承には問題がないように見えます。実装の多重継承は、委譲(他のオブジェクトへの参照)で代用できるので、それほど重要ではありませんが、型(仕様)の多重継承はしばしば非常に便利で、合理的な方法で簡単に置き換えられるものではありませんでした。
そこで当時の Java 設計者は、実装(コード)には単一継承しか認めず、型(仕様)には複数継承を認めるという解決策を行いました。
それがインターフェイスです。インターフェースは 振る舞い を定義します。他の言い方だと「型」「仕様」「シグネチャ」「ルール」「規格」などでしょうか。
見る人によって同じクラスでも違う振る舞いをするものに見えるという事を表現するのがインターフェースの多重継承の役割です。そのクラスに多面性を持たせる、複数の特徴を持たせるといえます。
ArrayList は以下の5つの振る舞いを持ちます。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
- clone メソッドの使用が許可されているオブジェクト
- シリアライズ実行可能なオブジェクト
- ランダムアクセスがサポートされたオブジェクト
- シーケンシャルアクセスだとパフォーマンスを保証しない
- foreachループ文の対象オブジェクト
- 拡張for文で扱えるということ
- 順序付けられた可変長な配列で、要素の重複を許可したオブジェクト
このような多面性が型の多重継承で実現されているため、複数あるコレクションオブジェクトの中でも ArrayList は「i番目の要素を取り出す」という処理が早いという特徴を持っています。 より正確に言うと、内部的には要素を配列で保持しているので インデックスによるアクセスは高速ですが、リストの途中に要素の挿入・削除をする場合の性能は LinkedList に比べて劣ります。
これを型の多重継承を用いずに、ArrayList の様に単一のクラスでこの多面性を実現するには、複数の具象クラスを用いるのが一般的だと思います。委譲や標準的な継承(extends)などの使用が必要になります。これらはカプセル化や疎結合(インターフェイスと比較して)を破壊し、変更容易な構造とは言い難いものになります。
また、
「メンバ変数」と「状態(データ)」
で紹介した考え方も通じる考えです。
❐ 呼び出し側から具象クラスをカプセル化(情報隠蔽)したい場合(多態性の前提)
「パッケージ間などでの結合度」と「不変・可変の分離へのアプローチ」
具象に依存するのではなく、抽象に依存することで、疎結合にすることができますよということですね。
大雑把に言うと、具象に依存すると一部の変更が全体に影響するかもだけど、抽象依存するとめっちゃ限定できる様になるかもよ、っていう話です。
また、クラス指向なオブジェクト指向言語では『不変を軸に、可変であろう箇所をクラスに抽出する』を想定しています。この辺りは SOLID が全部ぶち込まれた話になると思っています。
詳細は「第二部 〜インターフェイス〜」の 2-5-2. インターフェイスで可変を抽出する をご覧ください。
終わりに
いかがでしたか?分かりやすくまとめたかったのですが、結局難しい話になってしまいましたが、なんとか25900文字くらいでまとめれました。
エンジニアとして2年目を迎えたばかりですので、不足や間違いがあればご指摘いただけると嬉しいです!!