よしたろうブログ

設計・人文知・歴史・哲学・漫画とかの話が好きです。

『改訂版:Javaの継承と抽象化 ~ 第2部 インターフェイス ~』

こちらは以下記事の改訂版の記事になります。

『旧版:Javaの継承と抽象化~ 第2部 インターフェイス ~』

以下の項目を追加し、その他にも修正加筆を行いました。

はじめに

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

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

前部 「継承」

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

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

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

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

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

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

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

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

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

2. インターフェイスとは?

Javaインターフェイスが使用される理由

  • 多重継承問題の解決
  • 疎結合の実現
  • 抽象依存の実現
  • ポリモーフィズムの実現
  • 情報隠蔽時の使用する側と使われる非公開側の接続口としての公開部分
  • クラス階層から独立した型継承の実現

継承(extends)による多重継承問題をインターフェイスがどう解決するのかを説明する前に、インターフェイスの基本を説明します。

また、基本的に Java8 defaultメソッド機能が追加される前のインターフェイスについて話しています。 Java8 以降のインターフェイスについても後述します。

インターフェイスは上記の通り多重継承問題の解決のために登場しましたが、カプセル化ポリモーフィズムを表現するためにも用いられます。上記も交えてインターフェイスの説明をしていきたいと思います。

非常に簡単に言えば、何かと何かの『境目』の部分・別のものと別のものを繋ぐための『接点』のことを表し、同時に『規格』『ルール』『振る舞い』『機能』を表現します。現実世界でもプログラミングの世界でも同じ概念です。また、インターフェイスは外部からクラスを呼び出す時の接点となります。外部から呼ばれることを前提としているので、アクセス修飾子は public・default(なし) のみとなっています。

プログラミングの世界では何と何を繋ぐのでしょうか?クラスとかオブジェクトですね。

2-1. インターフェイスの特徴

  1. メソッドは抽象メソッドしか(元々は)持てない → 実装がない
    1. 暗黙的に public abstract が付与される
    2. 抽象メソッドは、デフォルトで public かつ abstract が付与される
    3. Java8 では具象メソッドの保持も可能に。ただし抽象クラスとは目的が異なる(後述)
    4. Java9 では private な具象メソッドも持てるようになった
  2. インターフェイスを implements した具象クラスでのみ実装ができる、抽象メソッドのオーバーライドを強制
    1. override しないとコンパイルエラー
    2. override は「上書き」ではなく『再定義』という意味
    3. Java8 では具象メソッドの保持も可能に。ただし抽象クラスとは目的が異なる(後述)
    4. Java9 では private な具象メソッドも持てるようになった
  3. 直接インスタンス化できない
    1. 匿名クラスを使用すると可能
    2. 関数型インターフェイスラムダ式を使用すると可能
  4. 多重継承ができる
    1. 実装の多重継承はできないが、型の多重継承は可能
  5. コンストラクタの記述はできない
    1. 初期化すべきフィールドは持てない
  6. 定義できるフィールドは static final な定数のみで宣言と同時に初期化が必須
    1. 暗黙的に public static final が付与される
    2. 静的(static)とはインスタンス化しなくてもアクセスできるという意味
    3. 最終(final)とは上書きできないという意味。
      1. 余談だけど、参照型は参照先(メモリ)が final になるだけ
  7. 設定できるメンバ
    1. 抽象メソッド
    2. 最終静的定数フィールド
    3. default メソッド(Java8 以降)
      1. 既存のインターフェースに関連して何らかの機能をまとめておきたい時
    4. static メソッド(Java8 以降)
      1. 実装クラス側で明示的に実装されない場合既定で採用される実装
    5. private な具象メソッド (Java9 以降)
  8. インターフェイス自体のアクセス修飾子は public と default のみ
    1. どこでも使える様でなければ意味なし
    2. default(指定なし) は同一パッケージ内からのみアクセス許可

2-2. インターフェイスのメリット

  1. 多重継承(型のみ)が可能
  2. 具象依存から抽象依存を実現
  3. 処理の中の可変を抽出しモジュールを安定化する
  4. 情報隠蔽により実装が隠蔽される(実装の複雑さを減らすため)
  5. クラス間・パッケージ間での疎結合を実現できる
  6. クラス階層から独立しているため、クラス階層と無関係に実装することができる
  7. ポリモーフィズムが実現できる
  8. 複数人の大規模の開発において、共通処理をするメソッド名に統一性を持たせ、処理の画一性をある程度制御できる
  9. メソッド実装漏れ、メソッド名の間違いがあればコンパイルエラーされるのでコーディングミスを防ぐ

2-3. インターフェイスのデメリット

  1. あとからの変更が困難
  2. 特に公開APIとして世界に公開されている場合は一つの変更が世界中に影響する
  3. インターフェースが無闇に生産されると複雑化し管理コストが大きくなる
  4. インターフェイスによる擬似的な多重継承では実装の継承はできない

2-4. クラスとインターフェイスの違い

型(仕様)の継承:implements どのようなメソッドを持っているか、どのように振る舞うかを継承。 型・メソッド名・引数と引数の方・戻り値の型、といったシグネチャ

実装(コード)の継承:extends どのようなデータ構造を使い、どのようなアルゴリズムで処理するかを継承。 仕様を実現するための具体的なメソッド・実装の詳細、コードのことです。

  • 型(仕様)継承は『implements』(インターフェース)
  • 実装継承・型(仕様)継承は『extends』(クラス・抽象クラス)

「型」+「実装」= クラス

となります。それに対して型・仕様情報だけを切り出したのがインターフェイスです。 多重継承による問題は実装の継承に由来するものでした。

インターフェイスとは 『型(仕様)情報のみを提供するもの』 です。

なので実装するためのクラスが別に必要になります。それがいわゆる実装クラスです。インターフェイスは抽象化が行われたものなので、実装クラスのことを具象クラスとも表現できます。

本記事ではインターフェイスの実装を、型(仕様)の継承(implements)と読んでいます。

※ Java8 での default メソッド機能のサポートによって具象メソッドの実装が可能になったことに伴い、実装の継承も行えるようになりました。これについては後述します。

