116. Java 接口 - 默认方法
在 Java 8 中,接口引入了默认方法,使得在接口中为方法提供实现成为可能。这意味着即使接口的用户没有对某个方法进行实现,仍然可以调用该方法。这一特性非常适合向现有接口添加新功能而不破坏现有的实现类。
背景
假设你在开发一款计算机控制的汽车制造商软件,并且这些制造商使用接口来标准化他们汽车的方法。最初,他们的接口只包括控制汽车的方法,如加速、刹车等。
但是,随着汽车技术的发展,你们现在希望添加一个新功能:飞行。为了实现这一点,汽车制造商需要在原有的接口中增加一些方法。
如果我们直接在接口中添加新方法,所有已经实现该接口的类都必须更新实现,这可能会导致很多代码的重写。如果我们将新方法定义为静态方法,那么这些方法将不会成为核心方法,而是作为工具方法使用,可能会失去接口的灵活性。
默认方法为此提供了一个完美的解决方案。它允许我们向接口中添加新功能,并保持向后兼容,确保旧的实现类不需要进行修改。
示例:TimeClient 接口的演变
考虑一个用于时间管理的接口 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:
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); // 新增方法
}
但是,如果你这么做,SimpleTimeClient 就需要重新实现 getZonedDateTime() 方法。
使用默认方法(Default Methods)
为了避免强制修改 SimpleTimeClient 类,你可以使用默认方法来为 getZonedDateTime() 提供默认实现:
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();
// 新增静态方法获取 ZoneId
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));
}
}
在这个接口中,getZonedDateTime() 方法已经有了默认实现,SimpleTimeClient 类不需要做任何改动,就可以使用新方法。这样,原本实现了旧接口的类(如 SimpleTimeClient)可以继续工作,而不需要进行任何更新。
测试类:TestSimpleTimeClient
我们可以创建一个测试类来验证新方法是否可用:
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("America/Los_Angeles").toString());
}
}
扩展接口时的行为
-
直接继承默认方法: 如果你扩展一个包含默认方法的接口,那么新接口将继承所有的默认方法。例如:
public interface AnotherTimeClient extends TimeClient { }任何实现
AnotherTimeClient的类都将自动获得TimeClient接口中定义的getZonedDateTime()方法的默认实现。 -
重新声明默认方法为抽象方法: 你也可以在扩展的接口中将默认方法重新声明为抽象方法,迫使实现类必须提供自己的实现:
public interface AbstractZoneTimeClient extends TimeClient { public ZonedDateTime getZonedDateTime(String zoneString); // 抽象方法 }任何实现
AbstractZoneTimeClient的类都必须实现getZonedDateTime()方法。 -
重新定义默认方法: 你还可以在扩展接口时重新定义默认方法,从而覆盖接口中已有的默认实现。例如:
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()); } } }在这个接口中,
getZonedDateTime()方法已经覆盖了父接口的默认实现,如果实现HandleInvalidTimeZoneClient的类不提供自定义实现,则会使用这个新版本的默认方法。
总结
- 默认方法:可以为接口中的方法提供默认实现,使得接口可以向后兼容。
- 静态方法:可用于定义工具方法,不影响接口的实现类。
- 继承默认方法:扩展接口时,可以继承默认方法,或者将其重新声明为抽象方法,也可以覆盖它。
通过合理使用默认方法,接口的扩展和修改变得更加灵活和兼容现有代码。