kagamihogeの日記

kagamihogeの日記です。

The Java TutorialsのDefault Methodsのところをテキトーに訳した

英語の勉強中です。というわけで、The Java TutorialsDefault Methodsセクションをテキトーに訳しました。

Default Methods

Interfacesセクションでは、自動車操作のメソッドを記述する業界標準インタフェースを公開している無人自動車製造企業などの例を説明しています。もし、それらの無人自動車製造企業が飛行機能のような新機能を彼らの車に追加したい場合、どうすればよいでしょうか? こうした企業は新しいメソッドをパートナー企業(電子誘導装置製造業者のような)が飛行自動車にソフトウェアを適合できる形で定義したいでしょう。自動車製造企業は飛行関連メソッドをどこに定義すればよいのでしょうか? もしオリジナルのインタフェースに新しいメソッドを追加する場合、そのインタフェースを実装するであろうプログラマはその実装を上書きしなければなりません。staticメソッドとして追加するのであれば、プログラマはユーティリティーメソッドとして扱い、必須でないコアメソッドとして見なすでしょう。

Default methodsはライブラリのインタフェースに新機能を追加させることを可能にし、インターフェースの古いバージョンを基に書かれたコードとのバイナリ互換性を保障します。

Answers to Questions and Exercises: Interfacesで説明された、TimeClientインタフェースを例に取ります。

import java.time.*; 
 
public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
}

以下のSimpleTimeClientクラスはTimeClientを実装しています。

package defaultmethods;

import java.time.*;
import java.lang.*;
import java.util.*;

public class SimpleTimeClient implements TimeClient {
    
    private LocalDateTime dateAndTime;
    
    public SimpleTimeClient() {
        dateAndTime = LocalDateTime.now();
    }
    
    public void setTime(int hour, int minute, int second) {
        LocalDate currentDate = LocalDate.from(dateAndTime);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(currentDate, timeToSet);
    }
    
    public void setDate(int day, int month, int year) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime currentTime = LocalTime.from(dateAndTime);
        dateAndTime = LocalDateTime.of(dateToSet, currentTime);
    }
    
    public void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime timeToSet = LocalTime.of(hour, minute, second); 
        dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
    }
    
    public LocalDateTime getLocalDateTime() {
        return dateAndTime;
    }
    
    public String toString() {
        return dateAndTime.toString();
    }
    
    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println(myTimeClient.toString());
    }
}

TimeClientインタフェースに新機能を追加したいと仮定します。新しい機能は、ZonedDateTimeオブジェクト(このオブジェクトはタイムゾーン情報を保持すること以外はLocalDateTimeと同様です)を通してタイムゾーンを指定するための機能とします。

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();                           
    ZonedDateTime getZonedDateTime(String zoneString);
}

TimeClientインターフェースの修正に続いて、SimpleTimeClientを修正してgetZonedDateTimeメソッドを実装する必要があるでしょう。しかし、abstractとしてgetZonedDateTime(以前の例のように)を回避するよりも、代わりにdefault implementationを定義できます。(実装無しで定義されるメソッドabstract methodであることを思い出して下さい)

package defaultmethods;
 
import java.time.*;

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
    
    static ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }
        
    default ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }
}

メソッドシグネチャの最初をdefaultキーワードにするとdefault methodを定義できます。default methodsを含むインタフェースのすべてのメソッド定義は暗黙的にpublicとなるため、public修飾子を省略可能です。

このインタフェースにより、SimpleTimeClientクラスを修正しなくてもよくなり、このクラス(とTimeClientインタフェースを実装するすべてのクラス)はgetZonedDateTimeメソッドが既に定義されていたことになります。以下のTestSimpleTimeClientの例では、SimpleTimeClientインスタンスを用いてgetZonedDateTimeメソッドを呼び出しています。

package defaultmethods;
 
import java.time.*;
import java.lang.*;
import java.util.*;

public class TestSimpleTimeClient {
    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println("Current time: " + myTimeClient.toString());
        System.out.println("Time in California: " +
            myTimeClient.getZonedDateTime("Blah blah").toString());
    }
}

Extending Interfaces That Contain Default Methods

default methodを含むインタフェースを拡張する際には、以下に従う必要があります。

  • デフォルトメソッドは何もしないと、拡張されるインターフェースにデフォルトメソッドを継承させます。
  • default methodを再宣言(Redeclare)するには、abstractにします。
  • default methodを再定義(Redefine)するには、オーバーライドします。

TimeClientインタフェースを以下のように拡張すると仮定します。

public interface AnotherTimeClient extends TimeClient { }

AnotherTimeClientインタフェースを実装するすべてのクラスはdefault method TimeClient.getZonedDateTimeで定義される実装を持ちます。

TimeClientインタフェースを以下のように拡張すると仮定します。

public interface AbstractZoneTimeClient extends TimeClient {
    public ZonedDateTime getZonedDateTime(String zoneString);
}

AbstractZoneTimeClientインタフェースを実装するすべてのクラスはgetZonedDateTimeメソッドを実装する必要があります。このメソッドはインタフェースのその他のすべての非default metod(かつ非static)のようなabstractメソッドです。