2-5. インターフェイスの目的と注意点

  1. 呼び出す側と実装の間にインターフェイスが介在することで結合度がさがる(疎結合
    1. 呼び出し側と呼び出される側の分離
  2. 呼び出し側から実装の詳細が隠蔽・分離される(情報隠蔽化・ポリモーフィズム
    1. 呼び出し側から見えるのは役割(仕様)だけになり実装が見えなくなる
  3. インターフェイスや抽象クラスを実装する具象クラス(インスタンス)を入れ替えることで同じ操作で応答を変化させることができる(ポリモーフィズム
  4. カプセル化されたモジュール間での通信経路(情報隠蔽カプセル化
  5. 具象依存から抽象依存へと関係性を逆転できる(依存性逆転の原則)
  6. 情報隠蔽が行えることによる開発当初の設計意図の保持(プロダクトの劣化防止)
    1. 設計思想を理解していなくてもインターフェイスによる制約によってある程度は制御できる
  7. クラス階層を無視した型の継承(implements)
  8. 型の多重継承の実現(オブジェクトに多面性を持たせる)

クラス・モジュール・システムが複数のレベル・レイヤーで結合しすぎると、変更修正の差異に多くの追加の変更を必要とします。インターフェイスまたは抽象クラスを使用すると、この副作用を防ぐことができます。

カプセル化とは

  • 設計段階においては、扱うデータと処理をひとまとめに表現(最も重要)
    • これにより、仕様変更時の影響範囲を限定させることができる
    • ビジネスロジックでアクセサメソッドを使用するとカプセル化を壊す危険性がある
  • カプセルの中で扱っているデータを隠蔽し外部からアクセスできない様にする
    • 情報隠蔽といいカプセル化を維持する。データ抽象化とは目的がことなある。
    • アクセス修飾子を用いて外部からの可視性を制御 → フィールドを private にすることが該当
  • getter / setter の使用
    • getter/setterの善悪は一律で判断できない。
    • setterはバグの温床になりうるので悪。
    • setterを根絶するにはオブジェクト生成の段階でオブジェクトとして完成させる。
    • getterは悪とされるパターンとそうでないパターンが存在する。
    • インターフェース層のためにgetterが必要となる。
    • getterによりビジネスロジックをオブジェクトの外へと流出させてしまうのは悪。

参考:現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法

getter setter プロパティを使わない

第3章で説明したように、 「データクラス」 は諸悪の根源です。 getter setter プロパティはデータクラスのための設計パターンです。 メソッドは、 何らかの判断/加工/計算をしなければいけません。 イン スタンス変数をそのまま返すだけの getter を書いてはいけません。 インスタンス変数を書き換える setter は、 プログラムの挙動を不安定 にし、バグの原因になります。 第1章で説明した 「値オブジェクト」の 設計パターンを使って、不変にするのが良い設計です。 プロパティは、getter / setter そのものです。 言語仕様としてサポート されていても使うべきではありません。

外部公開のインターフェイスAPIとも呼びます。インターフェイスさえ見ればクラス内部の実装を見なくてもクラスを扱うことができます。

ポリモーフィズムとは

あるオブジェクトを使用する側がそのオブジェクトの扱い方を変えていないのに実際の応答は異なることです。これのメリットは、使用する側のプログラムと使用される側のプログラムとが分離され、どちらかの変更があったときに片方は影響を受けなくなることです。インターフェイスや抽象クラスの具象クラスとなるインスタンスによって異なる挙動を実現できることです。噛み砕くと、「あるオブジェクトへの操作が呼び出し側ではなく呼び出された側のインスタンスによって定まる特性」です。難しいですね。保守性・可読性・拡張性を高めることができます。

先程、インターフェイスは『接点』だといいましたが、それと同時に『規格』『ルール』も表現します。インターフェイスは、具体的な処理内容は具象(実装)クラスとなるインスタンスに委ねていて、どんな振る舞いをするべきなのかだけが決められたクラスの雛形です。

例えば、

「コンセントって差し口あって、海外では3個だったりするけど日本では差し口2個だよね。ほんで、充電ケーブル刺したら電気が返ってくるよね。」

という様なルール・イメージ・お約束事を表現するのもインターフェイスの役割です。「規格」とう表現は非常に的確だと思います。この規格の元で製造されるのがコンセントです。日本では差し口二個のコンセントが、海外では差し口三個のコンセントが出来上がるわけですが、形状がなんであれコンセントに期待している振る舞いは『コード付きのプラグをコンセントに差し込む』という事前契約を果たしたら『電気を返す』という事後契約が『守られる』ことですね。

オブジェクト指向では規格と、その規格の元に作られる製品を分けています。規格を表現するのがインターフェイスです。

インターフェイスに対してプログラミングを行う事でポリモーフィズムカプセル化が実現できます。この「インターフェイスに対してプログラミングを行う」というのは、抽象メソッドに対してプログラミングを行うという意味になります。つまり、抽象メソッドを具象化することで実装の詳細をコーデイングするということです。このことを『抽象依存の原則』と呼びます(抽象クラスも同様です)。

『抽象依存の原則』は抽象化された概念を表すオブジェクトに依存するという意味です。

抽象化とは、抽象化したい複数のオブジェクトに、共通で存在する概念や性質を定義・抜き出し、それ以外のものは捨て去る、ということです。決して共通の機能を取り出すわけではありません。

呼び出し側はインターフェイスを使用するだけで期待する振る舞いが返ってきますが、抽象メソッドを具象化するインスタンスが異なればその振る舞いを実現する実装の詳細が異なります。

上記の例で言えば、抽象メソッドの実装とは、差し口の数が異なるコンセントを作るという部分です。 コンセントに期待する役割は変わりません。電気をくれるならなんでもいいわい、ということですね。

実装(コンセントの差し口の数)が変わっていても、規格 = インターフェイス(コード付きのプラグをコンセントに差したら電気を返ってくる)は変わっていないので(ポリモーフィズム)、使う側からすると実装が変わってようが意識する必要がありません(カプセル化)。

機能の差し替えは呼び出された側のインスタンスの差し替えで実現できます。異なる挙動を後から追加することも実現しやすくなります。これもポリモーフィズムです。

※ 上記で、契約うんぬん書きましたが「契約プログラミング」という概念からきてます。過去記事に書きましたので興味あればご覧ください

2-5-1. インターフェイスで行えること

下記のイラストで具体的な内容を載せています。 レイヤー間をまたいだインスタンスの生成知識を持つことで抽象依存が壊され、レイヤー間・クラス間が密結合化し、ポリモーフィズムが壊れることを表しています。

こちらは『new が抽象依存を破壊する図』もしくは『new が疎結合を破壊する図』の方が意味が解りやすいかもしれません。

①『new がポリモーフィズムを破壊する図』

「new をすること」は密結合を意味します。 new をする際にコンストラクタで初期化が行われるからです。コンストラクタの初期化に必要な引数、その引数が内部でどの様に使用されるのか?といった実装の詳細を知らなくてはいけません。この様なオブジェクトの生成知識を知ることは密結合を招きます。

②『インターフェイスポリモーフィズム実現』

③『インターフェイスを挟む図』

大雑把に言ってしまえばインターフェイスとは実装に注目せず、役割を表現するものです。呼び出す側が知りたいのは実装の詳細(コード)そのものではなく、その実装がどんな機能を実現できるのか?なのです。抽象化を用いない一般的な Class での実装では、呼び出し側と呼び出された具象クラスの関係性が非常に強い結合関係(密結合)を持ちます。呼び出した側は、そのクラスの役割と実装を知ってしまう形になります。インターフェイスは具象クラスの機能を抽象化・抽出し(具体的には仕様・型)したもので、実装はインターフェイスの具象クラスに抽出します。

プレゼンテーション層(以降P層)に属する ControllerClass がビジネスロジック層(以降BL層)に属する ServiceClass を new によって呼び出すのではなく、ServiceClassを具象クラスと(implements)する様な インターフェイスを間に置いて、P層ではそのインターフェイスを介してインスタンスを扱う様にする。これはBL層とデータアクセス層(以降DB層)でも同じ。

ただ、注意点として図ではインターフェイスはレイヤー(層)間にあるが、実際はそれぞれのレイヤーに包含される。図では分かりやすさとしてそう表現してますがイメージのしやすさを優先しているので不正確です。

これによって、P層の ControllerClass ではBL層の ServiceClass のインスタンスをあたかもインターフェイスとしてポリモーフィズムを使って扱える様になる。この様にインターフェイスを介することで呼び出す側(P層のControllerClass)は具体的にどのクラスのインスタンスが動作するかが解らなくなります。知る必要がなくなるのです。これが抽象化であり、実装との依存分離であり、情報隠蔽(呼び出す側からは実装が見えないが役割だけはわかっている)、でありポリモーフィズムと呼ばれるものです。

2-5-2. インターフェイスで可変を抽出する

オブジェクト指向

『不変を軸に、可変であろう箇所をクラスに抽出する』

ことを想定してます。

差分プログラミングは、共通部分を抽出しそれを継承することでした。オブジェクト指向は不変だけではなく可変部分の抽出も行うことができます。

以下は、先日9/3に行なったLTで作成した画像です。

不変な部分の抽出はわかりやすいです。そのまま継承の話になります。以下では重複してる箇所を不変・変更されずらいという前提で話しています。

わかり辛いのが可変の抽出部分ですね。

この様にいくつかの具象クラスから、汎用的・共通な性質をまとめる、もしくは抽象化し抽出しスーパークラスを作ることを『汎化』と言います。汎化は必ず『is-a』となります。

※これから作るクラスに共通な機能をスーパークラスにまとめることを汎化とする意味もある様です。あくまで、設計段階で行うのが汎化だとのこと。

汎化によって共通の性質(処理)がまとめられたスーパークラスからそれらを、より具体的に表現すること(実装・具象化)を『特化』といいます

※注意:ここでは簡略化のためこういった表現ですが、is-a の関係性の保持の破壊、もしくは単なる機能の使い回しであるならば、継承を用いるべきではありません。コンポジションとデリゲーションが強く推奨されます。

可変な処理が流れの何処かで存在するとこのモジュール全体で、安定性が低下します。可変な部分を不変として扱うことができればこのモジュールは安定しますね。

そんな時に使えるのがインターフェイスです(抽象クラスもなんです)。 可変な処理というのは場合によって行いたいことが変わる部分ですが、処理の中の一部分であるという俯瞰した目線でみると、期待する役割自体は同じであることが多いと思います。例えば処理の順序は同じだけど、場合によっては実装の詳細が異なるなどです。そんなときはインターフェイスなどで役割を抽象化して仕様と実装を分離してあげればいいのです。

場合によって実装の詳細が異なるものはインターフェイス(もしくは抽象クラス)の具象クラスとして個別に独立して定義してあげます。 個別に定義された具象クラスを抽象化したものがインターフェイスです。実際にはここではインターフェイスと抽象クラスによる抽象化でなくても extends による継承でも実現できますが、基本的には実装の継承は使わない方がいいと考えます。詳細には前回の第一部にて記述しております。 可変な処理を外に抽出し、場合ごとの実装も個別に定義したことによって具象クラスのインスタンスインターフェイスとして扱うポリモーフィズムが可能になりました。インターフェイスを呼び出せば可変だった実装が呼び出せる様になりましたので処理の流れも以下の様に変更されます。

これにより、処理の流れの中から可変が消えて全体として安定することができました。インターフェイスは振る舞いを定義しているだけです。役割・型・仕様・規格・ルールなどと表現できますが、抽象的なことしか定義してませんので実装を持たなくていいです。つまり安定しています。

更に機能追加を行いたい場合は、既存の具象クラスを修正変更するのではなく、新しい具象クラスを定義します。これにより処理の流れを可変と不変が入り混じっている状態から不変のみの状態に変更することができました、

また最後の画像では、仕様変更に対する対応は新しい具象クラスを追加することで対応しています。既存の具象クラスも追加された具象クラスもインターフェイス型のインスタンスとして扱うことができます。これにより仕様変更に強い設計が可能になります。このことをオープン・クロースド原則( Open/closed principle)と言います。オープンにするのは拡張(機能追加)に対して、クローズドにするのは修正(機能追加の為に既存のコード(責任)を修正する)に対してです。

この原則下のインターフェイスはその構成が不変になり、コードの修正せずとも、各要素の振る舞いを拡張することが可能になる。稼働中のソフトウェアでは、ソースコードを変更した場合、コードレビューやユニットテストなどの品質検査が必要となる。しかし、開放/閉鎖原則に沿ったソフトウェアは、既存のソースコードを変更せずに機能修正や機能追加を行うことができる。そのため、品質検査を再実行する必要がない

また、インターフェイスの具象クラスは型(仕様)の継承(implements)を行なっています。処理の流れとしてもインターフェイスに矢印の向きが集中する形に変更されました。これを依存性逆転の原則( Dependency inversion principle)とよびます。具象(実装や詳細ともいう)に依存してはいけないということです。

 

2-5-3. クラス階層を無視して無関係なクラスにインターフェイスの型継承が行える

詳細は『第4部:抽象クラスとインターフェイスの違い』の「組み合わせ爆発(combinational explosion)」の項で触れますが、

追記:こちらで記載すると共に第4部での『組み合わせ爆発(combinational explosion)』の記述を一旦中止します。私にはまだ早かった様です....すみません....

クラス・抽象クラスは Object クラスを root としたツリー階層の中に組み込まれています。こちらは『第一部:継承 1-7. 暗黙的な継承『Object クラス』にて簡単に触れています。

インターフェースは、クラス・抽象クラスの様なクラスツリーの一部として存在するのではなくクラス階層から独立しています。そのため、どの階層に位置しているクラスであろうと特定の機能(型継承)を割り込ませることができます。 継承(extends)のように、基底クラスに依存せずともインターフェースの型継承(implements)が可能です。

クラスの単一継承のため柔軟性に欠けるのに対して、インタフェースは多重継承することができ、クラスの型階層とは無関係に実装することができるため非常に柔軟です。

2-5-4. 多重継承が行える(型の継承のみ)

こちらについては

にて解説します。

2-5-5. インターフェイスの注意点

インターフェースの変更は困難

一度リリースされたpublicなインターフェースに対し、後から変更を加えることは非常に難しい。 このような問題を回避する為、正式リリース前にできるだけ多くのプログラマにこのインターフェースを使って実装してもらい、欠陥を早期に発見できるようにすると良い。インターフェースは最初から正しい設計をする必要がある。

初期にインターフェースの設計をミスると、いざ改修が必要になった時には既に手遅れになっているような場合も多く、その場合は大いなる苦しみを伴いながら作りかえを行うことになります。 インターフェースの設計がその後のシステムの保守性・堅牢性の明暗を分けると言っても過言では無いため、インターフェースを設計するときは、抽象度は本当に適切なのか?余計な情報まで受け取ったり渡したりしてしまってないか?など、慎重に考えるべき。このことは Effective Java でも語られています。

特に、public に公開されたインターフェイスAPI)は使用ユーザーの数が多いほど変更時のインパクトは大きくなります。 default メソッドのお陰で、インタフェースの機能拡張時の影響度は比較的抑えられるようになった。それでも、default メソッドによるインタフェースの機能拡張は、すべての互換性維持を保証できません。

インターフェイスAPI拡張双方の互換性維持が簡単になったとしても、設計自体は以前として慎重に行うべきです。

インターフェース分離の原則 'Interface Segregation Principle (ISP)'

太ったインターフェースを使うのではなく、高凝集なインターフェースを使うべきという原則です。クライアントが関心のあるメソッドについてのみ知る必要があるように、大きなインターフェイスをより小さく、より具体的なインターフェイスに分割します。

クライアントとなるクラスやインターフェースが複数あって、それぞれ必要なメソッドが違うのに、1つのインターフェースで実装してしまうと、関係のないメソッドの影響を受けかねないというものです。

この辺りは Fat Controller と同じ問題ですね。

引用元:Interface-Segregation Principle (ISP) - Principles of Object-Oriented Class Design

  • 「多くのクライアント固有のインターフェースは、1 つの汎用インターフェースよりも優れています」
  • 「あるクラスから別のクラスへの依存関係は、可能な限り最小のインターフェイスに依存する必要があります」
  • 「クライアント固有のきめの細かいインターフェイスを作成します。」
  • 「クライアントは、使用しないインターフェースに依存することを強いられるべきではありません。この原則は、ファット インターフェイスの欠点を扱います。ファット インターフェイスはまとまりがありません。言い換えれば、クラスのインターフェースは、メンバー関数のグループに分割する必要があります。」

 

  • “Many client specific interfaces are better than one general purpose interface“
  • “The dependency of one class to another one should depend on the smallest possible interface“
  • “Make fine grained interfaces that are client specific.“
  • “Clients should not be forced to depend upon interfaces that they don’t use. This principle deals with the disadvantages of fat interfaces. Fat interfaces are not cohesive. In other words the interfaces of classes should be broken into groups of member functions.“

依存性逆転の原則 'Dependency Inversion Principle (DIP)'

上位モジュールが下位モジュールに依存してはならない、抽象が実装に依存するのではなく実装が抽象に依存すべきという原則です。

ダメな例:

  • 上位モジュールが、必要なメソッドを下位モジュールが提供しています。
  • 下位モジュールで、たとえばシグニチャの修正が発生したとき、上位モジュールを修正しなくてはなりません。

上位モジュールとは継承関係で例えるとスーパークラスに該当し、下位モジュールとはサブクラスが該当します。

依存性を逆転させた例:

  • 上位モジュールが必要なメソッドのみを含む、抽象であるインターフェース(もしくは抽象クラス)を用意します。
  • 下位モジュールはそのインターフェース(もしくは抽象クラス)を実装します。
  • こうすると、上位モジュールがシグネチャの所有権を持つことになり、依存性が逆転します。

つまり、インターフェイスで型(仕様)を継承し具象クラスで実装を定義することでこの原則を実現できます。実装への依存がなくなり、抽象への依存が可能になります。

抽象に依存する様な設計が望ましいのです。依存しているというのは、依存対象の知識を持っているということです。依存自体が悪いのではなく問題は『何に』依存するのか?です。

具象に依存すると実際にどういう実装をしているか?という知識を依存する側が持つことになります。実際にどういう実装をしているかという知識を持つべきではないのです。具象に依存するというのは、依存する側が依存する具象クラスの実装知識を持つことを意味します。

依存対象について必要以上の知識を持たなければ、それは疎結合だといえます。逆に密結合とは互いに知識を多く共有しているということです。密結合であれば、片方の変更修正がもう片方に大きく影響します。

この様な具象に依存する密結合は、抽象依存原則に反し、ポリモーフィズムを破壊し、変更容易性を低下させてしまいます。

ちなみに、抽象化を行えるインターフェイスと抽象クラスを『抽象型』と呼びます。

ただ、Robert Cecil Martin、通称「ボブおじさん」いわく以下の様な前提があるようです。

このコラムでは、OCPとLSPの構造的な意味について説明します。これらの原則を厳密に使用することで得られる構造は、それ自体で一つの原則に一般化することができます。私はこれを「依存関係逆転原理」(DIP)と呼んでいます。

Robert C. Martin, C++ Report, May 1996

OCPは開放閉鎖原則、LSPはリスコフの置換原則(Liskov Substitution Principle)のことです。

リスコフの置換原則

引用:はらへメモ リスコフの置換原則

リスコフの置換原則

  • Liskov Substitution Principle
  • 派生型は基底型と置換可能でなければならない
    • 型の継承関係の正当性は本来の性質とは別
      • 例: Rectangle → Square
  • 設計者はユーザーの視点で 合理的な仮定 をしなければならない
    • 契約による設計 ( Design By Contract )
      • 事前条件と事後条件を取り決めておく
    • IS-A関係のあるオブジェクトは同等に振る舞うべき
      • 例: SquareはRectangleのように振る舞えない
        • Squareは縦横の長さに別々の値を持てない
  • リスコフの置換原則に違反しているか判断する経験則
    • 派生クラスで機能が退化している
      • 基底クラスでは実装されているのに、派生クラスでは何もしないメソッドなど
    • 派生クラスから例外がはかれる
      • 基底クラスのユーザーが期待しない例外を派生クラスのメソッドが投げるなど
  • リスコフの置換原則は[開放/閉鎖原則]を有効にする主要な役割を果たす原則の一つ

defaultメソッド・staticメソッドを用いた実装の多重継承

後述します。

2-5-6. インターフェースもただの道具

インターフェースや抽象クラスによる抽象化は非常に便利で強力なツールです。ですが、一つの道具で全てを賄おうとしたり何にでも適用するのはよくある間違いです。

何事においても利点と欠点、作用と副作用があり、それは相対的な立場で入れ代わっていきます。

カプセル化でデータとメソッドをひとまとめにしたり、抽象化のために複数のオブジェクトを汎化してスーパークラスに抽出するなどの利点や実現できることは記載してきました。

ですが、それがいついかなる時も有用かといえばそんなことはありません。

例えると、ショベルカーなどの重機で小さな積み木の家を作るように、かえって複雑性や難易度を上げてしまうようなこともあるかもしれません。

手段が目的化してはいけないということです。

汎化したり一つのクラスに隠蔽したりすると、処理の流れが複雑化したり人間からすると分かりづらいシステムになってしまうこともあります。

この認知的負荷の増大をしてまでそれらを行う必要がないのであれば『しない』選択も必要です。

継承でも書きましたが、あくまでトレードオフです。チームの成熟度や、プロダクトの規模や長期的に使用されるのか?そういった視点でも使用が適してるかどうかの答えは変わってくるはずです。

銀の弾丸ないので、トレードオフを理解しながら使用の検討を行うべきだと思います。

2-6. 多重継承はなぜ必要なのか?

『第1部 継承』の最後に以下の様に書きました。

1-8. Java における多重継承問題の解決策 型(仕様)継承による解決

多重継承の問題はすべて、メソッドの実装とフィールドといった実装継承に関係しているのが解ります。実装の多重継承は厄介ですが、型の多重継承には問題がないように見えます。実装の多重継承は、委譲(他のオブジェクトへの参照)で代用できるので、それほど重要ではありませんが、型(仕様)の多重継承はしばしば非常に便利で、合理的な方法で簡単に置き換えられるものではありませんでした。

そこで当時の Java 設計者は、実装(コード)には単一継承しか認めず、型(仕様)には複数継承を認めるという解決策を行いました。

それがインターフェイスです。

インターフェイスによる型のみの多重継承が、実装の多重継承に起因する複数の問題を解決します。なぜなら多重継承問題は実装の継承が原因だからです。

この辺の説明は第1部:継承「1-7. 多重継承とは?なぜ必要なのか?」でしてます。

インタフェースは、型(仕様)の継承のみを行います。型(仕様)とはシグネチャ(型・メソッド名・引数と引数の方・戻り値の型)や振る舞いのことでした。ざっくりと言えば、呼び出し側が期待する振る舞いのことです。

インターフェイスインターフェイスを implements する具象クラスに振る舞いを定義します。何ができるか?何をするか?を型として定めます。インターフェイスは実装を継承させずに型を継承するためのメカニズムです。

Java8以降では具象メソッドであるdefaultメソッドの実装が許可されましたが、それは実装の継承を行うためではないことは第一部ですでに説明しました。機能の違いだけに注目すると混乱しますが、そもそもの用途を概念レベルで理解しておけば使い分けで迷わなくなると思います。

この辺りは最後の4部にて解説したいと思います。あと余談ですが、関数型インターフェイスにおいては、default メソッド継承が非常に効果的だと見かけたことがるのですが、残念ながら資料を見つけることができませんでした。誰かご存知の方いれば教えてください。

2-6-1. 型の多重継承で何が出来るのか?

インターフェースは 振る舞い を定義します。他の言い方だと「型」「仕様」「シグネチャ」「ルール」「規格」などでしょうか。

見る人によって同じクラスでも違う振る舞いをするものに見えるという事を表現するのがインターフェースの多重継承の役割です。そのクラスに多面性を持たせる、複数の特徴を持たせるといえます。

参考:クラスArrayList

以下は ArrayList の実装クラスです。 AbstractList を継承(extends)し、RandomAccess, Cloneable, java.io.Serializable 三つのインターフェースの継承(implemnets)を行なっています。つまり、その三つのインターフェイスが表す振る舞いを持っているのが ArrayList なのです。

また以下のコードだけでは把握できませんが、上記の他以下のインタフェースも継承(implements)しています。

ちなみに階層構造的には上から

  1. Iterable インターフェイス
  2. Collection インターフェイス
  3. List インターフェイス
  4. ArrayList インターフェイス

RandomAccess, Cloneable, java.io.Serializable の三つのインターフェイスは階層構造とは関係なく継承(implements)されています。

このことはインターフェイスのメリットで挙げている以下の部分のことです。

  1. クラス階層から独立しているため、クラス階層と無関係に実装することができる
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

ArrayList は以下の5つの振る舞いを持ちます。

  1. clone メソッドの使用が許可されているオブジェクト
  2. シリアライズ実行可能なオブジェクト
  3. ランダムアクセスがサポートされたオブジェクト
    1. シーケンシャルアクセスだとパフォーマンスを保証しない
  4. foreachループ文の対象オブジェクト
    1. 拡張for文で扱えるということ
  5. 順序付けられた可変長な配列で、要素の重複を許可したオブジェクト

このような多面性が型の多重継承で実現されているため、複数あるコレクションオブジェクトの中でも ArrayList は「i番目の要素を取り出す」という処理が早いという特徴を持っています。 より正確に言うと、内部的には要素を配列で保持しているので インデックスによるアクセスは高速ですが、リストの途中に要素の挿入・削除をする場合の性能は LinkedList に比べて劣ります。

これを型の多重継承を用いずに、ArrayList の様に単一のクラスでこの多面性を実現するには、複数の具象クラスを用いるのが一般的だと思います。委譲や標準的な継承(extends)などの使用が必要になります。これらはカプセル化疎結合インターフェイスと比較して)を破壊し、変更容易な構造とは言い難いものになります。

上記と比べ型の多重継承はクラス同士の結合関係を疎結合にすることができます。 疎結合にしていくことによって、後の実装変更の影響を受けにくい構造にできるのです。

1・2・3は下記で簡単に説明していますが、4・5については下記の記事にてある程度の解説をしております。

「初心者のためのデザインパターン入門」シリーズ 第1回【Iterator −処理を繰り返す−】

RandomAccess インターフェイス

ランダム・アクセスをサポートしていることを示すマーカー・インタフェース。 このインターフェイスを継承(implements)したクラスは、ランダムアクセスされる集約オブジェクト(配列やコレクション)であることを示します(ランダムアクセスリスト)

ランダムアクセス云々は過去記事にて紹介してます。 「初心者のためのデザインパターン入門」シリーズ 第1回【Iterator −処理を繰り返す−】 ~3. for文と拡張for文の違い~

にて紹介してます。

ちなみに逆の、シーケンシャルアクセスがサポートされた集約オブジェクトは LinkedList です。

Cloneable, Serializable インターフェフィス

この二つのインターフェイスはマーカインタフェースと呼びます。 フィールドやメソッドを一切持たないインタフェースで、マーカインタフェースを実装することによって何らかの特性を持つことを示すために使います。

Cloneableインタフェースを実装しているクラスは clone可能であることを示し、Serializableインタフェースを実装しているクラスは インスタンスシリアライズ可能であることを示します。

もし型の多重継承を行わずこの多面性を実現しようと思うと 疎結合にしていくことによって、後の実装変更の影響を受けにくい構造にすることができるのがオブジェクト指向のメリットになります。

シリアライズとは

ArrayList クラスはSerializable の目印(マーカー)が付いたので、シリアライズ対象のクラスとなります。

シリアライズとは「直列化」を意味する単語でオブジェクトが持つデータを、コンピューターが読み書きできるようにバイナリデータへ変換する役割を持ちます。

このバイナリデータは、加工ができるように一列に並ぶ形で処理がされるため「直列化」と呼ばれます。

より正確には、オブジェクトの状態をStreamの状態(1バイトずつ読み書きできるバイト配列状のデータ構造)に符号化することをシリアライズ、逆にStreamの状態をオブジェクトに復号化することをデシリアライズと呼びます。

シリアライズされたオブジェクトは仮想マシン間やネットワークをまたいで転送できるようになります。 また、ファイルに保存できるようになります。

クローンとは

インスタンスの複製を作ることです。インスタンスの複製にはcloneメソッドを使うことが必要になります。そしてcloneメソッドを使うには、その複製したいクラスがCloneableインターフェースを継承していることが必要です。

cloneメソッド自体は java.lang.Object に定義されているため、すべてのクラスがcloneメソッドを持っていることになります。ArrayList クラスは Cloneable の目印(マーカー)がついたので clone 対象のクラスです。

Cloneable インタフェースを implements していないクラスが clone メソッドを呼び出すと CloneNotSupportedException がスローされます。clone メソッドは java.lang.Object において protected で宣言されてますので、 clone をしたいときは clone() メソッドを実装する必要があります。

インターフェスを実装しているかどうかでメソッドの振る舞いが変わるという特殊なインタフェースの使い方ですね。

余談ですが、マーカー・インタフェースを implement することで、そのクラスをその interface のサブクラスとし、動作を制御することができます。

マーカー・インタフェースは一定の特性を持つクラスを識別するもので、その目的は、型の実装やプログラムのパーツ間での取り決めの定義よりも、メタデータの提供に密接に関連しています。Javaでは、バージョン5でアノテーションが導入されました。メタデータの提供という意味では、アノテーションの方が優れた方法のようです。なので今となっては、Javaでマーカー・インタフェースを使う意味ほとんどない様です。

2-6-2. インターフェイスによる擬似的な多重継承では実装の継承はできない

インターフェイスによる型の多重継承のデメリットについて下記の説明を見つけました。Mix-inで実装と型の多重継承は実現できますが、複雑化するというデメリットもあります。どちらがベストという話ではないのかなと考えています。トレードオフかなと。プロダクトや設計、使用言語、現場での規約などで最適な選択は常に変動するかと

引用元:Javaの知られざる欠陥(下)

ただしインタフェースによる疑似多重継承には,致命的な欠陥がある。インタフェースでは実装を継承できない。Rubyの作成者であるまつもとゆきひろ氏は「多重継承の代わりにインタフェースを使ったのはJavaの賢いところ。ただし,実装の継承を落としたことはとても痛い」と指摘する。

二つのクラスの機能を引き継ぐ場合,Javaではどちらかのクラスのソース・コードをコピーすることになる。この結果,同じコードがさまざまな個所に分散し,保守性が極端に低下する。

同じ処理をクラスという単位にまとめて保守性を上げようとするオブジェクト指向の利点が生かせない。「マーケティング的には,Javaは大成功を収めた言語だ。しかしプログラミング言語の実装という意味では失敗作だと思う」(まつもと氏)。

ただしこちらは、骨格実装という形である程度カバーできるのかもしれません。詳細は第四部にて解説したいと思いますが以下に概要を示します。

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

  • インタフェースと抽象クラスを同時に使用
  • インタフェースの大まかな実装を抽象クラスで実装
  • その抽象クラスもインタフェースとあわせて使用する
  • インタフェースでは型を定義
  • 抽象クラスはそのインタフェースのデフォルト実装を行う
  • Javaの多くの標準ライブラリに用いられている

ex.クラス AbstractList

public abstract class AbstractList<E> extends
 AbstractCollection<E> implements List<E> {
    /**
     * Sole constructor.  (For invocation by subclass constructors, typically
     * implicit.)
     */
    protected AbstractList() {
    }

骨格実装の実現方法は2種類

  1. インターフェイスと抽象クラスの使用
  2. インタフェースで default メソッドを実装する
    1. 後述の default 実装

2-7. Java8 で default メソッドが追加された経緯

そもそもなぜ具象メソッドを定義できるようにしたのでしょうか? 以下の Oracle の公式ドキュメントに記述がありますが、わかりずらいです。

https://docs.oracle.com/javase/tutorial/java/IandI/nogrow.html

Java Magazineの2018年1月/2月号に掲載された内容が Oracle のHPで確認できたのでそちらを以下に引用しています。翻訳時に理解しずらい言い回しなど編集してます。原文原文読みたい方は引用元へどうぞ!

参考・引用元:The Evolving Nature of Java Interfaces

Java 8による新しい夜明け


これまで、Java 8で導入された新機能をあえて無視してきました。これは、Java 8が、この言語の以前の設計上の決定(「コードは単一継承のみ」など)と矛盾する機能を追加したため、いくつかの構成要素の関係を説明するのが非常に難しくなったからです。例えば、インターフェースと抽象クラスの違いと存在の正当性を論じるのは、かなり厄介なことになります。後ほど紹介しますが、Java 8のインターフェースは拡張され、より抽象クラスに近いものになりましたが、微妙な違いがあります。

この問題の説明では、まずJava 8以前の状況を説明し、次に新しいJava 8の機能を加えるというように、歴史的な経路をたどってきました。これは、現在の機能の組み合わせの正当性を理解するためには、このような歴史を踏まえて初めて可能になるからです。

もし今、JavaチームがゼロからJavaを設計し、後方互換性を壊すことが問題でないとしたら、同じように設計することはないでしょう。しかし、Java言語は理論的なものばかりではなく、実用のためのシステムなのです。そして、現実の世界では、これまでのものをすべて壊さずに、言語を進化させ、拡張する方法を見つけなければならないのです。Java 8では、デフォルトメソッドとインタフェース内の静的メソッドの2つが進化を可能にする仕組みとなっています。


インタフェースの進化


Java 8の開発で問題になったのは、インタフェースをどう進化させるかでした。Java 8では、Java言語にラムダやその他いくつかの機能が追加され、Javaライブラリにある既存のインターフェースのいくつかを適応させることが望ましいとされました。しかし、このインタフェースを使用している既存のコードをすべて壊さずにインタフェースを進化させるにはどうしたらよいでしょうか。

既存のライブラリにインターフェースMagicWandがあると想像してください。

public interface MagicWan
{
    void doMagic();
}

このインターフェイスは、すでに多くのプロジェクトで多くのクラスによって使用され、実装されています。しかし、あなたは今、本当に素晴らしい新機能を思いついたので、本当に便利な新しいメソッドを追加したいと思います。

public interface MagicWand
{
    void doMagic();
    void doAdvancedMagic();
}

そうすると、この新しいメソッドの実装を提供する必要があるため、以前このインターフェイスを実装していたすべてのクラスが壊れます。つまり、一見すると、既存のユーザーコードを壊すか(これはやりたくない)、簡単に改良する機会もなく古いライブラリに固執する運命にあるかのどちらかで、行き詰っているように見えます。(実際には、サブインターフェイスインターフェイスを拡張するなどの他のアプローチもありますが、これには独自の問題があり、ここでは触れません)。Java 8では、既存のコードを壊すことなく既存のインターフェイスに追加できるという、 両者の長所を得るための巧妙なトリックを思いつきました。これは、デフォルトメソッドと静的メソッドを使って実現されており、これから説明します。

デフォルトメソッド

デフォルトメソッドは、デフォルトの実装であるメソッド本体を持つインターフェイスのメソッドです。メソッドシグネチャの最初に default 修飾子を使って定義し、完全なメソッドボディを持ちます。

public interface MagicWand
{
    void doMagic();
    default void doAdvancedMagic()
    {
        ... // some code here
    }
}

このインターフェイスを実装するクラスは、このメソッドに対して独自の実装を行うか(オーバーライドする)、このメソッドを完全に無視するか(この場合、インターフェイスからのデフォルトの実装を受け取る)、どちらかを選択できるようになりました。古いコードは引き続き動作し、新しいコードはこの新しい機能を使うことができます。

要するに、新しい機能を追加したくなったけど現状のインターフェイスの仕様ではとんでもない問題が発生するからその問題を抑えつつ、新機能を追加するためにはどうすればいいのか?という問いの答えが「defaultメソッド」だったということですね。

新機能はどうやら Stream API のことのようです、多分。

引用元:【詳解】抽象クラスとインタフェースを使いこなそう!!

java8からはStream APIが使えるようになりました。 ※Stream APIの使い方についてはこちらを参照してください。 Stream APIはコレクションの操作(Listの操作とか)ができるようになってます。 この操作をするためにListインタフェースに型を定義(Stream APIなど外から使えるように)して、具体的な処理は実装クラスに書くことになると、継承しているクラスを全て書き換えたりとても大変なことになります。 この互換性を失わないためのものとして出来たのがdefaultメソッドです。

Listインタフェースを見てみると実際にdefaultメソッドが存在しました。

default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext())
            li.set(operator.apply(li.next()));
        }
    }

