Effective Javaを読むチャレンジ-項目18-

項目18 抽象クラスよりインタフェースを選ぶ

項目16からが本書読破の最初の壁、という気がします。要点として書いてあることはふわっと分かったつもりになれますが、細かいところの内容や言い回しや例など、どんだけ考えても理解できないものばかり。確か以前挑戦したときもこの辺りまで読んで挫折した記憶があります。なので頑張って出来るだけ丁寧に読んでいきます。

Javaは複数の実装を許す型を定義するためにインタフェースと抽象クラスの2つの仕組みを提供している

ここでいう複数の実装を許す型とは何でしょうか。自分のクラスの実装を自分以外の複数のクラスで可能にする、という感じでしょうか。寿司屋とかラーメン屋ののれん分けですね。大勝軒の名前(型)を背負った店が全国あちこちに散らばる(複数の実装を許す)みたいなものだと思ってます。

この後、インタフェースと抽象クラスの違いを説明しています。この辺は最後にまとめます。

というわけで最初の段落については最後にまとめるのでばっさりカット。ただ、Java SE 8以降はラムダ式実現のために関数型インタフェースとデフォルトメソッドが導入されています。このデフォルトメソッドとは、関数型言語の仕組みを導入したいがためにインタフェースにdefault修飾子を付けたデフォルトメソッドというメソッドの実装を許可するというJava言語仕様の変更です。元々コレクションフレームワークの拡張のための仕様変更だったんですが、これによってJavaでも実装を多重継承できるようになりました。

抽象クラスに対するこの制約は、型定義として抽象クラスを使用することを著しく妨げている

この制約というのは、抽象クラスが単一継承のみを許可しているということです。型定義というのは、そのクラスがどういう役割を持っているか、何ができるか、ということです。単一継承しかできないということは、抽象クラスはそのサブクラスに対して、自分の型定義の伝播(そして一部の実装の強制)をさせることしかできません。ミックスインのように別のクラスに型定義を付与するような使い方には向いてないということです。

既存のクラスを、新たなインタフェースを実装するように変更することは容易にできる

既存のクラスにインタフェースを実装するのは「implements」を付けて必要なメソッドを実装すればいいけど、既存のクラスに「extends」を付けて抽象クラスを継承するように変更するのは不可能ということらしいです。サブクラス化させるすべてのクラスの上位の型階層に抽象クラスが割り込む必要があり、型階層に大きな損害をもたらし、そのクラスのすべての子孫にその型の継承を拡張させることが適切かどうかに関わらず、継承を余儀なくさせるということですが・・・。

これが抽象クラスを継承するように変更するのが不可能な理由になってますかね?不適切な理由ではなく、不可能と言い切っているのがよく分かりません・・・既存のクラスが既に別のクラスを継承していたらextendsの追加は不可能ですが、それだけではない?

クラスは2つ以上の親を持つことができないし、ミックスインを入れるべき妥当な場所がそのクラス階層にない

はい、上記の答えです。既に別のクラスを継承していたら抽象クラスの追加は不可能(2つ以上の親を持てない)という意味の「不可能」のようです。

後半部分に関しては、すぐに分かりますね。2つ以上の親を持てないなら、ミックスインとなる抽象クラスを用意して追加することはできません。じゃあ、クラス階層のどこか中間にミックインさせたいクラスを挟み込むのか、というと、それはもうミックスインではなく新たな振る舞いを拡張しているだけです。

インタフェースは、階層を持たない型フレームワークを構築することを可能にする

型フレームワークという言い回しが難しいですが、要は、複数のインタフェースを組み合わせることで、階層を無視した(extendsしないから)新しい型を作れるよ、ということです。その例がシンガーとソングライターのインタフェースを合わせてシンガーソングライターというインタフェースを作るものです。

こういうやつですね。これを抽象クラスで実現すると、クラス階層の組み合わせ爆発が起きる・・・ということみたいです。

このインタフェースを抽象クラスで作ってみたいのですが、まったくコードが浮かばず、組み合わせ爆発の例がイメージできないままです。

ラッパークラスイデオムを通じて、インタフェースは、安全かつ強力な機能エンハンスを可能にする

これは項目16のラッパークラスのことを言っています。機能エンハンスとは機能拡張とか機能強化とかそういうニュアンスです。抽象クラスではそうはいかないということは項目16で説明している通りです。型の定義を抽象クラスで行うと、そのクラスはもろくなると本書では言っています。ここでいうもろさというのは、サブクラスは親クラスでの変更に弱く、影響されやすいということです。そのあたりは項目16で書かれています・・・が、項目16でも特に説明の難しい部分です。

外部に公開する重要なインタフェースごとに骨格実装クラスを付随させて提供することで、インタフェースと抽象クラスの長所を組み合わせることができる

これは外部に提供するインタフェースと付随して、そのインタフェースを実装した抽象クラスを提供するという手法です。このクラスのことを骨格実装(Skeletal Implementation)クラスと言っています。AbstractInterfaceとも呼ばれるそうです。その例はjava.util.AbstractCollectionや、java.util.AbstractMapなどです。

AbstractMapを参考に見てみます。AbstractMapはMapインタフェースを実装する骨格実装です。その抜粋です。

このようにインタフェースの実装を補助して提供することができます。インタフェースを実装するより簡単です。

インタフェースを発展させるよりは、抽象クラスを発展させる方が、はるかに容易

リリースの後でクラスにメソッドを追加する場合、抽象クラスでは抽象クラスに具象メソッドを追加すれば、そのすべてのサブクラスで追加したメソッドを使用することができます。しかしながら、インタフェースではそうはいきません。インタフェースにメソッドを追加すると、追加したメソッドを実装していないクラスはコンパイルエラーが起きます。これはとてつもなく影響が大きいです。つまり、インタフェースがリリースされて広く実装されると、そのインタフェースを変更することは不可能になります。インタフェースを選択する場合、最初の設計が非常に重要になります。その設計がほぼ永久的に変更できないかもしれないからです。

抽象クラスとインタフェースのまとめ

比較項目 抽象クラス インタフェース
実装を含めるか 含めることができる 含めることはできない(Java SE 8以降はできる)
継承範囲 サブクラスのみ継承可能 任意のクラスに実装可能
継承方法 単一継承 多重継承
既クラスを複数実装可能に できない できる
ミックスインに 向いてない 向いている
別の型を組み合わせて新しい型を作成するのに 向いてない(組み合わせ爆発を起こす) 向いている
型定義に 向いてない(もろいクラスになる) 向いている(ラッパークラスの例)
発展のしやすさ 発展しやすい 発展しにくい(実装が広まってからではほぼ不可能)

自分の経験として、コードの再利用としては継承をバンバン使う現場がほとんどでした。それについては項目16でコンポジションを使えとありましたが、実際、コンポジションってなかなか見ませんね。あと継承とインタフェースの違いというと、メンバを継承できるところですか。インターフェースにメンバを書くと暗黙的にpublic static finalになります。

こういうところからじっくり設計できる現場ってそうそうない気がしますね・・・。

広告
  • LINEで送る