Java 中文官方教程 2022 版(四)(2)https://developer.aliyun.com/article/1486287
使用接口作为类型
原文:
docs.oracle.com/javase/tutorial/java/IandI/interfaceAsType.html
当你定义一个新接口时,你正在定义一个新的引用数据类型。你可以在任何可以使用其他数据类型名称的地方使用接口名称。如果你定义一个类型为接口的引用变量,那么你分配给它的任何对象必须是实现了该接口的类的实例。
举例来说,这里有一种方法可以找到一对对象中最大的对象,适用于任何从实现了Relatable
接口的类实例化的对象:
public Object findLargest(Object object1, Object object2) { Relatable obj1 = (Relatable)object1; Relatable obj2 = (Relatable)object2; if ((obj1).isLargerThan(obj2) > 0) return object1; else return object2; }
通过将object1
强制转换为Relatable
类型,它可以调用isLargerThan
方法。
如果你坚持在各种类中实现Relatable
,那么从任何这些类实例化的对象都可以使用findLargest()
方法进行比较——前提是这两个对象属于同一类。同样,它们也可以使用以下方法进行比较:
public Object findSmallest(Object object1, Object object2) { Relatable obj1 = (Relatable)object1; Relatable obj2 = (Relatable)object2; if ((obj1).isLargerThan(obj2) < 0) return object1; else return object2; } public boolean isEqual(Object object1, Object object2) { Relatable obj1 = (Relatable)object1; Relatable obj2 = (Relatable)object2; if ( (obj1).isLargerThan(obj2) == 0) return true; else return false; }
这些方法适用于任何“可比较”的对象,无论它们的类继承关系如何。当它们实现了Relatable
接口时,它们可以是自己类(或超类)类型和Relatable
类型。这使它们具有多重继承的一些优势,可以同时具有来自超类和接口的行为。
接口的演变
考虑您开发的名为DoIt
的接口:
public interface DoIt { void doSomething(int i, double x); int doSomethingElse(String s); }
假设以后,您想要向DoIt
添加第三个方法,使接口现在变成:
public interface DoIt { void doSomething(int i, double x); int doSomethingElse(String s); boolean didItWork(int i, double x, String s); }
如果您进行此更改,则所有实现旧DoIt
接口的类都将中断,因为它们不再实现旧接口。依赖于此接口的程序员将会强烈抗议。
尽量预见接口的所有用途并从一开始完全指定它。如果要向接口添加其他方法,您有几个选项。您可以创建一个扩展DoIt
的DoItPlus
接口:
public interface DoItPlus extends DoIt { boolean didItWork(int i, double x, String s); }
现在,您的代码用户可以选择继续使用旧接口或升级到新接口。
或者,您可以将新方法定义为默认方法。以下示例定义了一个名为didItWork
的默认方法:
public interface DoIt { void doSomething(int i, double x); int doSomethingElse(String s); default boolean didItWork(int i, double x, String s) { // Method body } }
请注意,您必须为默认方法提供实现。您还可以为现有接口定义新的静态方法。实现增强了新默认或静态方法的接口的类的用户无需修改或重新编译它们以适应额外的方法。
默认方法
原文:
docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html
接口部分描述了一个涉及计算机控制汽车制造商发布行业标准接口的示例,描述了可以调用哪些方法来操作他们的汽车。如果这些计算机控制汽车制造商为他们的汽车添加新功能,比如飞行,会怎么样?这些制造商需要指定新的方法来使其他公司(如电子导航仪制造商)能够调整他们的软件以适应飞行汽车。这些汽车制造商会在哪里声明这些新的与飞行相关的方法?如果他们将它们添加到原始接口中,那么已经实现这些接口的程序员将不得不重新编写他们的实现。如果将它们添加为静态方法,那么程序员会将它们视为实用方法,而不是必要的核心方法。
默认方法使您能够向库的接口添加新功能,并确保与为旧版本接口编写的代码的二进制兼容性。
考虑下面的接口,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()); } }
扩展包含默认方法的接口
当您扩展包含默认方法的接口时,可以执行以下操作:
- 完全不提及默认方法,让您扩展的接口继承默认方法。
- 重新声明默认方法,使其为
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
指定的实现。
静态方法
除了默认方法之外,您还可以在接口中定义静态方法。(静态方法是与定义它的类相关联的方法,而不是与任何对象相关联的方法。类的每个实例共享其静态方法。)这使您更容易在库中组织辅助方法;您可以将特定于接口的静态方法保留在同一接口中,而不是在单独的类中。以下示例定义了一个静态方法,用于检索与时区标识符对应的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
接口包含两个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
对实现接口Comparable
的元素类型为List
的实例进行排序。成员entireDeck
是一个List
的实例,其元素类型为扩展了Comparable
的Card
类型。类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 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
实例,那将更简单。特别是,如果您的开发人员可以创建一个比较任何可以从getValue
或hashCode
等方法返回数值的对象的Comparator
实例,那将很有帮助。Comparator
接口已经通过静态方法comparing
增强了这种能力:
myDeck.sort(Comparator.comparing((card) -> card.getRank()));
在这个例子中,您可以使用方法引用:
myDeck.sort(Comparator.comparing(Card::getRank));
这种调用更好地演示了如何指定不同的排序标准并避免创建多个排序实现。
Comparator
接口已经通过其他版本的静态方法comparing
(如comparingDouble
和comparingLong
)进行了增强,使您能够创建比较其他数据类型的Comparator
实例。
Java 中文官方教程 2022 版(四)(4)https://developer.aliyun.com/article/1486289