Redian新闻
>
Java 缺失的特性:扩展方法

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:

好吧,ChatGPT 认为 Java 里面的扩展方法就是通过工具类提供的静态方法 :)。所以接下来我将介绍一种全新的黑科技:

Manifold(https://github.com/manifold-systems/manifold)

准备条件

Manifold 的原理和 Lombok 是一样的,也是在编译期间通过注解处理器进行处理。所以要在 IDEA 中正确使用 Manifold,需要安装 Manifold IDEA 的插件:
然后再在项目 pom 的 maven-compiler-plugin 中加入 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>
如果你的项目中使用了 Lombok,需要把 Lombok 也加入 annotationProcessorPaths:
<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>

编写扩展方法

JDK 中,String 的 split 方法,使用的是字符串作为参数,即 String[] split(String)。我们现在来为 String 添加一个扩展方法 String[] split(char):按给定的字符进行分割。
基于 Manifold,编写扩展方法:
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); }}

可以发现本质上还是工具类的静态方法,但是有一些要求:

  1. 工具类需要使用 Manifold 的 @Extension 注解

  1. 静态方法中,目标类型的参数,需要使用 @This 注解

  1. 工具类所在的包名,需要以 extensions.目标类型全限定类名 结尾

—— 用过 C# 的同学应该会会心一笑,这就是模仿的 C# 的扩展方法。

关于第 3 点,之所以有这个要求,是因为 Manifold 希望能快速找到项目中的扩展方法,避免对项目中所有的类进行注解扫描,提升处理的效率。
具备了扩展方法的能力,现在我们就可以这样调用了:
Amazing!而且你可以发现,System.out.println(numStrs.toString()) 打印的居然是数组对象的字符串形式 —— 而不是数组对象的地址。查看反编译后的 App.class,发现是将扩展方法的调用,替换为静态方法调用:
而数组的 toString 方法,使用的是 Manifold 为数组定义的扩展方法 ManArrayExt.toString(@This Object array)

[Ljava.lang.String;@511d50c0 什么的,Goodbye,再也不见~

因为是在编译期将扩展方法的调用替换为静态方法调用,所以使用 Manifold 的扩展方法,即使调用方法的对象是 null 也没有问题,因为处理后的代码是把 null 作为参数传递到对应的静态方法。比如我们对 Collection 进行扩展:
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(); }}
然后调用的时候:
List<String> list = getSomeNullableList();
// list 如果为 null 会进入 if 块,而不会触发空指针异常if (list.isNullOrEmpty()) { // TODO}
java.lang.NullPointerException,Goodbye,再也不见~

数组扩展方法

JDK 中,数组并没有一个具体的对应类型,那为数组定义的扩展类,要放到什么包中呢?看下 ManArrayExt 的源码,发现 Manifold 专门提供了一个类 manifold.rt.api.Array,用来表示数组。比如 ManArrayExt 中为数组提供的 toList 的方法:
我们看到 List<@Self(true) Object> 这样的写法:@Self 是用来表示被注解的值应该是什么类型,如果是 @Self,即 @Self(false),表示被注解的值和 @This 注解的值是同一个类型;@Self(true) 则表示是数组中元素的类型。
对于对象数组,我们可以看到 toList 方法返回的就是对应的 List<T>(T 为数组元素的类型):
但如果是原始类型数组,IDEA 指示的返回值是:
但是我用的是 Java 啊,擦除法泛型怎么可能拥有 List<char> 这么伟大的功能 —— 所以你只能用原生类型来接收这个返回值 :)
—— 许个愿:Project Valhalla 会在 Java21 中 GA。
我们经常在各个项目中看到,大家先把某个对象包装成 Optional,然后进行 filtermap 等。通过 @Self 的类型映射,你可以这样为 Object 加入一个非常实用的办法:
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); }}
那么任何对象,都将拥有 asOpt() 方法。
相比于之前的需要包装一下的不自然:
Optional.ofNullable(someObj).filter(someFilter).map(someMapper).orElseGet(someSupplier);

你现在可以自然而然的使用 Optional
someObj.asOpt().filter(someFilter).map(someMapper).orElseGet(someSupplier);

当然,Object 是所有的类的父类,这样做是否合适,还是需要谨慎的思考一下。

扩展静态方法

