实现一个接口

定义Relatable接口

要声明实现接口的类,需在类声明中包含一个 implements 子句。一个类可以实现多个接口,因此 implements 关键字后需跟随由逗号分隔的该类所实现接口的列表。按惯例,如果存在 extends 子句,则 implements 子句应紧随其后。

考虑一个定义如何比较对象大小的接口。

public interface Relatable {

    // this (object calling isLargerThan())
    // and other must be instances of 
    // the same class returns 1, 0, -1 
    // if this is greater than, 
    // equal to, or less than other
    public int isLargerThan(Relatable other);
}

若需比较相似对象的大小(无论对象类型如何),则实例化这些对象的类应实现 Relatable 接口。

任何类只要能比较实例化对象的相对”大小”,即可实现 Relatable 接口。对于字符串,可比较字符数量;对于书籍,可比较页数;对于学生,可比较体重;以此类推。对于平面几何对象,面积是理想选择(参见后文的 RectanglePlus 类);而三维几何对象则适用体积。所有此类类均可实现 isLargerThan() 方法。

若知晓某个类实现了 Relatable 接口,则可确定该类实例化对象的大小可进行比较。

实现Relatable接口

以下是《创建对象》章节中介绍的矩形类,现已重写以实现 Relatable 接口。

public class RectanglePlus implements Relatable {
    public int width = 0;
    public int height = 0;
    public Point origin;

    // four constructors
    public RectanglePlus() {
        origin = new Point(0, 0);
    }
    public RectanglePlus(Point p) {
        origin = p;
    }
    public RectanglePlus(int w, int h) {
        origin = new Point(0, 0);
        width = w;
        height = h;
    }
    public RectanglePlus(Point p, int w, int h) {
        origin = p;
        width = w;
        height = h;
    }

    // a method for moving the rectangle
    public void move(int x, int y) {
        origin.x = x;
        origin.y = y;
    }

    // a method for computing
    // the area of the rectangle
    public int getArea() {
        return width * height;
    }
    
    // a method required to implement
    // the Relatable interface
    public int isLargerThan(Relatable other) {
        RectanglePlus otherRect 
            = (RectanglePlus)other;
        if (this.getArea() < otherRect.getArea())
            return -1;
        else if (this.getArea() > otherRect.getArea())
            return 1;
        else
            return 0;               
    }
}

由于 RectanglePlus 实现了 Relatable 接口,因此可以比较任意两个 RectanglePlus 对象的大小。

注:Relatable 接口中定义的 isLargerThan() 方法接受类型为 Relatable 的对象。该代码行将 other 强制转换为 RectanglePlus 实例。类型转换告知编译器该对象的真实类型。若直接在 other 实例上调用 getArea()(即 other.getArea()),编译将失败,因为编译器无法识别 other 实际上是 RectanglePlus 的实例。

进化中的接口

考虑你开发的一个名为 DoIt 的接口:

public interface DoIt {
   void doSomething(int i, double x);
   int doSomethingElse(String s);
}

假设在后续开发中,你需要为DoIt添加第三个方法,此时接口将变更为:

public interface DoIt {
   void doSomething(int i, double x);
   int doSomethingElse(String s);
   boolean didItWork(int i, double x, String s);
}

若实施此变更,所有实现旧版 DoIt 接口的类都将失效,因为它们不再实现该旧接口。依赖此接口的程序员们必将强烈抗议。

尝试预见接口的所有使用场景,并从一开始就完整地定义它。若需为接口添加额外方法,可采用多种方案。例如创建扩展 DoItDoItPlus 接口:

public interface DoItPlus extends DoIt {
   boolean didItWork(int i, double x, String s);
}

现在,您的代码用户可以选择继续使用旧接口,或升级至新接口。
或者,你可以将新方法定义为默认方法。以下示例定义了一个名为 didItWork() 的默认方法:

public interface DoIt {

   void doSomething(int i, double x);
   int doSomethingElse(String s);
   default boolean didItWork(int i, double x, String s) {
       // Method body 
   }
}

请注意,您必须为默认方法提供实现。您也可以为现有接口定义新的静态方法。对于那些实现已增强接口(包含新增默认方法或静态方法)的类,用户无需修改或重新编译即可支持这些新增方法。

默认方法

