文档

Java™ 教程
隐藏目录
默认方法
导航:学习Java语言
课程:接口和继承
部分:接口

默认方法

“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方法。然而,与其像上一个示例中那样将getZonedDateTime保留为abstract方法(即没有实现),您可以定义一个默认实现。(请记住,抽象方法是没有实现的方法。)

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修饰符。

使用这个接口,您不需要修改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("当前时间: " + myTimeClient.toString());
        System.out.println("加利福尼亚的时间: " +
            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("无效的时区ID: " + zoneString +
                "; 使用默认时区代替。");
            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("无效的时区:" + zoneString +
                ";使用默认时区代替。");
            return ZoneId.systemDefault();
        }
    }

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

与类中的静态方法一样,您可以在接口中的方法定义之前使用 static 关键字指定该方法定义为静态方法。接口中的所有方法声明,包括静态方法,都是隐式的 public,因此您可以省略 public 修饰符。

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

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

考虑描述在 问题和练习:类 中所述的 CardDeck 类。该示例将 CardDeck 类重写为接口。 Card 接口包含两个 enum 类型(SuitRank)和两个抽象方法(getSuitgetRank):

package defaultmethods;

public interface Card extends Comparable<Card> {
    
    public enum Suit { 
        DIAMONDS (1, "方片"), 
        CLUBS    (2, "梅花"   ), 
        HEARTS   (3, "红桃"  ), 
        SPADES   (4, "黑桃"  );
        
        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 , "二"  ),
        THREE  (3 , "三"), 
        FOUR   (4 , "四" ), 
        FIVE   (5 , "五" ), 
        SIX    (6 , "六"  ), 
        SEVEN  (7 , "七"),
        EIGHT  (8 , "八"), 
        NINE   (9 , "九" ), 
        TEN    (10, "十"  ), 
        JACK   (11, "杰克" ),
        QUEEN  (12, "皇后"), 
        KING   (13, "国王" ),
        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实例进行排序。成员entireDeck是一个List实例,其元素类型为Card,而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<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等方法返回数值的对象,那将很有帮助。Comparator接口通过静态方法comparing增强了这种能力:

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到Two的降序对一副扑克牌进行排序(而不是从Two到Ace)?与之前一样,你可以指定另一个lambda表达式。然而,如果他们能够通过调用一个方法来反转现有的Comparator,对于你的开发人员来说会更简单。通过默认方法reversedComparator接口已经增强了这种能力:

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

这个例子演示了如何使用默认方法、静态方法、lambda表达式和方法引用来增强Comparator接口,从而创建更具表达力的库方法。通过查看它们的调用方式,程序员可以快速推断出它们的功能。使用这些构造来增强你的库接口。


上一页: 进化的接口
下一页: 接口摘要