头图

给 Java 造个轮子 - Chain

在前不久发的「Java 中模拟 C# 的扩展方法」一文中给 Java 模拟了扩展方法。但毕竟没有语法支持,使用起来还是有诸多不便,尤其是需要不断交错使用不同类中实现的“扩展方法”时,切换对象非常繁琐。前文也提到,之所以想到研究“扩展方法”其实只是为了“链式调用”。

那么,为什么不直接从原始需求出发,只解决链式调用的问题,而不去考虑更宽泛的扩展方法呢?前文研究过通过 Builder 模式来实现链式调用,这种方式需要自己定义扩展方法类(也就是 Builder),仍然比较繁琐。

Chain 雏形

链式调用的主要特点就是使用了某个对象之后,可以继续使用该对象 …… 一直使用下去。你看,这里就两件事:一是提供一个对象;二是使用这个对象 —— 这不就是 Supplier 和 Consumer 吗?Java 在 java.util.function 包中正好提供了同名的两个函数式接口。这样一来,我们可以定义一个 Chain 类,从一个 Supplier 开始,不断的“消费”它,这样一个简单的 Chain 雏形就出来了:

public class Chain<T> {
    private final T value;

    public Chain(Supplier<? extends T> supplier) {
        this.value = supplier.get();
    }

    public Chain<T> consume(Consumer<? super T> consumer) {
        consumer.accept(this.value);
        return this;
    }
}

现在,假如我们有一个 Person 类,它有一些行为方法:

class Person {
    public void talk() { }
    public void walk(String target) { }
    public void eat() { }
    public void sleep() { }
}

还是前文中那个业务场景:谈妥了,出去,吃饭,回来,睡觉。非链试调用是这样的:

public static void main(String[] args) {
    var person = new Person();
    person.talk();
    person.walk("饭店");
    person.eat();
    person.walk("家");
    person.sleep();
}

如果用 Chain 串起来就是:

public static void main(String[] args) {
    new Chain<>(Person::new).consume(Person::talk)
        .consume(p -> p.walk("饭店"))
        .consume(Person::eat)
        .consume(p -> p.walk("家"))
        .consume(Person::sleep);
}

上面已经完成了 Chain 封装,还是蛮简单的,只不过有两个小问题:

  1. consume() 字太多,写起来麻烦。如果改名的话,do 很合适,可惜是关键字 …… 不如改成 act 好了;
  2. 链接调用往往是用在表达式中,需要有个返回值,所以得加个 Chain::getValue()

完善 Chain

实际上,链式调用过程中,也不一定就只是“消费”,有可能还需要“转换”,用程序员的话来说,就是 map()—— 将当前对象作为参数传入,计算完成之后得到另一个对象。可能大家在 java stream 中用到 map() 比较多,不过这里的场景更像 Optional::map

来看看 Optional::map源代码

public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

可以看出来这个 map() 的逻辑很简单,就是把 Function 运行的的结果再封装成一个 Optional 对象。我们在 Chain 中也可以这么干:

public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
    return new Chain<>(() -> mapper.apply(value));
}

写到这里发现,使用 Supplier 的思路虽然没错,但要直接从“值”构造 Chain 对象还挺不容易的 —— 当然可以加一个构造函数的重载来解决这个问题,但我想像 Optional 那样写两个静态方法来实现,同时隐藏构造函数。修改后完整的 Chain 如下:

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class Chain<T> {
    private final T value;

    public static <T> Chain<T> of(T value) {
        return new Chain<>(value);
    }

    public static <T> Chain<T> from(Supplier<T> supplier) {
        return new Chain<>(supplier.get());
    }

    private Chain(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public Chain<T> act(Consumer<? super T> consumer) {
        consumer.accept(this.value);
        return this;
    }

    public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
        return Chain.of(mapper.apply(value));
    }
}

继续改造 Chain

map() 总是会返回一个新的 Chain 对象。如果某次处理中存在很多个 map 步骤,那就会产生很多个 Chain 对象。能不能在一个 Chain 对象中解决呢?

定义 Chain 的时候用到了泛型,而泛型类型在编译后就会被擦除掉,和我们直接把 value 定义成 Object 没多大区别。既然如此,map() 的时候,直接把 value 给换掉,而不是产生新的 Chain 对象是否可行呢?—— 确实可行。但是一方面需要继续使用泛型来约束 consumermapper,另一方面,需要在内部进行强制的类型转换,还得保证这个转换不会有问题。