default メソッドの登場前はインタフェースを拡張して抽象メソッドを追加した場合、該当インタフェースを実装しているクラス全てに影響が伝搬するため、実装クラスは抽象メソッドの実装が必要でした。しかし、default メソッドの登場でインタフェース側でその差分を吸収することができるようになり、実装クラスに影響を与えることなく インタフェースの抽象メソッドを追加できるようになりました。

default メソッドは具象クラスで実装する必要はありません。その場合は暗黙的に default メソッドの実装内容を継承します。

2-7-1. Java8 default メソッドと多重継承問題の整合性

多重継承問題は実装の継承に依存しています。それに対する解決策がインターフェイスでした。仕様の継承のみを提供します。ところがdefault・staticメソッドによって具象メソッドを内包することができる様になり、実装の継承が可能になりました。

また、これにより抽象クラスとインターフェイスの違いが非常に曖昧になりましたが依然として中核的な違いが残っていて、抽象クラスとは全く目的が異なります。default メソッドは Java7 と Java8 の互換性のため生まれました。抽象クラスのように共通の性質をまとめるためのものではないので、抽象クラスとは全く別物です。これについては『抽象クラスとインターフェイスの使い分け』の項で説明します。

多重継承問題の解決のために登場したインターフェイスですが実装の継承が可能になってしまいました。default メソッドが追加されたインターフェイスは多重継承問題に対してどの様に解決策を提供したのでしょうか?

