我平时次要应用 C#、JavaScript 和 TypeScript。然而最近因为某些起因须要用 Java,不得不再捡起来。回想起来,最近一次应用 Java 写残缺的应用程序时,Java 还是 1.4 版本。

这么多年过来,Java 的确有不少改良,像 Stream,var 之类的,我还是晓得一些。但用起来感觉还是有点缚手缚脚,施展不开的感觉。这必定是和语法习惯无关,但也不乏 Java 本身的起因。比方,我在 C# 中罕用的「扩大办法」在 Java 中就没有。

C# 的「扩大办法」的语法,能够在不批改类定义,也不继承类的状况下,为某些类及其子类增加公开办法。这些类的对象在调用扩大办法的时候,就跟调用类本人申明的办法一样,毫不违和。为了了解这种语法,上面给一个示例(不论你会不会 C#,只有有 OOP 根底应该都能看明确的示例)

using System;// 定义一个 Person 类,没有定义方法public class Person {    public string Name { get; set; }}// 上面这个类中定义扩大办法 PrintName()public static class PersonExtensions {    public static void PrintName(this Person person) {        Console.WriteLine($"Person name: {person.Name}");    }}// 主程序,提供 Main 入口,并在这里应用扩大办法public class Program {    public static void Main(string[] args) {        Person person = new Person { Name = "John" };        person.PrintName();    }}

有 OOP 根底的开发者能够从惯例常识判断:Person 类没有定义方法,应该不能调用 person.PrintName()。但既然 PrintName() 是一个静态方法,那么应该能够应用 PersonExtensions.PrintName(person)

确实,如果尝试了 PersonExtensions.PrintName(person) 这样的语句就会发现,这句话也能够正确运行。然而留神到 PrintName() 申明的第一个参数加了 this 润饰。这是 C# 特有的扩大办法语法,编译器会辨认扩大办法,而后将 person.PrintName() 翻译成 PersonExtensions.PrintName(person) 来调用 —— 这就是一个语法糖。

C# 在 2007 年公布的 3.0 版本中就增加了「扩大办法」这一语法,那曾经是 10 多年前的事件了,不晓得 Java 什么时候能反对呢。不过要说 Java 不反对扩大办法,也不全对。毕竟存在一个叫 Manifold 的东东,以 Java 编译器插件的模式提供了扩大办法个性,在 IDEA 中须要插件反对,用起来和 C# 的感觉差不多 —— 遗憾的是每月 $19.9 租用费间接把我劝退。

然而程序员往往会有一种不撞南墙不回头的执念,难道就没有近似的办法来解决这个问题吗?

剖析苦楚之源

须要应用扩大办法,其实次要起因就一点:想扩大 SDK 中的类,然而又不想用动态调用模式。尤其是须要链式调用的时候,静态方法真的不好用。还是拿 Person 来举例(这回是 Java 代码):

class Person {    private String name;    public Person(String name) { this.name = name; }    public String getName() { return name;}}class PersonExtension {    public static Person talk(Person person) { ... }    public static Person walk(Person person) { ... }    public static Person eat(Person person) { ... }    public static Person sleep(Person person) { ... }}

业务过程是:谈妥了进来吃饭,再回来睡觉。用链接调用应该是:

person.talk().walk().eat().walk().sleep()
留神:别说改 Person,咱们假如它是第三方 SDK 封装好的,PersonExtension 才是咱们写的业务解决类

但显然不能这么调用,按 PersonExtension 中的办法,应该这么调用:

sleep(walk(eat(walk(talk(person)))));

苦楚吧?!

苦楚之余来剖析下咱们以后的需要:

  1. 链式调用
  2. 没别的了……

链式调用的典型利用场景

既然须要的就是链式调用,那咱们来想一想链式调用的典型利用场景:建造者模式。如果咱们用建造式模式来写 Extension 类,应用时候把原对象封装起来,就能够实现链式调用了么?

class PersonExtension {    private final Person person;    PersonExtension(Person person) {        this.person = person;    }    public PersonExtension walk() {        out.println(person.getName() + ":walk");        return this;    }    public PersonExtension talk() {        out.println(person.getName() + ":talk");        return this;    }    public PersonExtension eat() {        out.println(person.getName() + ":eat");        return this;    }    public PersonExtension sleep() {        out.println(person.getName() + ":sleep");        return this;    }}

用起来很不便:

new PersonExtension(person).talk().walk().eat().walk().sleep();

扩大到个别状况

如果到此为止,这篇博文就太水了。

咱们绕了个弯解决了链式调用的问题,然而人心总是不容易失去满足,一个新的要求呈现了:扩大办法能够写无数个扩大类,有没有方法让这无数个类中定义的办法连贯调用上来呢?

