116. Java 接口 - 默认方法

79 阅读4分钟

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());
    }
}

扩展接口时的行为

  1. 直接继承默认方法: 如果你扩展一个包含默认方法的接口,那么新接口将继承所有的默认方法。例如:

    public interface AnotherTimeClient extends TimeClient { }
    

    任何实现 AnotherTimeClient 的类都将自动获得 TimeClient 接口中定义的 getZonedDateTime() 方法的默认实现。

  2. 重新声明默认方法为抽象方法: 你也可以在扩展的接口中将默认方法重新声明为抽象方法,迫使实现类必须提供自己的实现:

    public interface AbstractZoneTimeClient extends TimeClient {
        public ZonedDateTime getZonedDateTime(String zoneString);  // 抽象方法
    }
    

    任何实现 AbstractZoneTimeClient 的类都必须实现 getZonedDateTime() 方法。

  3. 重新定义默认方法: 你还可以在扩展接口时重新定义默认方法,从而覆盖接口中已有的默认实现。例如:

    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 的类不提供自定义实现,则会使用这个新版本的默认方法。


总结

  • 默认方法:可以为接口中的方法提供默认实现,使得接口可以向后兼容。
  • 静态方法:可用于定义工具方法,不影响接口的实现类。
  • 继承默认方法:扩展接口时,可以继承默认方法,或者将其重新声明为抽象方法,也可以覆盖它。

通过合理使用默认方法,接口的扩展和修改变得更加灵活和兼容现有代码。