接口章节描述了一个涉及计算机控制汽车制造商的案例,这些制造商发布行业标准接口,说明可调用哪些方法来操作其汽车。如果这些计算机控制汽车制造商为其汽车添加新功能(例如飞行功能),该怎么办?这些制造商需要定义新方法,以便其他公司(如电子导航仪器制造商)能将其软件适配至飞行汽车。那么汽车制造商该在哪里声明这些飞行相关的新方法?若将其添加到原有接口中,已实现这些接口的程序员就必须重写实现代码。若作为静态方法添加,程序员则会将其视为实用方法而非核心方法。
默认方法使您能够为库的接口添加新功能,同时确保与为旧版本接口编写的代码保持二进制兼容性。
请考虑以下接口 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

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();
        IO.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() 方法。不过,与其像前例那样将 getZonedDateTime() 声明为抽象方法,你也可以定义一个默认实现。(请记住:抽象方法是指声明时未提供实现的方法。)

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 修饰符。
通过此接口,您无需修改 SimpleTimeClient 类,该类(以及任何实现TimeClient接口的类)都将预先定义好getZonedDateTime() 方法。以下示例 TestSimpleTimeClientSimpleTimeClient 实例调用了 getZonedDateTime() 方法:

扩展包含默认方法的接口

当你扩展包含默认方法的接口时,可以执行以下操作:

  • 完全不提及默认方法,这使得你扩展的接口能够继承默认方法。
  • 重新声明默认方法,使其成为抽象方法。
  • 重新定义默认方法,该方法将覆盖原有实现。
  • 假设你将接口 TimeClient 扩展如下:
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 修饰符。
从 Java SE 9 开始,您可以在接口中定义私有方法,以将接口默认方法中的通用代码片段抽象出来,同时定义其实现。这些方法属于实现部分,定义时既不能是默认方法也不能是抽象方法。例如,您可以将 getZoneId 方法设为私有,因为它包含接口实现内部的代码片段。

将默认方法集成到现有库中

默认方法使您能够为现有接口添加新功能,同时确保与为旧版本接口编写的代码保持二进制兼容性。特别是,默认方法允许您向现有接口添加接受lambda表达式作为参数的方法。本节演示了 Comparator 接口如何通过默认方法和静态方法得到增强。

考虑 CareDeck 类。Card 接口包含两种枚举类型(Suit and Rank)以及两个抽象方法(getSuit()getRank()):

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 接口包含多种操作牌组中卡牌的方法:

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

public class PlayingCard implements Card {

    private Rank rank;
    private Suit suit;

    // constructor

    // implementations of Card abstract methods 
    public Suit getSuit() {
        return this.suit();
    }
    public Rank getRank() {
        return this.rank();
    }
    
    // implementation of Comparable<Card> method
    public int compareTo(Card o) {
        return this.hashCode() - o.hashCode();
    }

    // toString, equals, hashCode
}

StandardDeck 实现抽象方法 Deck.sort() 的实现如下:

public class StandardDeck implements Deck {
    
    private List<Card> entireDeck;
    
    // constructor, accessors
    
    // you need to add all the methods from Deck
    public void sort() {
        Collections.sort(entireDeck);
    }
    
    // toString, equals, hashCode
}

Collections.sort() 方法用于对实现 Comparable 接口的 List 实例进行排序。成员变量 entireDeckList 的实例,其元素类型为 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 list, Comparator<? super T> c)方法(即带 Comparator 参数的排序方法版本)。可在 StandardDeck 类中定义如下方法:

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

通过此方法,您可以指定 Collections.sort()方法如何对 Card 类的实例进行排序。实现方式之一是通过实现Comparator接口来定义卡片排序规则。示例 SortByRankThenSuit 即采用了这种方式:

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实例来比较任何可通过 getValue()hashCode() 等方法返回数值的对象时,将极具实用价值。Comparator接口现已通过静态方法comparing增强了此项能力:

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

在此示例中,您可以改用方法引用:

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

此调用更清晰地展示了如何指定不同的排序标准,同时避免创建多个排序实现。
Comparator接口已通过其他版本的比较静态方法得到增强,例如comparingDouble()comparingLong(),这些方法使您能够创建用于比较其他数据类型的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() 得到增强,例如 thenComparingDouble()thenComparingLong(),这些方法使您能够构建用于比较其他数据类型的 Comparator 实例。

假设开发人员需要创建一个Comparator实例,以便按逆序对对象集合进行排序。例如,如何将一副扑克牌按点数降序排列(从A到2,而非从2到A)?与之前类似,你可以指定另一个lambda表达式。但若能通过调用方法来反转现有Comparator,对开发人员而言会更简便。Comparator接口已通过默认方法reversed()增强了此功能:

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

此示例展示了如何通过默认方法、静态方法、lambda表达式和方法引用增强Comparator接口,从而创建更具表达力的库方法。程序员只需观察方法的调用方式,就能快速推断其功能。请运用这些构造来增强您库中的接口。