英語の勉強中です。というわけで、The Java TutorialsのDefault 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で説明されたCard
とDeck
クラスを考えます。この例はインタフェースとしてCartとDeckクラスを書き換えます。Card
インタフェースは二つのenum
(Suit
とRank
)と二つの抽象メソッド(getSuit
とgetRank
)を持ちます。
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; }
PlayingCardはCard
インタフェースを実装し、StandardDeckはDeck
インタフェースを実装します。
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(ListStandardDeck
クラスに以下のようなメソッド定義をします。
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
インスタンスを生成できれば、開発者の作業はシンプルになります。特に、getValue
やhashCode
のような数値を返す任意のオブジェクトを比較するComparator
インスタンスを開発者が生成するのに役立ちます。Comparator
インタフェースはcomparing static メソッドでこの機能拡張を行っています。
myDeck.sort(Comparator.comparing((card) -> card.getRank()));
この例では、代わりにMethod Referencesも使用可能です。
myDeck.sort(Comparator.comparing(Card::getRank()));
この呼び出し方は、方法よりも要求*2の例を良く表現しています。
Comparator
インタフェースはcomparingDoubleとcomparingLongのような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のバリエーション(thenComparingDoubleとthenComparingLongなど)も機能拡張しており、そのデータ型で比較する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で拡張されているかを解説しました。ライブラリでインタフェースを拡張するためにこれらの機能を使用してください。