乐趣区

Java™ 教程(默认方法)

默认方法
接口部分描述了一个涉及计算机控制汽车制造商的例子,他们发布了行业标准接口,描述了可以调用哪些方法来操作他们的汽车,如果那些计算机控制的汽车制造商为他们的汽车添加新的功能,如飞行,该怎么办?这些制造商需要指定新的方法,以使其他公司(如电子制导仪器制造商)能够使其软件适应飞行汽车,这些汽车制造商将在哪里声明这些与飞行有关的新方法?如果他们将它们添加到原始接口,那么实现了这些接口的程序员将不得不重写他们的实现,如果他们将它们作为静态方法添加,那么程序员会将它们视为实用方法,而不是必要的核心方法。
默认方法使你能够向库的接口添加新功能,并确保与为这些接口的旧版本编写的代码的二进制兼容性。
考虑以下接口 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 保留为抽象(如上例所示),请记住,抽象方法是在没有实现的情况下声明的方法。
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(“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,因此你可以省略 public 修饰符。
将默认方法集成到现有库中
默认方法使你能够向现有接口添加新功能,并确保与为这些接口的旧版本编写的代码的二进制兼容性,特别是,默认方法使你能够将接受 lambda 表达式的方法添加为现有接口的参数,本节演示如何使用默认和静态方法增强 Comparator 接口。
考虑 Card 和 Deck 类,此示例将 Card 和 Deck 类重写为接口,Card 接口包含两个枚举类型(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 接口包含操作 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,它扩展了 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 实例来比较任何可以从 getValue 或 hashCode 等方法返回数值的对象,那将会很有帮助。使用此静态方法 comparing 增强了 Comparator 接口:
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 实例,使其能够以相反的顺序对对象集合进行排序,例如,你如何按照等级的降序排序扑克牌组,从 Ace 到 2(而不是从 2 到 Ace)?和以前一样,你可以指定另一个 lambda 表达式,但是,如果你的开发人员可以通过调用方法来反转现有的 Comparator,那么它会更简单,使用此功能增强了 Comparator 接口,默认方法 reversed:
myDeck.sort(
Comparator.comparing(Card::getRank)
.reversed()
.thenComparing(Comparator.comparing(Card::getSuit)));
这个例子演示了 Comparator 接口是如何通过默认方法、静态方法、lambda 表达式和方法引用来增强的,从而创建更具表现力的库方法,程序员可以通过查看调用这些方法的方式来快速推断这些方法的功能,使用这些构造来增强库中的接口。

上一篇:不断发展的接口
下一篇:继承

退出移动版