这篇Java教程基于JDK1.8。教程中的示例和实践不会使用未来发行版中的优化建议。
默认方法
接口 部分描述了一个用电脑控制汽车的制造商的故事,制造商会发布一些行业标准接口,这些接口描述可以调用哪些方法来操作汽车。如果这些制造商为汽车增加新的能力,比如飞行,那会怎么样呢?制造商们需要指定新的方法,这样其他公司(如导航仪制造商)才能够将其软件应用于飞行汽车。汽车制造商们在哪里去发布这些新的适用于飞行汽车的方法呢?如果将它们添加到原来的接口,那么基于原来接口开发的程序员不得不重写实现。如果将它们声明为静态方法,那程序员们会把它们当做工具方法,而不是必须实现的核心方法。
默认方法使你能够向仓库的接口添加新功能,并确保兼容这些为旧版本接口编写的代码。
考虑下面的接口,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:
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对象指定一个时区:
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方法不是抽象的,你可以给它一个 默认 的实现。
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方法。下面的示例调用了 SimpleTimeClient 的 getZonedDateTime 方法:
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对象时,它将使用系统默认的时区。
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 示例:
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();
}
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 接口。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 list, Comparator<? super T> c)
。可以在 StandardDeck 中定义如下方法:
public void sort(Comparator<Card> c) {
Collections.sort(entireDeck, c);
}
在这个方法中,你可以定义Collections.sort如何对 Card实例进行排序。一种方式是通过实现 Comparator 接口来指定如何对纸牌排序。如下所示 :
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 接口的程序员,你将在该接口中添加哪些方法来让其他程序员能够更容易的指定排序标准。
为简单起见,假设要对纸牌的排名进行排序,不考虑其花色。你可以用如下方法来调用 *StandardDeck.sort * 方法:
StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
(firstCard, secondCard) ->
firstCard.getRank().value() - secondCard.getRank().value()
);
由于 Comparator 是一个函数式接口,所以你可以在 sort 方法中使用lambda表达式作为参数。在本例中,lambda表达式比较两个整型值。
对开发者而言,通过调用方法 Card.getRank 可以很容易的创建一个 Comparator 实例。尤其对开发者最有用的是,能够比较任意对象,只要对象能够通过一些方法(比如 getValue 或者 hashCode)返回数值就能创建一个 Comparator 实例。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方法,用来构建比较其他数据类型的 Comparator 实例。
假如开发者要创建一个逆序排列的 Comparator 实例。比如:对纸牌先根据排名逆序排,从Ace到Two?如果是以前的做法,你会指定另外一个lambda表达式。但是,如果开发人员能够通过调用一个方法来反转现有的比较器,那么将会更加简单。比较器提供了一个静态方法 reversed来完成这项能力:
myDeck.sort(
Comparator.comparing(Card::getRank)
.reversed()
.thenComparing(Comparator.comparing(Card::getSuit)));
这个示例演示了如何使用默认方法、静态方法、lambda表达式和方法引用来增强Comparator接口,从而创建更具表现力的库方法,程序员可以通过查看调用方法来快速推断这些方法的功能。使用这些构造能大大增强库中的接口能力。