当前位置:Java -> 释放Java接口的威力

释放Java接口的威力

长时间以来,Java接口只是接口,一组贫乏的函数原型。即使在那时,也有对接口的非标准使用(例如标记接口),但也仅此而已。

然而,自从Java 8以来,接口发生了重大变化。默认方法和静态方法的添加启用了许多新的可能性。例如,允许在不破坏旧代码的情况下向现有接口添加新功能。或者将所有实现隐藏在工厂方法后,并强制执行“针对接口编码”策略。封闭接口的添加使得可以在代码设计意图中创建真正的和表达式。这些变化使得Java接口成为一个功能强大、简洁和富有表现力的工具。让我们来看看一些非传统的Java接口应用。

流畅构建器

流畅(或分阶段)构建器是一种用于组装对象实例的模式。与传统的构建器模式不同之处在于,它可以防止创建不完整的对象,并强制执行字段初始化的固定顺序。这些特性使其成为可靠且易于维护的代码首选。

流畅构建器的想法相当简单。它不是在设置属性后返回相同的构建器实例,而是返回一个新类型(类或接口),该新类型只有一个方法,因此引导开发人员完成实例初始化过程。流畅构建器可以在最后省略build()方法;例如,当设置最后一个字段时,组装过程就结束了。

不幸的是,流畅构建器的直接实现非常冗长:

public record NameAge(String firstName, String lastName, Option<String> middleName, int age) {
    public static NameAgeBuilderStage1 builder() {
        return new NameAgeBuilder();
    }

    public static class NameAgeBuilder implements NameAgeBuilderStage1,
                                                  NameAgeBuilderStage2,
                                                  NameAgeBuilderStage3,
                                                  NameAgeBuilderStage4 {
        private String firstName;
        private String lastName;
        private Option<String> middleName;

        @Override
        public NameAgeBuilderStage2 firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }

        @Override
        public NameAgeBuilderStage3 lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }

        @Override
        public NameAgeBuilderStage4 middleName(Option<String> middleName) {
            this.middleName = middleName;
            return this;
        }

        @Override
        public NameAge age(int age) {
            return new NameAge(firstName, lastName, middleName, age);
        }
    }

    public interface NameAgeBuilderStage1 {
        NameAgeBuilderStage2 firstName(String firstName);
    }

    public interface NameAgeBuilderStage2 {
        NameAgeBuilderStage3 lastName(String lastName);
    }

    public interface NameAgeBuilderStage3 {
        NameAgeBuilderStage4 middleName(Option<String> middleName);
    }

    public interface NameAgeBuilderStage4 {
        NameAge age(int age);
    }
}


它也不够安全,因为仍然可以将返回的接口强制转换为NameAgeBuilder,然后调用age()方法,从而获得一个不完整的对象。

我们可能会注意到每个接口都是典型的功能接口,只有一个方法在内。有了这个想法,我们可以将上面的代码重写成以下形式:

public record NameAge(String firstName, String lastName, Option<String> middleName, int age) {
    static NameAgeBuilderStage1 builder() {
        return firstName -> lastName -> middleName -> age -> new NameAge(firstName, lastName, middleName, age);
    }

    public interface NameAgeBuilderStage1 {
        NameAgeBuilderStage2 firstName(String firstName);
    }

    public interface NameAgeBuilderStage2 {
        NameAgeBuilderStage3 lastName(String lastName);
    }

    public interface NameAgeBuilderStage3 {
        NameAgeBuilderStage4 middleName(Option<String> middleName);
    }

    public interface NameAgeBuilderStage4 {
        NameAge age(int age);
    }
}


除了更加简洁外,此版本不容易受到(即使是 hacky 的)过早创建对象的影响。

减少实现

虽然默认方法是为了在不破坏现有实现的情况下扩展现有接口而创建的,但这并不是它们唯一的用途。

很长一段时间以来,如果我们需要同一个接口的多个实现,在这些实现中有许多共享的代码,避免代码重复的唯一方法就是创建一个抽象类,并将这些实现从中继承。尽管这避免了代码重复,但这个解决方案相对冗长,导致了不必要的耦合。抽象类是一个纯粹的技术实体,在应用领域中没有相应的部分。

有了默认方法,抽象类就不再必要了;通用功能可以直接写在接口中,减少样板代码,消除耦合,提高可维护性。

但是如果我们更进一步呢?有时可以只使用非常少的具体实现方法来表达所有必要的功能。理想情况下,只需一个。这使得实现类非常紧凑,易于理解和维护。例如,让我们实现Maybe<T>单子(又是Optional<T>/Option<T>的另一个名字)。无论我们打算实现多丰富和多样化的API,都可以称为对单个方法的调用,我们将其称为fold()

<R> R fold(Supplier<? extends R> nothingMapper, Function<? super T, ? extends R> justMapper)


此方法接受两个函数;一个在值存在时调用,另一个在值缺失时调用。应用的结果就是实现方法的结果。有了此方法,我们可以实现map()flatMap()

default <U> Maybe<U> map(Function<? super T, U> mapper) {
    return fold(Maybe::nothing, t -> just(mapper.apply(t)));
}

default <U> Maybe<U> flatMap(Function<? super T, Maybe<U>> mapper) {
    return fold(Maybe::nothing, mapper);
}


