読者です 読者をやめる 読者になる 読者になる

C# 使いから見てうらやましい Java8 の default 実装の使い方

Java8 から追加されるインターフェイスの default 実装ですが、C# の拡張メソッドに似てますよね。 実際、このどちらも「シンインターフェイス」を定義するだけで「リッチインターフェイス」が手に入ります。

しかし、C# の拡張メソッドと Java のインターフェイスの default 実装には、それぞれの利点と欠点があります。

拡張メソッドの利点

拡張メソッドの利点は、インターフェイスの実装者だけでなく、 インターフェイスの使用者に対してもインターフェイスの拡張が開かれている点です。 既存の型ですら、後付けでメソッドを追加することができるということです。

using System;

public static class StringExtension
{
    // インターフェイスでなくても、どんな型に対しても拡張可能
    public static int ToInt(this string self)
    {
        return int.Parse(self);
    }
}
var num = "100".ToInt();

それに対して Java8 のインターフェイスの default 実装は、 あくまでインターフェイスの提供者しか提供できません。

これは、Java8 の default 実装に対する、拡張メソッドの大きなメリットです。

default 実装の利点

では default 実装には利点がないか、というと、そうではありません。 default 実装は、リッチインターフェイスをシンインターフェイスに変換してしまうこともできるのです。

import java.util.*;

// Appendableは3つのメソッドを提供するインターフェイスだが、
// SamAppendable(SamはSingle Abstract Method)はそのうち2つをdefault実装として提供することで、
// 1つの抽象メソッドを実装すればいいようにしている
interface SamAppendable extends Appendable {
    Appendable append(CharSequence csq, int start, int end);
    default Appendable append(CharSequence csq) {
        this.append(csq, 0, csq == null ? 0 : csq.length());
        return this;
    }
    default Appendable append(char c) {
        this.append("" + c, 0, 1);
        return this;
    }
}

public class Main {
    static void appendHoge(SamAppendable a) {
        a.append("hoge");
    }
    public static void main(String[] args) {
        // (実はあまりいい例ではないのだけれど)
        // SAMインターフェイスにしたおかげで、ラムダ式が使える!
        appendHoge((csq, start, end) -> {
            System.out.printf("csq: %s, start: %d, end: %d%n", csq, start, end);
            return null;
        });
    }
}

それに対して、拡張メソッドではこのようなことは出来ません。

ジェネリックな IEnumerable は非ジェネリックな IEnumerable を継承しているため、 GetEnumerator を 2 種類実装する必要があって面倒なんですが、 「あれ、Java 8 の default 実装だとこの問題解決できるんじゃ?」 ・・・と思ったのがこのエントリを書くことになったきっかけです。 なるほど、拡張メソッドにしなかったのには理由がありそうですね。

そんなことより Scala ですよ

こんな感じで、どちらも一長一短であって優劣つけがたい感じなのですが・・・ なんと、Scala の trait はこのどちらにも対応しています。

既存の型へのメンバの追加

object Extensions {
  implicit class StringExtension(val value: String) {
    def toInteger: Int = value.toInt
  }
}

2.10 からかなり簡単に書けるようになりました。

リッチインターフェイスのシンインターフェイス化

trait SamAppendable extends Appendable {
  def append(csq: CharSequence, start: Int, end: Int): Appendable
  def append(csq: CharSequence): Appendable = {
    this.append(csq, 0, if (csq == null) 0 else csq.length)
    this
  }
  def append(c: Char): Appendable = {
    this.append("" + c, 0, 1)
    this
  }
}

Scala では Appendable を SAM インターフェイスにする意味はないですが、 リッチでファットなインターフェイスをシンインターフェイスにできるのは、 既存の Java コードとのやり取りを考えるとありがたいかもしれません。

Java8

Java8 に期待するより、今使える Scala 使おう! でも、default 実装が拡張メソッドの劣化コピーとか言っちゃダメ!