TimeClientインタフェースを以下のように拡張すると仮定します。

public interface HandleInvalidTimeZoneClient extends TimeClient {
    default public ZonedDateTime getZonedDateTime(String zoneString) {
        try {
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.of(zoneString)); 
        } catch (DateTimeException e) {
            System.err.println("Invalid zone ID: " + zoneString +
                "; using the default time zone instead.");
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.systemDefault());
        }
    }
}

HandleInvalidTimeZoneClientインタフェースを実装するすべてのクラスはTimeClientインタフェースで指定されるgetZonedDateTimeの代わりにこのインタフェースで定義されるgetZonedDateTimeの実装を使用します。

Static Methods

default methodsに加えて、インタフェースにstatic metohdを定義できます。(staticメソッドとは、オブジェクトではなくそれが定義されるクラスに関連付いたメソッドことです。クラスの全インスタンスはstaticメソッドを共有します。)これはライブラリにヘルパークラス入れ込むために使用できます。個々のクラスではなく同一インタフェースにstaticメソッドを持たせられます。

以下の例は、タイムゾーン識別子に相当するZoneIdオブジェクトを検索するstaticメソッドを定義しています。もし与えられた識別子に相当するZoneIdオブジェクトが無い場合はシステムのデフォルトタイムゾーンを使用します。(結果として、getZonedDateTimeを単純化できます。)

public interface TimeClient {
    // ...
    static public ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    default public ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }    
}

クラスのstaticメソッド同様、インタフェースのメソッド定義はメソッドシグネチャの開始をstaticキーワードにすることでstaticメソッドになります。staticメソッドを含む、インタフェースの全メソッド宣言は、暗黙的にpublicとなるため、public修飾子は省略可能です。

Integrating Default Methods into Existing Libraries

default methodsは既存のインタフェースに新機能を追加でき、そのインタフェースの古いバージョンを基に書かれたコードとのバイナリ互換性を保障します。特に、default methodsは既存インタフェースに引数としてラムダ式を取るメソッドを追加できます。このセクションでは、Comparatorインタフェースがどのようにdefault methodとstaticメソッドで機能強化されるかを説明します。

Questions and Exercises: Classesで説明されたCardDeckクラスを考えます。この例はインタフェースとしてCartDeckクラスを書き換えます。Cardインタフェースは二つのenumSuitRank)と二つの抽象メソッドgetSuitgetRank)を持ちます。

package defaultmethods;

public interface Card extends Comparable<Card> {
    
    public enum Suit { 
        DIAMONDS (1, "Diamonds"), 
        CLUBS    (2, "Clubs"   ), 
        HEARTS   (3, "Hearts"  ), 
        SPADES   (4, "Spades"  );
        
        private final int value;
        private final String text;
        Suit(int value, String text) {
            this.value = value;
            this.text = text;
        }
        public int value() {return value;}
        public String text() {return text;}
    }
    
    public enum Rank { 
        DEUCE  (2 , "Two"  ),
        THREE  (3 , "Three"), 
        FOUR   (4 , "Four" ), 
        FIVE   (5 , "Five" ), 
        SIX    (6 , "Six"  ), 
        SEVEN  (7 , "Seven"),
        EIGHT  (8 , "Eight"), 
        NINE   (9 , "Nine" ), 
        TEN    (10, "Ten"  ), 
        JACK   (11, "Jack" ),
        QUEEN  (12, "Queen"), 
        KING   (13, "King" ),
        ACE    (14, "Ace"  );
        private final int value;
        private final String text;
        Rank(int value, String text) {
            this.value = value;
            this.text = text;
        }
        public int value() {return value;}
        public String text() {return text;}
    }
    
    public Card.Suit getSuit();
    public Card.Rank getRank();
}

Deckインタフェースはカードデッキを操作する様々なメソッドを持ちます。

package defaultmethods; 
 
import java.util.*;
import java.util.stream.*;
import java.lang.*;
 
public interface Deck {
    
    List<Card> getCards();
    Deck deckFactory();
    int size();
    void addCard(Card card);
    void addCards(List<Card> cards);
    void addDeck(Deck deck);
    void shuffle();
    void sort();
    void sort(Comparator<Card> c);
    String deckToString();

    Map<Integer, Deck> deal(int players, int numberOfCards)
        throws IllegalArgumentException;

}

PlayingCardCardインタフェースを実装し、StandardDeckDeckインタフェースを実装します。

StandardDeckクラスはDeck.sort抽象メソッドを以下のように実装します。

public class StandardDeck implements Deck {
    
    private List<Card> entireDeck;
    
    // ...
    
    public void sort() {
        Collections.sort(entireDeck);
    }
    
    // ...
}

Collections.sortメソッドComparableインターフェースを実装する要素型を含むListインスタンスをソートします。ListインスタンスであるentireDeckメンバ変数の要素はCard型で、これはComparableを拡張しています。PlayingCardは以下のようにComparable.compareToメソッドを実装しています。

public int hashCode() {
    return ((suit.value()-1)*13)+rank.value();
}

public int compareTo(Card o) {
    return this.hashCode() - o.hashCode();
}