Java Magazineの2018年1月/2月号に掲載された内容が Oracle のHPで確認できたのでそちらを以下に引用しています。翻訳時に理解しずらい言い回しなど編集してます。原文原文読みたい方は引用元へどうぞ!

参考・引用元:The Evolving Nature of Java Interfaces

Javaの設計者が、コードの多重継承を避けるために非常に慎重に行動していたことを指摘しました。では、今はどうなっているのでしょうか。この問題に対して、Java の設計者は例によって以下のような実用的なルールを考案しています。

  1. 同じ名前の抽象メソッドを複数継承しても、それらの抽象メソッドは同じメソッドと見なされるため問題ない
  2. フィールドのダイアモンド継承は、インターフェースに static final な定数以外のフィールドを含めることができないので回避できる
  3. 静的メソッドや static final な定数の継承は、使用時にインターフェイス名を接頭辞にしなければならないので名前は衝突しない
  4. 同じシグネチャで異なる実装の複数のデフォルトメソッドを、異なるインターフェイスから継承することは問題だがコンパイラエラーが発生する

結論

インターフェイスJavaの強力な機能です。プログラムの異なる部分の間の契約の定義、動的ディスパッチのための型の定義、型の定義とその実装の分離、Javaでの多重継承の可能性など、多くの場面で役に立ちます。これらはコードの中で非常によく使われるので、その挙動をよく理解しておく必要があります。 Java 8の新しいインターフェース機能、default メソッドなどはライブラリを書くときに最も役に立ちますが、アプリケーション・コードで使われることはあまりありません。

