Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
Interfaces 部分描述了一个涉及计算机控制汽车制造商的例子,他们发布了行业标准接口,描述了可以调用哪些方法来操作他们的汽车。如果那些计算机控制的汽车制造商为他们的汽车添加新的功能,例如飞行,该怎么办?这些制造商需要指定新的方法,以使其他公司(如电子制导仪器制造商)能够使其软件适应飞行汽车。这些汽车制造商将在哪里宣布这些与航班有关的新方法?如果他们将它们添加到原始接口,那么实现了这些接口的程序员将不得不重写他们的实现。如果他们将它们作为静态方法添加,那么程序员会将它们视为实用方法,而不是必要的核心方法。
默认方法使你能够向库的接口添加新功能,并确保与为这些接口的旧版本编写的代码的二进制兼容性。
考虑以下接口 TimeClient
,如 Answers to Questions and Exercises: Interfaces 中所述:
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
。但是,你可以改为定义 default implementation (默认实现),而不是让 getZonedDateTime
abstract
(如上例所示)。(请记住,abstract method 是在没有实现的情况下声明的方法。)
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()); } }
继承包含默认方法的接口时,可以执行以下操作:
abstract
。假设你继承接口 TimeClient
,如下所示:
public interface AnotherTimeClient extends TimeClient { }
任何实现接口 AnotherTimeClient
的类都将具有由 TimeClient.getZonedDateTime
默认方法指定的实现。
假设你继承接口 TimeClient
,如下所示:
public interface AbstractZoneTimeClient extends TimeClient { public ZonedDateTime getZonedDateTime(String zoneString); }
任何实现接口 AbstractZoneTimeClient
的类都必须实现方法 getZonedDateTime
;此方法是一个 abstract
方法,就像接口中的所有其他非默认(和非静态)方法一样。
假设你继承接口 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
指定的实现。
除了默认方法,你还可以在接口中定义 static methods 。(静态方法是一种与定义它的类相关联的方法,而不是与任何对象相关联的方法。该类的每个实例都共享其静态方法。)这使你可以更轻松地在库中组织辅助方法;你可以在同一个接口中保留特定于接口的静态方法,而不是在单独的类中。以下示例定义一个静态方法,该方法获取与时区标识符对应的 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
接口。
考虑 Questions and Exercises: Classes 中描述的 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
的实例进行排序,其元素类型实现接口 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());
但是,这种方法过于冗长;如果你能指定要排序 what (什么),而不是 how (如何) 排序,那会更好。假设你是编写 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 (方法引用):
myDeck.sort(Comparator.comparing(Card::getRank));
此调用更好地演示排序 what (什么) 而不是 how (如何) 进行排序。
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)));
此示例演示如何使用默认方法,静态方法,lambda 表达式和方法引用增强 Comparator
接口,以创建更具表现力的库方法,其功能程序员可以通过查看它们的调用方式快速推断出来。使用这些构造来增强库中的接口。