理论上来说,Chain 处理的是链式调用,一环扣一环,而每一环的结果都保存在 value 中用于下一环的开始。因此在泛型约束下,不管怎么变化都是不会出问题的。理论可行,不如实践一下:

// 因为存在大量的类型转换(逻辑确认可行),需要忽略掉相关警告
@SuppressWarnings("unchecked")
public class Chain<T> {
    // 把 value 声明为 Object 类型,以便引用各种类型的值
    // 同时去掉 final 修饰,使之可变
    private Object value;

    public static <T> Chain<T> of(T value) {
        return new Chain<>(value);
    }

    public static <T> Chain<T> from(Supplier<T> supplier) {
        return new Chain<>(supplier.get());
    }

    private Chain(T value) {
        this.value = value;
    }

    public T getValue() {
        // 使用到 value 的地方都需要把 value 转换为 Chain<> 的泛型参数类型,下同
        return (T) value;
    }

    public Chain<T> act(Consumer<? super T> consumer) {
        consumer.accept((T) this.value);
        return this;
    }

    public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
        // mapper 的计算结果无所谓是什么类型都可以给 Object 类型的 value 赋值
        this.value = mapper.apply((T) value);
        // 返回的 Chain 虽然还是自己(就这个对象),但是泛型参数得换成 U 了
        // 换了类型之后,后序的操作才会基于 U 类型来进行
        return (Chain<U>) this;
    }
}

最后那句类型转换 (Chain<U>) this 很是灵性,Java 中可以这么干(因为有类型擦除),C# 中无论如何都做不到!

再写段代码来试验一下:

public static void main(String[] args) {
    // 注意:String 的操作会产生新的 String 对象,所以要用 map
    Chain.of("     Hello World  ")
        .map(String::trim)
        .map(String::toLowerCase)
        // ↓ 把 String 拆分成 String[],这里转换了不相容类型
        .map(s ->s.split("\s+"))
        // ↓ 消费这个 String[],依次打印出来
        .act(ss -> Arrays.stream(ss).forEach(System.out::println));
}

输出结果正如预期:

hello
world

结语

为了解决链式调用的问题,我们在上一篇文章中研究了扩展方法,研究得有点“过度”。这次回归本源,就处理链式调用。

研究过程中如果有想法,不妨一试。如果发现 JDK 中有类似的处理方式,不防去看看源码 —— 毕竟 OpenJDK 是开源的!

还有一点,Java 泛型的类型擦除特性有时候确实会带来不便,但也有些时候是真的方便!


边城客栈
全栈技术专栏,公众号「边城客栈」,[链接]

一路从后端走来,终于走在了前端!

56.2k 声望
26.5k 粉丝
0 条评论
推荐阅读
2022,二着二着又混过一年
收到思否小姐姐的活动提醒,才发觉又到了年底,该写“总结”了。说起总结,总有些倦——每天工作要写日报、项目上要写周报、月底要写月报、季度还有季总结,当然还有半年总结和年终总结……一年大约是 250 个工作日、50...

边城6阅读 790评论 2

封面图
刨根问底 Redis, 面试过程真好使
充满寒气的互联网如何在面试中脱颖而出,平时积累很重要,八股文更不能少!下面带来的这篇 Redis 问答希望能够在你的 offer 上增添一把🔥。

菜农曰17阅读 1k

封面图
PHP转Go实践:xjson解析神器「开源工具集」
我和劲仔都是PHP转Go,身边越来越多做PHP的朋友也逐渐在用Go进行重构,重构过程中,会发现php的json解析操作(系列化与反序列化)是真的香,弱类型语言的各种隐式类型转换,很大程度的减低了程序的复杂度。

王中阳Go10阅读 2.1k评论 3

封面图
万字详解,吃透 MongoDB!
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常...

JavaGuide5阅读 885

封面图
计算机网络连环炮40问
本文已经收录到Github仓库,该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点,欢迎star~

程序员大彬8阅读 1.1k

与RabbitMQ有关的一些知识
工作中用过一段时间的Kafka,不过主要还是RabbitMQ用的多一些。今天主要来讲讲与RabbitMQ相关的一些知识。一些基本概念,以及实际使用场景及一些注意事项。

lpe2348阅读 1.9k

封面图
Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 2.3k评论 2

封面图

一路从后端走来,终于走在了前端!

56.2k 声望
26.5k 粉丝
宣传栏