你看,在以后的封装类中,咱们是没方法调用第二个封装类的办法的。然而,如果咱们能从以后封装类转换到第二个封装类,不是就能够了吗?

这个转换过程,大略过程是拿到以后封装的对象(如 person),把它作为参数传递下一个封装类的构造函数,结构这个类的对象,把它作为调用主体持续写下去……这样一 来,咱们须要有一个约定:

  1. 扩大类必须提供一个可传入封装对象类型参数的构造函数;
  2. 扩大类必须实现转换到另一个扩大类的办法

在程序中,约定通常会用接口来形容,所以这里定义一个接口:

public interface Extension<T> {    <E extends Extension<T>> E to(Class<E> type);}

这个接口的意思很明确:

  • 被封装的对象类型是 T
  • to 提供从以后 Extension 对象换到另一个实现了 Extension<T> 接口的对象下来

能够设想,这个 to 要干的事件就是去找 E 的构造函数,用它结构一个 E 的对象。这个构造函数须要定义了有惟一参数,且参数类型是 T 或其父类型(可传入)。这样在结构 E 对象的时候能力把以后扩大对象中封装的 T 对象传递到 E 对象中去。

如果找不到适合的构造函数,或者结构时产生谬误,应该抛出异样,用来形容类型 E 不正确。既然 E 是一个类型参数,无妨就应用 IllegalArgumentException 好了。此外,少数扩大类的 to 行为应该是一样的,能够用默认办法提供反对。另外,还能够给 Extension 加一个动态的 create() 办法来代替应用 new 创立扩大类对象 —— 让所有都从 Extension 开始。

残缺的 Extension 来了:

public interface Extension<T> {    /**     * 给一个被封装的对象 value,结构一个 E 类的对象来封装它。     */    @SuppressWarnings("unchecked")    static <T, E extends Extension<T>> E create(T value, Class<E> extensionType)        throws IllegalArgumentException {        Constructor<T> cstr = (Constructor<T>) Arrays            .stream(extensionType.getConstructors())            // 在构造方法中找到符合要求的那一个            .filter(c -> c.getParameterCount() == 1                && c.getParameterTypes()[0].isAssignableFrom(value.getClass())            )            .findFirst()            .orElse(null);        try {            // 如果没找到适合的构造函数 (cstr == null),或者其余状况下出错            // 就抛出 IllegalArgumentException            return (E) Objects.requireNonNull(cstr).newInstance(value);        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {            throw new IllegalArgumentException("invalid implementation of Extension", e);        }    }    // 为了给拿到以后封装的对象给 wrapTo 用,必须要 getValue() 接口    T getValue();    // wrapTo 接口及其默认实现    default <E extends Extension<T>> E to(Class<E> type) throws IllegalArgumentException {        return create(getValue(), type);    }}

当初把下面的 PersonExtension 拆成两个扩大类来作演示:

class PersonExt1 implements Extension<Person> {    private final Person person;    PersonExt1(Person person) { this.person = person; }    @Override    public Person getValue() { return person; }    public PersonExt1 walk() {        out.println(person.getName() + ":walk");        return this;    }    public PersonExt1 talk() {        out.println(person.getName() + ":talk");        return this;    }}class PersonExt2 implements Extension<Person> {    private final Person person;    public PersonExt2(Person person) { this.person = person; }    @Override    public Person getValue() { return person; }    public PersonExt2 eat() {        out.println(person.getName() + ":eat");        return this;    }    public PersonExt2 sleep() {        out.println(person.getName() + ":sleep");        return this;    }}

调用示例:

public class App {    public static void main(String[] args) throws Exception {        Person person = new Person("James");        Extension.create(person, PersonExt1.class)            .talk().walk()            .to(PersonExt2.class).eat()            .to(PersonExt1.class).walk()            .to(PersonExt2.class).sleep();    }}

结语

总的来说,在没有语法反对的根底上要实现扩大办法,基本思路就是

  1. 意识到指标对象上调用的所谓的扩大办法,理论是静态方法调用的语法糖。该静态方法的第一个参数是指标对象。
  2. 把静态方法的第一参数拿进去,封装到扩大类中,同时把静态方法改为实例办法。这样来防止调用时传入指标对象。
  3. 如果须要链式调用,须要通过接口约定并提供一些工具函数来辅助指标对象穿梭于各扩大类之中。

本文次要是尝试在没有语法/编译器反对的状况下在 Java 中模块 C# 的扩大办法。尽管有后果,但在理论应用中并不见得就好用,请读者在理论开发时留神剖析,酌情思考。