Java 8 官方教程翻译——默认方法(default method)

默认方法

在接口(interface)一节中提到了一个示例,其中涉及了一些发布工业标准接口的“计算机控制汽车“生产商,其发布的接口中描述了哪些方法(method)可以用来操作这些汽车。假如这些生产商需要给他们的汽车添加新的功能,例如飞行,那么就需要指定一些新的操作方法来使得第三方(例如一些电子导航装置的生产商)软件适配到这些新汽车上。那么在何处声明这些新的”飞行相关“方法呢?如果将之添加到原有的接口中,那么所有已经实现过过这些接口的程序员需要重写他们的实现(implementation)。如果将之添加为静态方法(static method),那么程序员会将之视为非必要非核心的工具方法。


默认方法(default method)可以帮助你将新的功能添加到库中原有的接口而且能确保与针对原有接口编写的代码保持二进制兼容(binary compatibility)。

参看如下接口,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类,并且该类(以及任何实现了该接口的类)将拥有已经定义好的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在HandleInvalidTimeZoneClient中提供的默认实现而非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修饰符。


将默认方法整合到现有库

默认方法(default method)可以帮助你将新的功能添加到现有接口而且能确保与之前针对该接口编写的代码保持二进制兼容(binary compatibility)。特别的是,默认方法还使得你可以向现有接口中添加接收lambda表达式参数的方法。这一节中将会向你展示Comparator接口是如何通过默认方法和静态方法得到增强了。

参看前面提到的Card和Deck类。这个示例将Card和Deck类重写为接口。Card接口包含了两个enum类型(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接口则包含了一些在桌上操作纸牌的方法:
     
     
     
     
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实例进行排序,List中的元素类型需要实现Comparable接口。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()方法对桌面上的纸牌首先按照花色(suit)然后按照面值(rank)排列。

假如需要按先面值再花色的规则进行排序该怎么做?你可能会实现Comarator接口来指定新的排序规则,并且使用方法sort(List list, Comparator 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接口是一个函数接口(functional interface),因此你可以使用lambda表达式作为参数调用sort方法。在这个示例中,lambda表达式比较了两个整形值。

如果开发者能仅仅通过调用方法Card.getRank创建Comparator对象就更加方便了。更进一步,如果假如开发者可以创建一个Comparator用于比较任何能通过某方法(例如getValue或hashCode)返回数值的对象,那将会是更有益的。事实上,Comparator接口已经通过静态方法comparing得到了此种增强。
     
     
     
     
myDeck.sort(Comparator.comparing((card) -> card.getRank()));
在该示例中,你可以使用一个方法引用(method reference)代替其中的lambda表达式:
     
     
     
     
myDeck.sort(Comparator.comparing(Card::getRank));
该调用就更加清晰的展示了代码的意图(对什么排序而非如何排序)

Comparator接口还增加了comparing的其他版本,例如comparingDouble和comparingLong使得你可以创建比较其他数据类型的Compatator。

假如开发者需要创建一个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实例来反向排序一个对象集合。假如要对桌上的纸牌根据面值进行降序排序,应该怎么做?照前所示,你可以指定另外一个lambda表达式。然而,如果能通过调用Comparator的现存方法来使得Comparator逆序岂不是更妙。事实上,Comparator已经通过默认方法reversed实现该功能:
     
     
     
     
myDeck.sort(
Comparator.comparing(Card::getRank)
.reversed()
.thenComparing(Comparator.comparing(Card::getSuit)));
该示例展示了Comparator接口是如何通过默认方法,静态方法,lambda表达式以及方法引用来得到增强的,使得可以创建更具表达力的库方法,进而使得开发者可以更加快速的理解方法的调用意图。总而言之,你可以使用这些设施来增强你的库。

原文链接:http://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

你可能感兴趣的:(编程语言)