系列中的上一篇
当前教程
实现接口
系列中的下一篇

系列中的上一篇: 接口

系列中的下一篇: 使用接口作为类型

实现接口

 

定义接口 Relatable

要声明实现接口的类,您需要在类声明中包含一个 implements 子句。您的类可以实现多个接口,因此 implements 关键字后面跟着一个用逗号分隔的列表,其中包含类实现的接口。按照惯例,implements 子句位于 extends 子句之后(如果有)。

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

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 接口

以下是“创建对象”部分中介绍的 Rectangle 类,已重新编写以实现 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 对象的大小。

注意:isLargerThan() 方法(如 Relatable 接口中定义的那样)接受一个 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 接口的类都将中断,因为它们不再实现旧接口。依赖此接口的程序员会强烈抗议。

尝试预测接口的所有用途,并从一开始就完全指定它。如果您想向接口添加其他方法,您有几种选择。您可以创建一个 DoItPlus 接口,它扩展 DoIt

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();
        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()。但是,与其将 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 修饰符。

使用此接口,您不必修改类 SimpleTimeClient,并且此类(以及任何实现接口 TimeClient 的类)将已经定义了方法 getZonedDateTime()。以下示例 TestSimpleTimeClientSimpleTimeClient 的实例调用方法 getZonedDateTime()

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

 

扩展包含默认方法的接口

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

  • 根本不提及默认方法,这将使您的扩展接口继承默认方法。

  • 重新声明默认方法,这将使其成为抽象方法。

  • 重新定义默认方法,这将覆盖它。

  • 假设您扩展接口 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 修饰符。

从 Java SE 9 开始,您可以在接口中定义私有方法,以从接口的默认方法中抽象出公共代码段,同时定义其实现。这些方法属于实现,在定义时既不能是默认的,也不能是抽象的。例如,您可以将 getZoneId 方法设为私有,因为它包含接口实现内部的代码段。

 

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

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

考虑 CardDeck 类。Card 接口包含两个 enum 类型(SuitRank)以及两个抽象方法(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()List 的实例进行排序,该实例的元素类型实现 Comparable 接口。成员 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<T> list, Comparator<? super T> c)(包含 Comparator 参数的 sort 方法版本)。您可以在 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 接口已通过静态方法 comparing 的其他版本进行了增强,例如 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 接口,以创建更具表现力的库方法,程序员可以通过查看它们是如何调用的来快速推断其功能。使用这些结构来增强您库中的接口。


上次更新: 2021 年 9 月 14 日


系列中的上一篇
当前教程
实现接口
系列中的下一篇

系列中的上一篇: 接口

系列中的下一篇: 使用接口作为类型