4 の場合は インターフェイス名.super.メソッド名 を使用して明示的に指定することで呼び出すことができます。

インターフェイスを内部で使用するだけなら影響は少ないのでしょうが、外部に公開し世界中の人間が使用しているようなAPIとしてのインターフェイスの変更は困難だという良い事例ですね。インターフェイスの変更は、その変更が破壊的な変更をもたらすのか?外部に公開しているインターフェイスなのかどうか?によって全く意味が異なりますね。

外部向けAPIとしてのインターフェイスは一旦リリースしたら変更とかほぼ不可能だから、最初に正しく設計することと徹底的なテストが重要だと。Effective Java では説明されています。

また再度持ち上げますが、あれだけ実装を伴う多重継承はダメって言ってたのに、interface が実装を持てるようになり実装の多重継承は可能になりました。

がそこは、やはり書籍 Effective Java によると、使うにしても慎重になと言うことらしいです。

default メソッドは既存のインターフェイスにメソッドを追加できますが、 それをするのはとても大きなリスクが存在します。

While default methods make it possible to add methods to exsting interfaces, there is great risk to doing so. Effective Java - Item 21: Design interface for posterity

色々と総合すると、やはり基本的には「API拡張時の互換性維持」が無難なのかなと思います。

2-7-2. 追記(2022/09/25)デフォルトメソッドの用途について

