Java 缺失的特性:扩展方法
什么是扩展方法
扩展方法,就是能够向现有类型直接“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改现有类型。调用扩展方法的时候,与调用在类型中实际定义的方法相比没有明显的差异。为什么需要扩展方法
考虑要实现这样的功能:从 Redis 取出包含多个商品ID的字符串后(每个商品ID使用英文逗号分隔),先对商品ID进行去重(并能够维持元素的顺序),最后再使用英文逗号将各个商品ID进行连接。传统写法:// "123,456,123,789"String str = redisService.get(someKey)
String itemIdStrs = String.join(",", new LinkedHashSet<>(Arrays.asList(str.split(","))));
使用 Stream 写法:
String itemIdStrs = Arrays.stream(str.split(",")).distinct().collect(Collectors.joining(","));
假设在 Java 中能实现扩展方法,并且我们为数组添加了扩展方法 toList (将数组变为 List ),为 List 添加了扩展方法 toSet (将 List 变为 LinkedHashSet ),为 Collection 添加了扩展方法 join (将集合中元素的字符串形式使用给定的连接符进行连接),那我们将可以这样写代码:
String itemIdStrs = str.split(",").toList().toSet().join(",");
相信此刻你已经有了为什么需要扩展方法的答案:
- 可以对现有的类库,进行 直接 增强,而不是使用工具类
- 相比使用工具类,使用类型本身的方法写代码更流畅更舒适
- 代码更容易阅读,因为是链式调用,而不是用静态方法套娃
在 Java 中怎么实现扩展方法
我们先来问问最近大火的 ChatGPT:如果你的项目中使用了 Lombok,需要把 Lombok 也加入 annotationProcessorPaths:<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">...<properties><manifold.version>2022.1.35</manifold.version></properties><dependencies><dependency><groupId>systems.manifold</groupId><artifactId>manifold-ext</artifactId><version>${manifold.version}</version></dependency>...</dependencies><!--Add the -Xplugin:Manifold argument for the javac compiler--><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>8</source><target>8</target><encoding>UTF-8</encoding><compilerArgs><arg>-Xplugin:Manifold no-bootstrap</arg></compilerArgs><annotationProcessorPaths><path><groupId>systems.manifold</groupId><artifactId>manifold-ext</artifactId><version>${manifold.version}</version></path></annotationProcessorPaths></configuration></plugin></plugins></build></project>
编写扩展方法 JDK 中, String 的 split 方法,使用的是字符串作为参数,即 String[] split(String) 。我们现在来为 String 添加一个扩展方法 String[] split(char) :按给定的字符进行分割。 基于 Manifold,编写扩展方法:<annotationProcessorPaths><path><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></path><path><groupId>systems.manifold</groupId><artifactId>manifold-ext</artifactId><version>${manifold.version}</version></path></annotationProcessorPaths>
可以发现本质上还是工具类的静态方法,但是有一些要求:package com.alibaba.zhiye.extensions.java.lang.String;import manifold.ext.rt.api.Extension;import manifold.ext.rt.api.This;import org.apache.commons.lang3.StringUtils;/*** String 的扩展方法*/@Extensionpublic final class StringExt {public static String[] split(@This String str, char separator) {return StringUtils.split(str, separator);}}
- 工具类需要使用 Manifold 的 @Extension 注解
- 静态方法中,目标类型的参数,需要使用 @This 注解
- 工具类所在的包名,需要以 extensions.目标类型全限定类名 结尾
然后调用的时候:package com.alibaba.zhiye.extensions.java.util.Collection;import manifold.ext.rt.api.Extension;import manifold.ext.rt.api.This;import java.util.Collection;/*** Collection 的扩展方法*/@Extensionpublic final class CollectionExt {public static boolean isNullOrEmpty(@This Collection<?> coll) {return coll == null || coll.isEmpty();}}
java.lang.NullPointerException ,Goodbye,再也不见~ 数组扩展方法 JDK 中,数组并没有一个具体的对应类型,那为数组定义的扩展类,要放到什么包中呢?看下 ManArrayExt 的源码,发现 Manifold 专门提供了一个类 manifold.rt.api.Array ,用来表示数组。比如 ManArrayExt 中为数组提供的 toList 的方法:List<String> list = getSomeNullableList();// list 如果为 null 会进入 if 块,而不会触发空指针异常if (list.isNullOrEmpty()) {// TODO}
那么任何对象,都将拥有 asOpt() 方法。 相比于之前的需要包装一下的不自然:package com.alibaba.zhiye.extensions.java.lang.Object;import manifold.ext.rt.api.Extension;import manifold.ext.rt.api.Self;import manifold.ext.rt.api.This;import java.util.Optional;/*** Object 的扩展方法*/@Extensionpublic final class ObjectExt {public static Optional<@Self Object> asOpt(@This Object obj) {return Optional.ofNullable(obj);}}
Optional.ofNullable(someObj).filter(someFilter).map(someMapper).orElseGet(someSupplier);
你现在可以自然而然的使用 Optional :
someObj.asOpt().filter(someFilter).map(someMapper).orElseGet(someSupplier);
当然, Object 是所有的类的父类,这样做是否合适,还是需要谨慎的思考一下。 扩展静态方法 我们都知道 Java9 给集合添加了工厂方法:
是不是很眼馋?因为如果用的不是 Java9 及以上版本(Java8:直接报我身份证就行),你就得用 Guava 之类的库 —— 然而 ImmutableList.of 用起来终究是比不上 List.of 这样的正统来的自然。 没关系,Manifold 说:“无所谓,我会出手”。基于 Manifold 扩展静态方法,就是在扩展类的静态方法上,也加上 @Extension :List<String> list = List.of("a", "b", "c");Set<String> set = Set.of("a", "b", "c");Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3);
然后你就可以欺骗自己已经用上了 Java8 之后的版本 —— 你发任你发,我用 Java8。 BTW,因为 Object 是所有类的父类,如果你给 Object 添加静态扩展方法,那么意味着你可以在任何地方直接访问到这个静态方法,而不需要 import —— 恭喜你,解锁了 “顶级函数”。 建议package com.alibaba.aladdin.app.extensions.java.util.List;import manifold.ext.rt.api.Extension;import manifold.ext.rt.api.This;import java.util.Arrays;import java.util.Collections;import java.util.List;/*** List 扩展方法*/@Extensionpublic final class ListExt {/*** 返回只包含一个元素的不可变 List*/@Extensionpublic static <E> List<E> of(E element) {return Collections.singletonList(element);}/*** 返回包含多个元素的不可变 List*/@Extension@SafeVarargspublic static <E> List<E> of(E... elements) {return Collections.unmodifiableList(Arrays.asList(elements));}}
关于 Manifold
我从 2019 年开始关注 Manifold,那时候 Manifold IDEA 插件还是收费的,所以当时只是做了简单的尝试。最近再看,IDEA 插件已经完全免费,所以迫不及待地想要物尽其用。目前我已经在一个项目中使用了 Manifold 来实现扩展方法的功能 —— 当事人表示非常上瘾,已经离不开了。如果你有使用上的建议和疑问,欢迎和我一起讨论。谨慎添加扩展方法
如果决定在项目中使用 Manifold 实现扩展方法,那么我们 一定要做到 “管住自己的手” 。 首先,就是上文说的,给 Object 或者其他在项目中使用非常广泛的类添加扩展方法,一定要非常的慎重,最好是要和项目组的同学一起讨论,让大家一起决定,否则很容易让人迷惑。 另外,如果要给某个类添加扩展方法,一定要先认真思考一个问题:“这个方法的逻辑是不是在这个类的职责范围内,是否有掺杂业务自定义逻辑”。例如下面这个方法(判断给定的字符串是不是一个合法的参数):很明显, isValidParam 不是 String 这个类的职责范围,应该把 isValidParam 继续放在 XxxBizUtils 里面。当然,如果你把方法名改成 isNotBlankAndNotEqualsIgnoreCaseNullLiteral ,那是可以的 :) —— 不过劝你别这么做,容易被打。public static boolean isValidParam(String str) {return StringUtils.isNotBlank(str) && !"null".equalsIgnoreCase(str);}