我们都知道 Java9 给集合添加了工厂方法:
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);
是不是很眼馋?因为如果用的不是 Java9 及以上版本(Java8:直接报我身份证就行),你就得用 Guava 之类的库 —— 然而 ImmutableList.of 用起来终究是比不上 List.of 这样的正统来的自然。
没关系,Manifold 说:“无所谓,我会出手”。基于 Manifold 扩展静态方法,就是在扩展类的静态方法上,也加上 @Extension
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 */ @Extension public static <E> List<E> of(E element) { return Collections.singletonList(element); }
/** * 返回包含多个元素的不可变 List */ @Extension @SafeVarargs public static <E> List<E> of(E... elements) { return Collections.unmodifiableList(Arrays.asList(elements)); }}
然后你就可以欺骗自己已经用上了 Java8 之后的版本 —— 你发任你发,我用 Java8。
BTW,因为 Object 是所有类的父类,如果你给 Object 添加静态扩展方法,那么意味着你可以在任何地方直接访问到这个静态方法,而不需要 import —— 恭喜你,解锁了 “顶级函数”。

建议

关于 Manifold

我从 2019 年开始关注 Manifold,那时候 Manifold IDEA 插件还是收费的,所以当时只是做了简单的尝试。最近再看,IDEA 插件已经完全免费,所以迫不及待地想要物尽其用。目前我已经在一个项目中使用了 Manifold 来实现扩展方法的功能 —— 当事人表示非常上瘾,已经离不开了。如果你有使用上的建议和疑问,欢迎和我一起讨论。

谨慎添加扩展方法

如果决定在项目中使用 Manifold 实现扩展方法,那么我们一定要做到 “管住自己的手”
首先,就是上文说的,给 Object 或者其他在项目中使用非常广泛的类添加扩展方法,一定要非常的慎重,最好是要和项目组的同学一起讨论,让大家一起决定,否则很容易让人迷惑。
另外,如果要给某个类添加扩展方法,一定要先认真思考一个问题:“这个方法的逻辑是不是在这个类的职责范围内,是否有掺杂业务自定义逻辑”。例如下面这个方法(判断给定的字符串是不是一个合法的参数):
public static boolean isValidParam(String str) {    return StringUtils.isNotBlank(str) && !"null".equalsIgnoreCase(str);}
很明显,isValidParam 不是 String 这个类的职责范围,应该把 isValidParam 继续放在 XxxBizUtils 里面。当然,如果你把方法名改成 isNotBlankAndNotEqualsIgnoreCaseNullLiteral,那是可以的 :) —— 不过劝你别这么做,容易被打。

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
天线宝宝长靴1W7,设计有种脑干缺失的美“大熊猫翻车名场面大赏!!” 有种脑干缺失的可爱...强势的母亲+缺失的父亲=病态失控的孩子,不幸的不只他…动力电池的2022:扩张、扩张,还是扩张机器学习:处理缺失值方法总结于布为教授:中国麻醉学科的发展成就与今后发展方向鲁迅、齐白石、张大千......央视《百年巨匠》天团,为孩子补齐缺失的人文艺术课!刚刚!CICC宣布:扩招40%EDA工具的发展方向猜想物理改变图像生成:扩散模型启发于热力学,比它速度快10倍的挑战者来自电动力学安省18岁美女大学生酒后乱性:表演后空翻后,被小帅哥"推倒"...算法解题:缺失的最小正整数观点丨张云飞:以新发展理念的内在统一推动发展方式绿色转型我们是否低估了奥密克戎的毒性:重复感染+重症+死亡+后遗症的六大质疑再次引蛇出洞?100美元的特斯拉值得买入,200美元的特斯拉呢?一诺对话藤校新女性:如何构建支持性的关系网多彩班芙行(1)-小镇的风情达人/自媒体做不好?我帮你总结了成功者的特性强势的母亲+缺失的父亲=病态失控的孩子,不幸的不只有汪小菲[啊这]加拿大18岁美女大学生酒后乱性:表演后空翻后,被小帅哥"推倒"...杨苡那一代女性:在新旧之间,尽量向外走i-Weekends | 《流浪地球2》明日上映!在这些细节中我们看和创投的共性:“谈了8任男友,个个是渣男”:潜意识缺失的爱,要靠一辈子来还论断句的重要性:本届世界杯中日韩都进16强!个人养老金对年轻人更友好,专家呼吁每天省杯咖啡提前规划养老!全国政协委员建议:扩大个人养老金覆盖面阎润涛逝世2周年谈阎颜之争踢人校友群缺失的品德,却在MIT校友群找到了HPV九价疫苗的自我修正:扩龄、减次,未来男性可享|焦点分析还不如GAN!谷歌、DeepMind等发文:扩散模型直接从训练集里「抄」新的一年,五步法理清个人职业发展方向共产党的文艺工作是为工农服务重庆:​锚定高质量发展方向 加快建设国家重要先进制造业中心教育随笔(114)《赵威后问齐使》教学后记10万人民币、远程办公就能移民,新时代的特色移民方法
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。