この記事を公開してから 約2週間後に以下の記事を読んで考え方が変わりました。

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

まとめ

 インターフェースは、クラスの多重継承に伴うメモリのレイアウトに関する問題を解決するために導入された。技術的には、状態を持たないということが重要である。

 一方、いくつかのプログラミング言語では、インターフェースは契約のみを持っていて、実装を一切持たないものとして定義されている。これはどちらかというと思想的な判断であって、技術上は、状態さえ持たなければ、メソッドなどの実装は持てるものである。

 インターフェースが実装を持てないことによって、後からそこにメンバーを追加できないという大きな足かせが生まれている。そして、「契約のみを持つ」という明瞭さがもたらすメリットよりも、今述べた足かせというデメリットの方が開発者に与える影響が大きいと見なされるようになりつつある。そこで、近年では「状態を持てない」という最低限の制約だけを残して、インターフェースにも実装を持てるようにすることが増えている。

公開インターフェイスので互換性問題の件で、インターフェイスが具象メソッドを保持できる様になったため、抽象クラスとの使い分けや存在意義がぼやけた様に思いますが、インターフェイスが一貫して変わらないのは『状態を持たない』という点の様です。

前項での結論として「色々と総合すると、やはり基本的には「API拡張時の互換性維持」が無難なのかなと思います。」に関しては、考えを改める必要があると思いました。

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

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

