Java8のインタフェース実装から多重継承とMixinを考える
2014年3月18日、ついにJava8が正式にリリースを迎えた。
折角なので、今後、Java8の新機能に関する記事をいくつかアップしていきたい。
Java8といえばやはりラムダ式だけど、既に色々な方がブログ等々で紹介しているので、今回は、Java8にて導入されたインタフェースのデフォルト実装と多重継承、Mix-inの関係について書いていきたいと思う。
かなり長丁場になりそうだが、最後までお付き合いいただければ幸いである。
では、Java8の世界に飛びだそう!
Java8のインタフェース実装とは?
Java8では、インタフェースに実装を持てるようになった。
定義の方法は「default」句、または「static」句を利用する2パターンが存在する。
今回の記事では「default」句を利用した場合のメソッドの振る舞いから多重継承とmix-inについて考えていく。
default句を使用したインタフェースのデフォルト実装
では実装サンプルを見ていこう。
public interface GreetingInterface { default String sayHello() { // default句を使う事で、インタフェース内にメソッドの実装を持てる return "Hello Java8!"; } } class GreetingInterfaceImpl implements GreetingInterface { } class Main { public static void main(String[] args) { System.out.println(new GreetingInterfaceImpl().sayHello()); // => Hello Java8! } }
GreetingInterface#sayHelloにデフォルトの処理が定義できていることが分かる。
GreetingInterfaceImplクラスではsayHelloをオーバーライドしていないがコンパイルエラーとはならず、デフォルト実装が呼び出される訳だ。
なぜデフォルトの実装を定義できるようになったのか?
既存のインタフェースにメソッドを追加したいという点がポイントだ。
例えばJava8のjava.util.Collectionインタフェースを見ると、新たにremoveIfといったメソッドが追加されている。このメソッドはラムダによる呼び出しが可能となっており、Javaの可能性を広げるメソッドの一つだ。
ただし、インタフェースにメソッドを追加するということは、すべての実装クラスに追加したメソッドのオーバーライドを強制することになる。
これは互換性の消失を意味し、Java7まで動いたコードがJava8で動かなくなるといった事態を招く。インタフェース上のメソッドにデフォルト実装を持たせ、オーバーライドを任意とすることによってこの互換性の問題を回避したという訳だ。
この仕様追加によって多重継承の問題は発生しないのか?
ここでポイントになるのは、Javaでは複数のインタフェースをimplementsできるという点だ。
インタフェースに実装を持てるようになったことで、実装の多重継承が可能となった。
多重継承は様々な問題を孕んでおり、採用されていない言語も多い。
これらの問題はJava8では発生しないのだろうか。また発生しない場合、どのように回避されているのだろうか。
この問題を理解するために先ず、継承と多重継承の意味や問題点について考えていこう。
継承と多重継承について考える
継承の意味
継承とは子クラスが親クラスの”特性”を引き継ぐ事だ。
この特性とは「クラスの仕様(型としての情報)」と「実装」を指す。
子クラスが親クラスの仕様を引き継ぐことで、外部のクラスは子クラスを親クラスと同一の”型”として扱える。また、子クラスは親クラスの実装を引き継ぐことで、その実装を再利用できるのだ。
Javaではこの”仕様”と”実装”の継承をextendsキーワードを使って表現する。
多重継承とは
複数のクラスの特性を継承できる仕組みを指す。継承する特性は”仕様のみ”でも構わないし、”仕様と実装”でも構わない。
親クラスを複数持つことができるため、様々なクラスを組み合わせて処理を実現することができ、使い方によっては非常に有用な仕組みとなるはずだ。
ただし、多重継承はその利便性の反面、様々な問題を孕んでおり、その問題への取り組み方は言語によって異なる。この問題にについて考えてみよう。
多重継承における問題点 〜菱形継承問題とは〜
多重継承の実現を考えた場合に遭遇する問題である。まず菱形継承から解説する。
菱形継承とは?
以下のような継承関係を指す。この継承関係はダイアモンド継承とも呼ばれる。
親クラスを継承する子クラスAと子クラスBがおり、孫クラスが子クラスA、B両方を継承するというパターンだ。継承関係が菱形のようになるから、菱形継承と呼ばれる。
どういった問題が発生するか
菱形継承では以下のような問題が発生する。
- メソッド名の重複による名前解決の問題
- 同一クラスを複数回継承してしまう問題
それぞれの問題を確認していこう。
問題1.メソッド名重複による名前解決の問題
親クラスが同名のメソッドを保持している場合に発生する。
先の菱形継承の例で言うと、子クラスAと子クラスBは両方ともgreetメソッドを保持している。
孫クラス側でこのgreetメソッドを呼び出した場合、子クラスA、子クラスBどちらのメソッドを呼べば良いだろうか。
子クラスAは”おはよう”と挨拶し、子クラスBは”こんばんは”と挨拶するかもしれない。
呼び出し先によって振る舞いが変わる可能性があるため、どちらを呼び出すかコードの実行系統で勝手に判断することはできない。
問題2.同一クラスを複数回継承してしまう問題
先の菱形継承の例で孫クラスから親クラスを見た場合、親クラスは「子クラスA越し見える親クラス」と「子クラスB越しに見える親クラス」の2種類が存在する。これは、継承ツリー上、親クラスは1つであっても、親クラスのインスタンスは複数(子クラスA経由で作ったインスタンスと子クラスB経由で作ったインスタンス)存在するということだ。
孫クラスから親クラスの実装を呼び出した場合、子クラスAと子クラスB、どちらを経由して辿り着くインスタンスを利用すればいいだろうか?
菱形継承問題は”実装の継承”によって発生する
先述の「継承の意味」にて、継承とは親クラスの「仕様」と「実装」を引き継ぐ事だと述べた。
上述の菱形継承問題は「実装」の継承を行った場合に発生する。
逆に言うと「仕様」のみ継承した場合はこの問題は起き得ないのだ。
「仕様」というのは”クラスが持っている操作(メソッド)は何か”という情報である。
複数の親から仕様を継承したとしても、それは操作を持っているという事実だけであって、実際の処理は子クラス側で定義する訳だから、上記のような問題は発生しない。
故にこれまでのJavaでは実装を持つクラスの多重継承を認めず、仕様のみ保持するインタフェースの多重継承(implements)のみ認めることで、多重継承にまつわる問題の発生を回避していたのだ。
Javaにおける多重継承の取り扱い
では、Javaではどのようにして多重継承を取り扱ってきたか見てみよう。
Java7までの多重継承の扱い
Javaではextendsキーワードに指定できるクラスは1つのみであり、”仕様”と”実装”両方を引き継ぐ多重継承は禁止されている。
ただし、複数のインタフェースをimplementsすることで、”仕様のみ”に絞ったの多重継承を認めている。先述の通り、”仕様のみ”の多重継承では菱形継承問題は発生しない。
Java8からの多重継承の扱い
では、Java8ではどうだろうか。
Java8ではインタフェースに実装を持てるようになったことで、図はこのように変わる。
インタフェースが実装を持てるようになったことで、従来のJavaでは制限していた”実装”の多重継承を実現できるようになった。
これはクラスや抽象クラスと同様に実装の継承も行うこととなり、上記の菱形継承の問題が発生する可能性がある。
Java8で発生する菱形継承
Javaでは複数のインタフェースをimplementsできるため、以下のような継承関係を作ることができる。
インタフェースも実装を持てる訳だから、実装クラスでは”仕様”と”実装”の継承を両方とも実現している。これは菱形継承の問題が発生しないのだろうか。
結論を先に書くと、Java8では名前解決ルールの導入とインタフェースの仕様を利用して菱形継承問題を回避している。
先に述べた菱形継承の問題別に解決方法を見ていこう。
「問題1.メソッド名の重複による名前解決問題」の解決
以下の2つのルールを設けることによって解決している。
ルール1.メソッドの呼出順に優先順位を付ける
ルール2.メソッドの呼出優先順位が判断できない、または同一となった場合、コンパイルエラーとしてオーバーライドを促す
順番に見ていこう。
メソッドの呼出順に優先順位を付ける
Java8ではメソッド名の重複が発生した場合、どちらのメソッドを呼び出すべきか、優先順位を付けて判断する。
優先順位は継承の深さを優先して探索される。つまり”メソッドの呼び出しクラスに最も近い位置にいるメソッドが呼ばれる”ということだ。
public interface ParentInterface { default String greet() { return "Hello, World!"; } } interface ChildInterfaceA extends ParentInterface { @Override default String greet() { return "Hello, Japan!"; } } interface ChildInterfaceB extends ParentInterface { // ChildInterfaceBではgreetをオーバーライドしない } class InterfaceImpl implements ChildInterfaceA, ChildInterfaceB { } class Main { public static void main(String[] args) { System.out.println(new InterfaceImpl().greet()); // => Hello, Japan! } }
ParnetInterface#greetメソッドをChildInterfaceAでのみオーバーライドしている。
この場合、InterfaceImplに最も近いgreetメソッドはChildInterfaceA#greet()であるから、ChildInterfaceA側のメソッドが呼ばれた。
こういった優先度付けはPythonなどでも採用されており、C3線形化と呼ばれるアルゴリズムを採用している。
Javaでも同様の実装となっているかは現時点では不明である。
メソッドの呼出優先順位が判断できない、または同一となった場合、コンパイルエラーとしてオーバーライドを促す
先の実装例で、ChildInterfaceBにもgreetメソッドを定義したらどうなるだろうか。
この場合は「クラスInterfaceImplは型ChildInterfaceAとChildInterfaceBからgreet()の関連しないメソッドを継承します。」というコンパイルエラーが発生する。
要は「どちらを呼べばいいか判断できないからメソッドをオーバーライドしろ」と言われるのだ。
以下のようにオーバーライドできる。
public interface ParentInterface { default String greet() { return "Hello, World!"; } } interface ChildInterfaceA extends ParentInterface { @Override default String greet() { return "Hello, Japan!"; } } interface ChildInterfaceB extends ParentInterface { @Override default String greet() { return "Hello, America!"; } } class InterfaceImpl implements ChildInterfaceA, ChildInterfaceB { @Override public String greet() { // Super句を使って呼びたい方を呼ぶ。独自の実装を持たせても構わない return ChildInterfaceA.super.greet(); } } class Main{ public static void main(String[] args) { System.out.println(new InterfaceImpl().greet()); // => Hello, Japan! } }
以上、2つのルールの採用によって、問題1は解決することができた。
「問題2.同一クラスを複数回継承してしまう問題」の解決
Javaのインタフェースはインスタンスを作ることができないからこの問題は発生しない。
これを状態を持つ/持たないとして表すと、先のJava8での多重継承の図はこうなる。
つまり、インタフェースのデフォルト実装と、クラスや抽象クラスで実現できる実装は完全に同一ではないということだ。
状態を持つ/持たないとはどういうことか
インスタンス変数を持てるかどうかの違いだ。
インタフェース上に定義した変数は自動的にpublic static finalな変数となる。
これは、インタフェース上のメソッドからはインスタンス変数を排除できることを意味する。
つまり、親クラスへ至る継承ルートが何パターン有ろうが、どの継承ルートを辿ったとしても行き着く先は同じ(インスタンスではない)であり、親の振る舞いは変わらないということだ。
インタフェースのデフォルト実装とstaticメソッドの違い
結局のところ、インタフェースのデフォルト実装は状態を持たない⇒つまりstaticメソッドと同じだと考えた方も居るだろう。
だが、これらはメソッドをオーバーライドし、多態性を持たせることができるか否かという点で異なる。
Javaの言語仕様ではstaticを付けたメソッドは子クラス側でオーバーライドできない。これはメソッドの情報がインスタンスではなくクラス(型)に紐付けられるからだ。
しかし、Java8のインタフェースデフォルト実装では、”状態を持たない”というstaticメソッドの特性を引き継ぎつつ、子クラス側でオーバーライドし、ポリモフィズムを実現することができるのだ。
まとめ – Java8のインタフェース実装はJava版Mixinか?
Java8ではインタフェースに実装を持てるようになり、インタフェースをimplemetsしたクラスで実装を利用できるようになった。
また、implementsは複数のインタフェースを対象にできるため、様々なインタフェースに定義された仕様や実装をクラスに取り込む事ができるようになった。これはMixinの考え方と同じだ。
投稿日時点のWikipediaではMixinについて以下のように解説されている。
mixin とはオブジェクト指向プログラミング言語において、サブクラスによって継承されることにより機能を提供し、単体で動作することを意図しないクラスである。
mixin からの継承は、特化の一形態ではなく、むしろ機能を他のクラスから集めるための手段である。あるクラスは多重継承により複数の mixin クラスから継承を行って、大半の機能を継承によって実現することができる。
Mixinとは単独で動作することを意図しないコード(再利用したいコード)を予め定義しておき、必要に応じてクラスに混ぜ込む(Javaで言うとimplementsする)ことによって、処理の再利用を促す仕組みの事である。
従来のJavaでは実装を多重継承できないという点から継承よりも委譲によるコードの再利用が発達してきた経緯がある。
今回のJavaによるMixin実現によって、これまでよりも気軽に実装の継承を活用することができるようになるかもしれない。
関連記事
-
-
文字コードの考え方から理解するUnicodeとUTF-8の違い
UnicodeとUTF-8の違いを理解していない方が結構居るようなので、文字コードの考え方を元に解説
-
-
Linuxプロセス起動時の環境変数ダンプの取得
UnixやLinux上で不具合の調査等々を行う際、特定のプロセス起動時の環境変数を知りたい場合がある
-
-
Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する
しばらくLinuxネタが続く・・。 近いうちに最近出たJava8ネタを書いてみようと思います。が、
-
-
sshd再起動時にssh接続が継続する動作について
Linux/Unixサーバにsshしている際、sshdを再起動したとする。 sshdは一度終了する
-
-
例示専用のIPアドレスとドメインを使いこなす
前回の記事ではネットワークに関する記事を投稿させていただいたが、今回も引き続きネットワーク関連のネタ
-
-
「Systemd」を理解する ーシステム管理編ー
前回の記事「Systemd」を理解するーシステム起動編ーでは、Systemdの概念とSystemdに
-
-
「Systemd」を理解する ーシステム起動編ー
2014年6月10日、とうとうRHEL7が正式リリースを迎えた。RHEL7での変更点については、この
-
-
ipsetを使ってスマートにiptablesを設定する
ギークな知人から「vpsでiptables設定していたらルール設定数の上限に引っかかって思い通りの設
-
-
Java8のHotSpotVMからPermanent領域が消えた理由とその影響
今回も前回の記事につづき、Java8による変更点で未だあまり紹介されていないポイントを記事にしようと
Comment
l6y2r8
pueh5w
fe1c5f
1nhbn4
1ad2dy
oqofiy
pprqj2
w8ubq5
a2ythb
s5cqzb
3ont2b
iilut1
xr2jsb
8yzlcb
2m6fd9
lamhw9
3d5yar
u2peri
i7b0em
i65nj9
lh9s3r
k0juj0
bkgkj0
l4dryo
96a7wt
h92014
bzt82z
m7lh6b