这些实现通用且适用于两种变体。请注意,由于我们有确切的两个实现,因此将接口设置为封闭的是完全有意义的。为了进一步减少样板代码,我们可以使用记录:

public sealed interface Maybe<T> {
    default <U> Maybe<U> map(Function<? super T, U> mapper) {
        return fold(Maybe::nothing, t -> just(mapper.apply(t)));
    }

    default <U> Maybe<U> flatMap(Function<? super T, Maybe<U>> mapper) {
        return fold(Maybe::nothing, mapper);
    }

    <R> R fold(Supplier<? extends R> nothingMapper, Function<? super T, ? extends R> justMapper);

    static <T> Just<T> just(T value) {
        return new Just<>(value);
    }

    @SuppressWarnings("unchecked")
    static <T> Nothing<T> nothing() {
        return (Nothing<T>) Nothing.INSTANCE;
    }

    static <T> Maybe<T> maybe(T value) {
        return value == null ? nothing() : just(value);
    }

    record Just<T>(T value) implements Maybe<T> {
        public  <R> R fold(Supplier<? extends R> nothingMapper, Function<? super T, ? extends R> justMapper) {
            return justMapper.apply(value);
        }
    }

    record Nothing<T>() implements Maybe<T> {
        static final Nothing<?> INSTANCE = new Nothing<>();

        @Override
        public <R> R fold(Supplier<? extends R> nothingMapper, Function<? super T, ? extends R> justMapper) {
            return nothingMapper.get();
        }
    }
}


尽管这在演示中并非绝对必要,但此实现使用了共享常量来实现'Nothing',从而减少了分配。此实现的另一个有趣属性是它不使用if语句(也不使用三元操作符)来实现逻辑。这提高了性能,并使Java编译器能够进行更好的优化。

该实现的另一个有用属性是它方便进行模式匹配(与Java的'Optional'相比):

var result = switch (maybe) {
    case Just<String>(var value) -> value;
    case Nothing<String> nothing -> "Nothing";
};


但有时,甚至实现类也是不必要的。下面的示例展示了整个实现如何适应接口中(完整代码可在这里找到):

public interface ShortenedUrlRepository {
    default Promise<ShortenedUrl> create(ShortenedUrl shortenedUrl) {
        return QRY."INSERT INTO shortenedurl (\{template().fieldNames()}) VALUES (\{template().fieldValues(shortenedUrl)}) RETURNING *"
            .in(db())
            .asSingle(template());
    }

    default Promise<ShortenedUrl> read(String id) {
        return QRY."SELECT * FROM shortenedurl WHERE id = \{id}"
            .in(db())
            .asSingle(template());
    }

    default Promise<Unit> delete(String id) {
        return QRY."DELETE FROM shortenedurl WHERE id = \{id}"
            .in(db())
            .asUnit();
    }

    DbEnv db();
}


将此接口转换为工作实例所需的全部工作只是提供环境的一个实例。例如,像这里一样:

var dbEnv = DbEnv.with(dbEnvConfig);

ShortenedUrlRepository repository = () -> dbEnv;


这种方法有时会导致代码过于简洁,有时需要编写更详细的版本以保留上下文。我认为这在Java代码中是相当不寻常的性质,因为Java经常因冗长而受到指责。

实用...接口?

很长一段时间内,实用性(以及常量)接口都是不可行的。也许主要原因是这样的接口可以实现,并且常量以及实用函数会成为实现的(不必要的)一部分。

但是有了封闭接口,可以以类似于怎样阻止实用类的实例化的方式解决这个问题:

public sealed interface Utility {
    ...

    record unused() implements Utility {}
}


乍一看,这种方式似乎没什么特别的意义。然而,使用接口可以消除每个方法和/或常量的可见性修饰符的需要。这反过来又减少了语法噪音的数量,这对类是强制性的,但对接口来说是多余的,因为它们的所有成员都是公共的。

接口和私有记录

这两种构造的结合使得以“无类OO”样式方便地编写代码成为可能,同时可以强制“按接口编码”,同时减少样板文件。例如:

public interface ContentType {
    String headerText();
    ContentCategory category();

    static ContentType custom(String headerText, ContentCategory category) {
        record contentType(String headerText, ContentCategory category) implements ContentType {}

        return new contentType(headerText, category);
    }
}


私有记录有两个作用:

  • 它使得实现的使用完全在控制之下。不可能直接实例化,只能通过静态工厂方法实例化。
  • 将实现保持接口附近,简化支持、扩展和维护。

注意接口并不是封闭的,所以可以做如下操作:

public enum CommonContentTypes implements ContentType {
    TEXT_PLAIN("text/plain; charset=UTF-8", ContentCategory.PLAIN_TEXT),
    APPLICATION_JSON("application/json; charset=UTF-8", ContentCategory.JSON),
    ;
    private final String headerText;
    private final ContentCategory category;

    CommonContentTypes(String headerText, ContentCategory category) {
        this.headerText = headerText;
        this.category = category;
    }

    @Override
    public String headerText() {
        return headerText;
    }

    @Override
    public ContentCategory category() {
        return category;
    }
}


结论

接口是Java的一个强大特性,通常被低估和未充分利用。本文试图阐明利用它们的力量以及获得清晰、有表现力、简明但可读性强的代码的可能方法。

推荐阅读: 银行常识汇总

本文链接: 释放Java接口的威力