Java™ 教程(默認方法)

默認方法

接口部分描述了一個涉及計算機控制汽車製造商的例子,他們發佈了行業標準接口,描述了能夠調用哪些方法來操做他們的汽車,若是那些計算機控制的汽車製造商爲他們的汽車添加新的功能,如飛行,該怎麼辦?這些製造商須要指定新的方法,以使其餘公司(如電子制導儀器製造商)可以使其軟件適應飛行汽車,這些汽車製造商將在哪裏聲明這些與飛行有關的新方法?若是他們將它們添加到原始接口,那麼實現了這些接口的程序員將不得不重寫他們的實現,若是他們將它們做爲靜態方法添加,那麼程序員會將它們視爲實用方法,而不是必要的核心方法。html

默認方法使你可以向庫的接口添加新功能,並確保與爲這些接口的舊版本編寫的代碼的二進制兼容性。java

考慮如下接口TimeClientgit

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對象):github

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,可是,你能夠改成定義默認實現,而不是將getZonedDateTime保留爲抽象(如上例所示),請記住,抽象方法是在沒有實現的狀況下聲明的方法。segmentfault

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關鍵字,接口中的全部方法聲明(包括默認方法)都是隱式public,所以你能夠省略public修飾符。api

使用此接口,你沒必要修改類SimpleTimeClient,而且此類(以及實現TimeClient接口的任何類)將已定義方法getZonedDateTime,如下示例TestSimpleTimeClientSimpleTimeClient的實例調用方法getZonedDateTimeoracle

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());
    }
}

擴展包含默認方法的接口

擴展包含默認方法的接口時,能夠執行如下操做:ui

  • 徹底不要提到默認方法,它容許擴展接口繼承默認方法。
  • 從新聲明默認方法,使其成爲抽象方法。
  • 從新定義覆蓋它的默認方法。

假設你按以下方式擴展TimeClient接口:this

public interface AnotherTimeClient extends TimeClient { }

任何實現接口AnotherTimeClient的類都將具備默認方法TimeClient.getZonedDateTime指定的實現。

假設你按以下方式擴展TimeClient接口:

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

任何實現AbstractZoneTimeClient接口的類都必須實現getZonedDateTime方法,此方法是一個抽象方法,就像接口中的全部其餘非默認(和非靜態)方法同樣。

假設你按以下方式擴展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接口的類都將使用此接口指定的getZonedDateTime實現,而不是接口TimeClient指定的實現。

靜態方法

除了默認方法,你還能夠在接口中定義靜態方法(靜態方法是一種與定義它的類相關聯的方法,而不是與任何對象相關聯的方法,該類的每一個實例都共享其靜態方法)。這使你能夠更輕鬆地在庫中組織輔助方法,你能夠在同一個接口中保留特定於接口的靜態方法,而不是在單獨的類中。如下示例定義一個靜態方法,該方法檢索與時區標識符對應的ZoneId對象,若是沒有與給定標識符對應的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關鍵字指定接口中的方法定義是靜態方法,接口中的全部方法聲明(包括靜態方法)都是隱式public,所以你能夠省略public修飾符。

將默認方法集成到現有庫中

默認方法使你可以向現有接口添加新功能,並確保與爲這些接口的舊版本編寫的代碼的二進制兼容性,特別是,默認方法使你可以將接受lambda表達式的方法添加爲現有接口的參數,本節演示如何使用默認和靜態方法加強Comparator接口。

考慮CardDeck類,此示例將CardDeck類重寫爲接口,Card接口包含兩個枚舉類型(SuitRank)和兩個抽象方法(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接口包含操做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對List的實例進行排序,其元素類型實現了Comparable接口,成員entireDeck是List的一個實例,其元素的類型爲Card,它擴展了ComparablePlayingCard類實現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<T> list, Comparator<? super T> c)(包含Comparator參數的sort方法的版本),你能夠在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());

可是,這種方法過於冗長,若是你能夠指定你想要排序的東西,而不是你想要排序的方式會更好,假設你是編寫Comparator接口的開發人員,你能夠向Comparator接口添加哪些默認或靜態方法,以使其餘開發人員可以更輕鬆地指定排序條件?

首先,假設你想要按等級對撲克牌進行排序,而不考慮花色,你能夠按以下方式調用StandardDeck.sort方法:

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

由於接口Comparator是一個功能性接口,因此可使用lambda表達式做爲sort方法的參數,在此示例中,lambda表達式比較兩個整數值。

若是他們能夠經過僅調用Card.getRank方法建立Comparator實例,那麼對於你的開發人員來講會更簡單,特別是,若是你的開發人員能夠建立一個Comparator實例來比較任何能夠從getValuehashCode等方法返回數值的對象,那將會頗有幫助。使用此靜態方法comparing加強了Comparator接口:

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

在此示例中,你可使用方法引用:

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

此調用更好地演示了要排序的內容而不是如何進行排序。

Comparator接口已經經過靜態方法comparing的其餘版本獲得了加強,例如comparingDoublecomparingLong,它們使你可以建立比較其餘數據類型的Comparator實例。

假設你的開發人員想要建立一個能夠用多個標準比較對象的Comparator實例,例如,你如何先按等級排序撲克牌,而後按花色排序?和之前同樣,你可使用lambda表達式來指定這些排序條件:

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加強了此功能:

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

Comparator接口已使用其餘版本的默認方法thenComparing進行了加強(例如thenComparingDoublethenComparingLong),使你能夠構建比較其餘數據類型的Comparator實例。

假設你的開發人員想要建立一個Comparator實例,使其可以以相反的順序對對象集合進行排序,例如,你如何按照等級的降序排序撲克牌組,從Ace到2(而不是從2到Ace)?和之前同樣,你能夠指定另外一個lambda表達式,可是,若是你的開發人員能夠經過調用方法來反轉現有的Comparator,那麼它會更簡單,使用此功能加強了Comparator接口,默認方法reversed

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

這個例子演示了Comparator接口是如何經過默認方法、靜態方法、lambda表達式和方法引用來加強的,從而建立更具表現力的庫方法,程序員能夠經過查看調用這些方法的方式來快速推斷這些方法的功能,使用這些構造來加強庫中的接口。


上一篇:不斷髮展的接口

下一篇:繼承

相關文章
相關標籤/搜索