2-8. 関数型インターフェイスラムダ式

参考:Java SE 8でのラムダ式とストリームを利用する新しいAPIと拡張API

デザインパターンの中には特定のオブジェ クトにデータの操作を委譲し、そのオブジェクトを他のオブジェクトに渡すことで、データと操作を分離するものがあります(strategy pattern)

このため、作成するオブジェクトは特定のクラスのデータを操作することが必要となります。 その操作をpublicにしないで、作成するオブジェクトだけが操作可能なようにカプセル化するには、オブジェクトの生成をデータの存在するクラスで行う必要があります。そのためのテクニックとして、インナークラスや匿名クラスという手法がありましたが、 Java8 からはラムダ式を使うことが出きるようになりました。

インナークラス

public なクラスの中に private なクラスが存在する。private なのでインナークラスが定義されたクラスからしか呼び出せない。

package com.jp.sss.Sample;

public class FactoryMethod {

    public static SampleInterface create() {
        return new FactoryMethod().new InnerSample();
    }

    private class InnerSample {
        public void some() {
            System.out.println("Inner Class");
        }
    }
}

// 呼び出す側(クライアント)は以下のような記述
FactoryMethod.create().some(); 

ラムダ式を取り扱う為には「関数型インターフェイス」が必要です。

関数型インターフェイス

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

