Java中interface接口定义用法示例官方详解
文章目录
定义接口
一个接口声明由修饰符、关键字interface
、接口名称、以逗号分隔的父接口列表(如果有的话)和接口体组成。例如:
public interface GroupedInterface extends Interface1, Interface2, Interface3 {
// 常量声明
// 自然对数的底数
double E = 2.718282;
// 方法签名
void doSomething(int i, double x);
int doSomethingElse(String s);
}
public
访问修饰符表示该接口可以被任何包中的任何类使用。如果您没有指定接口为public
,则该接口只能被与接口在同一包中定义的类访问。
接口可以扩展其他接口,就像类可以继承另一个类一样。然而,一个类只能继承一个其他类,而一个接口可以扩展任意数量的接口。接口声明包括一个逗号分隔的所有它扩展的接口列表。
接口体
接口体可以包含抽象方法、默认方法和静态方法。接口中的抽象方法后面跟着一个分号,但没有大括号(抽象方法不包含实现)。默认方法用default
修饰符定义,静态方法用static
关键字定义。接口中的所有抽象、默认和静态方法都隐式地是public
的,所以可以省略public
修饰符。
此外,接口可以包含常量声明。在接口中定义的所有常量值都隐式地是public
、static
和final
的。同样,您可以省略这些修饰符。
实现接口
要声明一个实现接口的类,您需要在类声明中包含一个implements
子句。您的类可以实现多个接口,因此implements
关键字后面跟着一个逗号分隔的接口列表,表示该类所实现的接口。按照惯例,如果有extends
子句,那么implements
子句应该位于其后面。
一个示例接口,Relatable
考虑一个定义如何比较对象大小的接口。
public interface Relatable {
// this (调用isLargerThan方法的对象)
// 和other必须是同一类的实例,返回1、0、-1
// 表示this大于、等于或小于other
public int isLargerThan(Relatable other);
}
如果希望能够比较相似对象的大小,无论它们是什么,实例化它们的类应该实现Relatable
接口。
只要有一种方式可以比较从类实例化的对象的相对"大小",任何类都可以实现Relatable
接口。对于字符串,可以是字符数;对于书籍,可以是页数;对于学生,可以是体重;等等。对于平面几何对象,面积是一个不错的选择(请参见下面的RectanglePlus
类),而对于三维几何对象,体积则适用。所有这些类都可以实现isLargerThan()
方法。
如果知道一个类实现了Relatable
接口,那么就知道可以比较从该类实例化的对象的大小。
实现Relatable
接口
下面是在"创建对象"部分中介绍的Rectangle
类的重写版本,以实现Relatable
接口。
public class RectanglePlus implements Relatable {
public int width = 0;
public int height = 0;
public Point origin;
// 四个构造函数
public RectanglePlus() {
origin = new Point(0, 0);
}
public RectanglePlus(Point p) {
origin = p;
}
public RectanglePlus(int w, int h) {
origin = new Point(0, 0);
width = w;
height = h;
}
public RectanglePlus(Point p, int w, int h) {
origin = p;
width = w;
height = h;
}
// 移动矩形的方法
public void move(int x, int y) {
origin.x = x;
origin.y = y;
}
// 计算矩形的面积的方法
public int getArea() {
return width * height;
}
// 实现Relatable接口所要求的方法
public int isLargerThan(Relatable other) {
RectanglePlus otherRect = (RectanglePlus)other;
if (this.getArea() < otherRect.getArea())
return -1;
else if (this.getArea() > otherRect.getArea())
return 1;
else
return 0;
}
}
由于RectanglePlus
实现了Relatable
接口,可以比较任意两个RectanglePlus
对象的大小。
**注意:**在Relatable
接口中定义的isLargerThan
方法接受一个类型为Relatable
的对象。在上面示例的粗体代码行中,将other
强制转换为RectanglePlus
实例。类型转换告诉编译器对象的真实类型。直接在other
实例上调用getArea()
(other.getArea()
)将无法编译通过,因为编译器不理解other
实际上是RectanglePlus
的实例。
使用接口作为类型
当您定义一个新的接口时,实际上是在定义一个新的引用数据类型。您可以在任何可以使用其他数据类型名称的地方使用接口名称。如果您定义一个类型为接口的引用变量,那么分配给它的任何对象都必须是实现该接口的类的实例。
例如,下面是一个用于找到一对对象中最大对象的方法,适用于任何从实现了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) {
// 方法体
}
}
请注意,您必须为默认方法提供实现。您还可以向现有接口定义新的静态方法。具有实现了新默认或静态方法的接口的类的用户无需修改或重新编译它们以适应附加的方法。
默认方法
在《接口》一节中,描述了一个例子,涉及到计算机控制汽车制造商发布的行业标准接口,描述可以调用哪些方法来操作它们的汽车。如果这些计算机控制汽车制造商为其汽车添加了新功能,比如飞行,他们将需要指定新的方法,以使其他公司(如电子导航仪制造商)能够将其软件适配到飞行汽车上。这些汽车制造商将在何处声明这些与飞行相关的新方法?如果将它们添加到原始接口中,那么已经实现该接口的程序员将不得不重写他们的实现。如果将它们添加为静态方法,则程序员会将其视为实用方法,而不是必要的核心方法。
默认方法允许您向库的接口添加新功能,并确保与针对较旧版本接口编写的代码具有兼容性。
考虑以下接口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());
}
}
扩展包含默认方法的接口
当您扩展包含默认方法的接口时,可以采取以下措施:
- 不提及默认方法,使扩展接口继承默认方法。
- 重新声明默认方法,使其变为抽象方法。
- 重新定义默认方法,覆盖它。
假设您扩展了接口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
对象,则使用系统默认时区。(因此,您可以简化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接口包含两个枚举类型(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是一个元素类型为Card的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接口来指定新的排序规则,并使用带有Comparator参数的sort(List list, Comparator<? super T> c)方法。您可以在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接口添加默认或静态方法,以便其他开发人员更容易地指定排序条件。
首先,假设您想要根据点数对扑克牌进行排序,而不考虑花色。您可以使用lambda表达式作为sort方法的参数来调用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接口已经通过static方法comparing增强了这种功能:
myDeck.sort(Comparator.comparing((card) -> card.getRank()));
在这个例子中,您可以使用方法引用来替代:
myDeck.sort(Comparator.comparing(Card::getRank));
这个调用更好地演示了如何指定不同的排序条件并避免创建多个排序实现。
Comparator接口还通过比较其他数据类型的默认方法(如comparingDouble和comparingLong)增强了静态方法comparing的版本。
假设您的开发人员想要创建一个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,那么对他们来说会更简单。Comparator接口已经通过默认方法reversed增强了这种能力:
myDeck.sort(
Comparator.comparing(Card::getRank)
.reversed()
.thenComparing(Comparator.comparing(Card::getSuit)));
这个例子演示了Comparator接口如何通过默认方法、静态方法、lambda表达式和方法引用来创建更富有表现力的库方法,程序员可以通过查看它们的调用方式快速推断出功能。使用这些构造来增强您的库中的接口。
接口概述
-
接口声明可以包含方法签名、默认方法、静态方法和常量定义。只有默认方法和静态方法有实现。
-
实现接口的类必须实现接口中声明的所有方法。
-
接口名称可以在任何可以使用类型的地方使用。
使用场景
如果以下任何情况适用于您的情况,请考虑使用接口:
-
您预计不相关的类将实现您的接口。例如,可比较和可克隆的接口被许多不相关的类实现。
-
您希望指定特定数据类型的行为,但不关心谁实现其行为。
-
您希望利用类型的多重继承。
如果以下任何情况适用于您的情况,请考虑使用抽象类:
- 您希望在多个相关类之间共享代码。
- 您期望扩展您的抽象类的类具有许多共同的方法或字段,或者需要除public之外的访问修饰符(例如protected和private)。
- 您希望声明非静态或非final字段。这使您可以定义可以访问和修改其所属对象的状态的方法。
参考链接
https://docs.oracle.com/javase/tutorial/java/IandI/createinterface.html