このcompareToメソッドは、カードデッキをソートするためにStandardDeck.sort()メソッドを呼び出すと、スーツ・ランク順に並び替えられます。

デッキをランク・スーツ順に並べ替えるにはどうすればよいでしょうか? 新しいソート条件を定義するためにComparatorインタフェースを実装する必要があり、それからsort(List list, Comparator<? super T> c)メソッドを使用します。StandardDeckクラスに以下のようなメソッド定義をします。

public void sort(Comparator<Card> c) {
    Collections.sort(entireDeck, c);
} 

このメソッドにより、どのようにCollections.sortメソッドCardクラスのインスタンスをソートするのかを定義可能です。その定義方法の一つは、カードのソート方法を定義するためにComparatorインタフェースを実装することです。SortByRankThenSuitがその例です。

package defaultmethods;

import java.util.*;
import java.util.stream.*;
import java.lang.*;

public class SortByRankThenSuit implements Comparator<Card> {
    public int compare(Card firstCard, Card secondCard) {
        int compVal =
            firstCard.getRank().value() - secondCard.getRank().value();
        if (compVal != 0)
            return compVal;
        else
            return firstCard.getSuit().value() - secondCard.getSuit().value(); 
    }
}

以下のソート呼び出しはカードデッキをランク・スーツの順にします。

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(new SortByRankThenSuit());

しかしながら、このアプローチは冗長です。ソートの方法ではなくソートの要求*1を定義できればベターです。いま、Comparatorインタフェースを作成するところだと仮定します。他の開発者がより簡単にソート条件を定義できるComparatorインタフェースを加えるために、defaultもしくはstatic methodsが出来ることとは何でしょうか?

まず、スーツに関わらずランクでカードデッキをソートしたい、と仮定します。StandardDeck.sortメソッドを以下のように呼び出すことが出来ます。

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
    (firstCard, secondCard) ->
        firstCard.getRank().value() - secondCard.getRank().value()
);

Comparatorインタフェースはfunctional interfaceなので、sortメソッドの引数にラムダ式が使用できます。この例では、ラムダ式は二つのinteger値を比較します。

Card.getRankメソッドのみを呼び出すComparatorインスタンスを生成できれば、開発者の作業はシンプルになります。特に、getValuehashCodeのような数値を返す任意のオブジェクトを比較するComparatorインスタンスを開発者が生成するのに役立ちます。Comparatorインタフェースはcomparing static メソッドでこの機能拡張を行っています。

myDeck.sort(Comparator.comparing((card) -> card.getRank())); 

この例では、代わりにMethod Referencesも使用可能です。

myDeck.sort(Comparator.comparing(Card::getRank()));

この呼び出し方は、方法よりも要求*2の例を良く表現しています。

ComparatorインタフェースはcomparingDoublecomparingLongのようなcomparing staticメソッドのバリエーションも拡張しており、そのデータ型で比較するComparatorインスタンスを生成できます。

いま、開発者は一つ以上の条件でオブジェクトを比較するComparatorインスタンスを生成しようとしている、と仮定します。たとえば、ランク・スーツ順にカードデッキをソートするにはどうすればよいでしょうか? 以前のように、ソート条件を指定するためにラムダ式を使用可能です。

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
    (firstCard, secondCard) -> {
        int compare =
            firstCard.getRank().value() - secondCard.getRank().value();
        if (compare != 0)
            return compare;
        else
            return firstCard.getSuit().value() - secondCard.getSuit().value();
    }      
); 

これを単純化するにはComparatorインスタンスからComparatorインスタンスを構築できれば良いことになります。ComparatorインタフェースはthenComparing default methodによってこの機能拡張を行っています。

myDeck.sort(
    Comparator
        .comparing(Card::getRank)
        .thenComparing(Comparator.comparing(Card::getSuit)));

ComparatorインタフェースはthenComparing default methodのバリエーション(thenComparingDoublethenComparingLongなど)も機能拡張しており、そのデータ型で比較するComparatorインスタンスを組み立てられます。

開発者がコレクションを逆順でソートするComparatorインスタンスを生成しようとしている、と仮定します。たとえば、ランクは逆順・Aceから2でカードデッキをソートするにはどのようにすればよいでしょうか? 従来どおりのやり方で、ラムダ式を指定できます。しかしながら、メソッド呼び出しで既存のComparatorを逆順にできば、開発者の作業は軽減されます。Comparatorインタフェースはreversed default methosによってこの機能拡張を行っています。

myDeck.sort(
    Comparator.comparing(Card::getRank)
        .reversed()
        .thenComparing(Comparator.comparing(Card::getSuit)));

この例では、プログラマメソッドの呼び出され方を調べる負荷を軽減する機能を持つライブラリメソッドを作成するために、どのようにComparatorインタフェースがdefault methods、staticメソッドラムダ式、method referencesで拡張されているかを解説しました。ライブラリでインタフェースを拡張するためにこれらの機能を使用してください。

*1:原文はwhat you want to sort, not how you want to sort. ソートを手続き的というより宣言的に、くらいに意訳しても良いだろうか?

*2:原文ではwhat to sort rather than how to do it