近年JavaC#のように関数型言語ではない言語も含めあらゆるプログラミング言語関数型プログラミングをサポートしています。関数型言語では関数をオブジェクトとして変数に代入したりメソッドの引数や戻り値とすることができ、必要なときに関数の手続きを呼び出すことができます。

モナド、カリー化、遅延評価、高階関数など関数型言語の概念はここでは説明できないのでしません。あくまで、Javaでのラムダ(Lambda)式、関数型インターフェース(Functional Interfaces)についてだけ簡単に説明します。

Javaでは匿名クラスの様に従来から関数オブジェクトと同じような機能を実現する手段はありましたが、Java8からはラムダ式を用いた形で関数型プログラミングの枠組みを整えた様です。

2-8-1. 匿名クラス

ラムダ式を理解するには『匿名クラス』を理解する必要がります。

インナークラスも可能ですが、それをより簡略化した様な形です。匿名クラスはクラスの定義とインスタンス化を同時に行うことが可能です。匿名クラスとして生成できるのは

  • 『定義済みのクラスのサブクラス』
  • 『定義済みのインタフェースを実装したクラス』

のいずれかとなります。

あるインタフェースを実装した匿名クラスを生成する場合は抽象メソッドを全て実装します。インターフェイスによって型の情報がすでにあり、必要なのは実装の内容のみなので抽象メソッドを具象化する際に必要な、実装の詳細だけあればいいのです(オーバーライド必須)

その場でしか使わないコードの場合はコードの可読性が上がるというメリットがあります。ただし、あまり複雑な処理を持つクラスを匿名クラスにしてしまうと返って可読性を落としかねないため比較的短いコードの処理を行う場合に向きます。

インターフェイス

public interface Flyable {
    public void fly();
}

匿名クラス

// インターフェイスを実装した匿名クラスを定義
public class sample {
    // Flyable型 匿名クラスのインスタンス生成 new が書ける
    //          ↓↓ここから匿名(名前がない)クラス
    Flyable f = new Flyable() {
        @Override
        public void fly() {
            // 処理内容....
        }
    };
}

2-8-2. 関数型インターフェイスラムダ式

関数型インターフェイス

/*
 * SAM は抽象メソッドを一つしか持たせない
 */
@FunctionalInterface
public interface Flyable {
    public String fly(int V, String U);
}

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

もし独自の関数型インターフェイスとして、SAMであることを強制したいならば

  • @FunctionalInterface

を付加すれば良い。コンパイラが関数型インタフェースの条件を満たしているかどうかチェックをしてくれます。

この状態で抽象メソッドを二つ以上定義すると以下の警告文が出る

Invalid '@FunctionalInterface' annotation;
Flyable is not a functional interface

「無効 '@FunctionalInterface' アノテーションが無効です。Flyable は関数型 インターフェイスではありません」

匿名クラス

public class Factory {
    public static Flyable create() {
        // インターフェイスなのに new が書ける。インターフェイスを new するのではなく
        // インターフェイスを実装した 匿名クラスのインスタンス生成し、その参照を扱う
        // クラスの名前がないので匿名(無名)クラスと呼ぶ
        return new Flyable() {
            @Override
            public String fly(int velocity, String unitOfVelocity) {
                return "飛行速度は時速" + velocity +  unitOfVelocity + "です!";
            }
        };
    }

}
public class Main {
    public static void main(String[] args) {
        System.out.println(Factory.create().fly(200, "km")); 
    }
}

// 飛行速度は時速200kmです!

ラムダ式に変換

public class sample {
    public static void main(String[] args) {
        return (velocity, unitOfVelocity) -> 
            "飛行速度は時速" + velocity +  unitOfVelocity + "です!";
    }
}

ラムダ式ではメソッド名の指定がありません。これは関数型インタフェースはSAMであるためラムダ式で実装するメソッドが明確で指定が不要なためです。また、引数の型指定がありませんが、ラムダ式で実装するメソッドが文脈から明確であるためメソッドの定義から型推論されるからです。ただ、入れようと思えば入れれます。

ということで、ラムダ式を用いることで関数型インターフェースを実装したインスタンスを持つことができるようになります。

ちなみに抽象メソッドが1つのみという制約はありますが、defaultメソッドはカウントされません。

他にも色々と注意点や出来ることがありますが勉強不足で不正確なのでここで終わります。Stream API などでいつか深ぼりたいと思います。

2-8-3. ラムダ式のメリット

以下の動画が非常に分かりやすく参考になりました。ラムダ式の登場前の話としてのデメリットとラムダ式のメリットについての解説部分の切り抜きです。5分ほどの動画です。

参考YouTube動画:憩先生のJavaワンポイント入門講座「ラムダ式のメリット」

正直なところあまり使いこなせていないので経験と実感から語れません。 非常に分かりやす動画なのでそちらをご覧いただければと思います。

また、Effective Java ではラムダ式についても語られています。要約すると以下の様な感じです。

匿名内部クラスよりラムダ式を選ぶ 行の少ないコードであれば、ラムダ式は匿名内部クラスより読み易いことがほとんど。逆にラムダには名前とドキュメンテーションがありませんから、自明なロジックでなかったり、数行を超えたロジックなら、ラムダで実装すべきではありません。

ということでインターフェイスの話はここで一旦終了になります。 次の第三部は抽象クラスについてはなし、最後に第四部として抽象クラスとインターフェイスの使い分けを説明したいと思います。

次部 「抽象クラス」