在前不久发的「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 封装,还是蛮简略的,只不过有两个小问题:
consume()
字太多,写起来麻烦。如果改名的话,do
很适合,惋惜是关键字 …… 不如改成act
好了;- 链接调用往往是用在表达式中,返回有个返回值,所以得加个
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
对象是否可行呢?—— 的确可行。然而一方面须要持续应用泛型来束缚 consumer
和 mapper
,另一方面,须要在外部进行强制的类型转换,还得保障这个转换不会有问题。
实践上来说,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));}
输入后果正如预期:
helloworld
结语
为了解决链式调用的问题,咱们在上一篇文章中钻研了扩大办法,钻研得有点“适度”。这次回归根源,就解决链式调用。
钻研过程中如果有想法,不妨一试。如果发现 JDK 中有相似的解决形式,不防去看看源码 —— 毕竟 OpenJDK 是开源的!
还有一点,Java 泛型的类型擦除个性有时候的确会带来不便,但也有些时候是真的不便!