インターフェースに実装を持たせることができる。
インターフェース、、、ということは、継承と違って、多重継承が可能!
プリミティブな振る舞いをインターフェース化しておけば、好きなものを合成して簡単に機能性を拡張できる!
- 社員番号とメッセージを渡すと、当該社員にメールで通知してくれるというクラスを作りたい
- 社員番号から社員を取得する処理は、UserRepositoryインターフェースにお任せする
- 社員にメールでメッセージを送付する処理は、SendMailインターフェースにお任せする
- ユーザ検索機能とメール送信機能を合成する
@Test
//テスト
public void test() throws Exception {
final Integer 社員番号 = 123;
final String メッセージ = "こんにちは世界";
new SendMessage().send(社員番号, メッセージ);
// ⇒ アドレス hoge@hoge.com に、メッセージ こんにちは世界 をメール送付しました
}
/** 指定社員番号宛にメールでメッセージを送るクラス */
public class SendMessage implements UserRepository, SendMail {
public void send(final Integer empId, final String message) {
final String mailAddress = getMailAddress(empId); //デフォルトメソッドのユーザ検索機能を利用
send(mailAddress, message); //デフォルトメソッドのメール送信機能を利用
}
}
/** ユーザ情報検索インターフェース */
public interface UserRepository {
public default String getMailAddress(final Integer empId) {
//TODO ほんとはちゃんと実装すること
return "hoge@hoge.com";
}
default String getName(final Integer empId) {
//TODO ほんとはちゃんと実装すること
return "ほげたろう";
}
}
/** メール送信インターフェース */
public interface SendMail {
default void send(final String mailAddress, final String message) {
//TODO ほんとはちゃんと実装すること
System.out.println(String.format("アドレス %s に、メッセージ %s を送付しました",
mailAddress,
message));
}
}
/** SMS送信インターフェース */
// SendMessage implements UserRepository, SendSMS とすれば、メール送信をSMS送信に切り替えられる
public interface SendSMS {
default void send(final String telNo, final String message) {
//TODO ほんとはちゃんと実装すること
System.out.println(String.format("電話番号 %s に、メッセージ %s を送付しました",
telNo,
message));
}
}
StreamAPI#filterの引数型 Predicate は、and / or を使って合成することで論理演算が可能。
public static class UserCollection {
final Predicate<User> 正社員 = user -> user.userType.equals(UserType.正社員);
final Predicate<User> 役員 = user -> user.title.equals(Title.役員);
final Predicate<User> 部長 = user -> user.title.equals(Title.部長);
private final List<User> list;
public UserCollection エグゼクティブなひとたち() {
return filterBy(正社員.and(役員)); // <= Predicateの合成
}
private UserCollection filterBy(Predicate predicate){
return new UserCollection(
list.stream()
.filter(predicate)
.collect(Collectors.toList());
}
and / or は、Predicate のデフォルトメソッドとして実装されている。
@FunctionalInterface
public interface Predicate<T> {
//実装すべき一つだけのメソッド
boolean test(T t);
//デフォルトメソッド
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
シグネチャ衝突するとエラーになる。たとえば、SendMail#send と SendSMS#send を多重継承した場合など。
/** 指定社員番号宛にメールでメッセージを送る */
public class SendMessage implements UserRepository, SendMail, SendSMS {
public void send(final Integer empId, final String message) {
final String mailAddress = getMailAddress(empId);
send(mailAddress, message);
}
}
Duplicate default methods named send with the parameters (String, String) and (String, String) are inherited from the types TestDefaultMethod.SendSMS and TestDefaultMethod.SendMail
その場合、sendメソッドをオーバーライド実装するか、どちらかのメソッドをsuperで指定してあげればOK
/** 指定社員番号宛にメールでメッセージを送る */
public class SendMessage implements UserRepository, SendMail, SendSMS {
@Override
public void send(final String mailAddress, final String message) {
//どの実装を使うのか明示する
SendSMS.super.send(mailAddress, message);
}
public void send(final Integer empId, final String message) {
final String mailAddress = getMailAddress(empId);
send(mailAddress, message);
}
}
インターフェースのデフォルトメソッドにより多重継承実装が実現可能となり、菱型継承問題が顕在化するようになる。
でもとりあえず対応されているよ。ややこしいから詳細は下記参照のこと。
Javaはファーストクラスオブジェクトがクラスのため、なにかと制約が多い。
RubyのmixinやScalaのtraitのように、関数がファーストクラスオブジェクトな言語は「振る舞い」だけを抽出・合成して多重継承を実現している。
Java8もインターフェースのデフォルト実装という荒業で「振る舞い」だけを抜き出して、ファーストクラスオブジェクトがクラスな制約を解決した。
また一歩モダンな言語に近づいた!
#ちなみにScalaはtraitにインスタンス変数を定義できる(すげー)
- 多重継承できるが、状態を持てない
- 多重継承できないが、状態を持てる
様々なクラスで汎用的に呼び出される振る舞いの実装に利用する、ログとか基底処理など。もしくはライブラリの基底クラスの実装など。
ただしScalaやPHPのTraitと違ってインスタンス変数が保持できない。いままで継承で実装していたものを単純に置き換えすることはできない。できることは振る舞いのみ。