这些Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
请参阅Java语言更改以了解Java SE 9及后续版本中更新的语言特性摘要。
请参阅JDK发布说明以获取有关所有JDK版本的新功能、增强功能以及已删除或弃用选项的信息。
“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()); } }
当你扩展一个包含默认方法的接口时,可以进行以下操作:
abstract
。假设你如下扩展了接口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
接口。
考虑描述在 问题和练习:类 中所述的 Card
和 Deck
类。该示例将 Card
和 Deck
类重写为接口。 Card
接口包含两个 enum
类型(Suit
和 Rank
)和两个抽象方法(getSuit
和 getRank
):
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
实例来比较任何可以从getValue
或hashCode
等方法返回数值的对象,那将很有帮助。Comparator
接口通过静态方法comparing
增强了这种能力:
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到Two的降序对一副扑克牌进行排序(而不是从Two到Ace)?与之前一样,你可以指定另一个lambda表达式。然而,如果他们能够通过调用一个方法来反转现有的Comparator
,对于你的开发人员来说会更简单。通过默认方法reversed
,Comparator
接口已经增强了这种能力:
myDeck.sort( Comparator.comparing(Card::getRank) .reversed() .thenComparing(Comparator.comparing(Card::getSuit)));
这个例子演示了如何使用默认方法、静态方法、lambda表达式和方法引用来增强Comparator
接口,从而创建更具表达力的库方法。通过查看它们的调用方式,程序员可以快速推断出它们的功能。使用这些构造来增强你的库接口。