网易首页 > 网易号 > 正文 申请入驻

Java 缺失的特性:扩展方法

0
分享至

什么是扩展方法

扩展方法,就是能够向现有类型直接“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改现有类型。调用扩展方法的时候,与调用在类型中实际定义的方法相比没有明显的差异。

为什么需要扩展方法

考虑要实现这样的功能:从 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:

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">


2022.1.35manifold.version>
properties>


systems.manifoldgroupId>
manifold-extartifactId>
${manifold.version}version>
dependency>

...
dependencies>


org.apache.maven.pluginsgroupId>
maven-compiler-pluginartifactId>
3.8.1version>
8source>
8target>
UTF-8encoding>
-Xplugin:Manifold no-bootstraparg>
compilerArgs>
systems.manifoldgroupId>
manifold-extartifactId>
${manifold.version}version>
path>
annotationProcessorPaths>
configuration>
plugin>
plugins>
build>
project>

如果你的项目中使用了 Lombok,需要把 Lombok 也加入 annotationProcessorPaths:


org.projectlombokgroupId>
lombokartifactId>
${lombok.version}version>
path>
systems.manifoldgroupId>
manifold-extartifactId>
${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 的扩展方法
*/
@Extension
public final class StringExt {

public static String[] split(@This String str, char separator) {
return StringUtils.split(str, separator);
}
}

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

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

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

3.工具类所在的包名,需要以 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 的扩展方法
*/
@Extension
public final class CollectionExt {

public static boolean isNullOrEmpty(@This Collection coll) {
return coll == null || coll.isEmpty();
}
}

然后调用的时候:

List 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 为数组元素的类型):

但如果是原始类型数组,IDEA 指示的返回值是:

但是我用的是 Java 啊,擦除法泛型怎么可能拥有 List 这么伟大的功能 —— 所以你只能用原生类型来接收这个返回值 :)

—— 许个愿:Project Valhalla 会在 Java21 中 GA。

我们经常在各个项目中看到,大家先把某个对象包装成 Optional,然后进行 filter、map 等。通过 @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 的扩展方法
*/
@Extension
public 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 list = List.of("a", "b", "c");
Set set = Set.of("a", "b", "c");
Map 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 扩展方法
*/
@Extension
public final class ListExt {

/**
* 返回只包含一个元素的不可变 List
*/
@Extension
public static List of(E element) {
return Collections.singletonList(element);
}

/**
* 返回包含多个元素的不可变 List
*/
@Extension
@SafeVarargs
public static List 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,那是可以的 :) —— 不过劝你别这么做,容易被打。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
果然,没有人比中金更懂M1!-4.2%,意味着什么?

果然,没有人比中金更懂M1!-4.2%,意味着什么?

金石随笔
2024-06-16 00:35:36
A股超1600家通过转融通出借股份做空自己,尤其这几家尽量远离!

A股超1600家通过转融通出借股份做空自己,尤其这几家尽量远离!

股海风云大作手
2024-06-16 09:19:07
惨烈!山东高档小区顶楼火灾,二人遇难,内情曝光令人震惊!

惨烈!山东高档小区顶楼火灾,二人遇难,内情曝光令人震惊!

饭桶说史
2024-06-16 19:04:54
上海申花1-1中超第4,继续保持不败,并一夜反超上海海港升至第一

上海申花1-1中超第4,继续保持不败,并一夜反超上海海港升至第一

侧身凌空斩
2024-06-16 21:36:37
玫瑰的故事:刘亦菲的瘪臀、粗腰、粗腿,是对内娱畸形审美的反击

玫瑰的故事:刘亦菲的瘪臀、粗腰、粗腿,是对内娱畸形审美的反击

喵喵娱乐团
2024-06-14 17:56:07
获全场最佳!西班牙中场技术出众却毫无名气,主帅:名字耽误了他

获全场最佳!西班牙中场技术出众却毫无名气,主帅:名字耽误了他

星耀国际足坛
2024-06-16 11:36:04
笑死了!河南专家建议中午不要浇地,结果在评论区被骂惨

笑死了!河南专家建议中午不要浇地,结果在评论区被骂惨

文雅笔墨
2024-06-16 00:17:39
姜萍父亲发声,家庭困难住着烂房子,姐妹都是学霸,刘奔爆笑回应

姜萍父亲发声,家庭困难住着烂房子,姐妹都是学霸,刘奔爆笑回应

兰子记
2024-06-15 21:53:08
姜萍考上高中但选择了中专,亲戚说两姐妹太懂事:读职高是想学点技术,姐姐目前在奶茶店打工

姜萍考上高中但选择了中专,亲戚说两姐妹太懂事:读职高是想学点技术,姐姐目前在奶茶店打工

纵相新闻
2024-06-16 15:43:35
上海电影节众男星状态:45岁邓超显沧桑,李治廷寸头造型阳刚帅气

上海电影节众男星状态:45岁邓超显沧桑,李治廷寸头造型阳刚帅气

扒虾侃娱
2024-06-15 21:23:43
24个1传0失误,土耳其不信邪,全员死攻张常宁,最后把自己打崩溃

24个1传0失误,土耳其不信邪,全员死攻张常宁,最后把自己打崩溃

我就是一个说球的
2024-06-16 14:18:02
银行女职员表白领导,后续来了:多方介入,网传不实,没利益关联

银行女职员表白领导,后续来了:多方介入,网传不实,没利益关联

眼光很亮
2024-06-16 09:00:09
普京怕啥来啥,北约志愿飞行员将直接参战:要3个月打败俄空天军

普京怕啥来啥,北约志愿飞行员将直接参战:要3个月打败俄空天军

帅先工场
2024-06-15 16:56:54
苏纳克一家四口亮相庆典!英国夫人穿碎花裙观礼,被老公牵手好甜

苏纳克一家四口亮相庆典!英国夫人穿碎花裙观礼,被老公牵手好甜

八八尚语
2024-06-16 15:06:20
身高191体重180火遍全网的马丁-亚当:上届欧洲杯我在家喝酒

身高191体重180火遍全网的马丁-亚当:上届欧洲杯我在家喝酒

直播吧
2024-06-16 17:34:10
乌兹别克斯坦夫人给韩国夫人上了一堂出访课,这次遇到了强对手!

乌兹别克斯坦夫人给韩国夫人上了一堂出访课,这次遇到了强对手!

小毅讲历史
2024-06-15 19:11:33
姜萍父亲月工资九百,姐姐打三份工,妈妈惹争议,网友支持去国外

姜萍父亲月工资九百,姐姐打三份工,妈妈惹争议,网友支持去国外

贾文彬的史书
2024-06-16 19:17:57
教师因救人上课迟到被处分!网传举报的是留学生,当事人回应!

教师因救人上课迟到被处分!网传举报的是留学生,当事人回应!

远荐
2024-06-16 15:08:00
大陆不再沉默,给黄仁勋上了一课,选在美收紧对华AI芯片出口之际

大陆不再沉默,给黄仁勋上了一课,选在美收紧对华AI芯片出口之际

陈菲副教授
2024-06-15 18:20:03
又火出圈,已累计卖出14万单!医生提醒:不要盲目选择

又火出圈,已累计卖出14万单!医生提醒:不要盲目选择

鲁中晨报
2024-06-16 20:43:06
2024-06-16 22:44:49
阿里云云栖号
阿里云云栖号
阿里云官方内容社区!
2941文章数 864关注度
往期回顾 全部

科技要闻

iPhone 16会杀死大模型APP吗?

头条要闻

理想车友聚会多车连环追尾 组织者:突遭大雨 车距较近

头条要闻

理想车友聚会多车连环追尾 组织者:突遭大雨 车距较近

体育要闻

没人永远年轻 但青春如此无敌还是离谱了些

娱乐要闻

上影节红毯:倪妮好松弛,娜扎吸睛

财经要闻

打断妻子多根肋骨 上市公司创始人被公诉

汽车要闻

售17.68万-21.68万元 极狐阿尔法S5正式上市

态度原创

健康
教育
数码
手机
公开课

晚餐不吃or吃七分饱,哪种更减肥?

教育要闻

高考志愿怎么报?人民日报发布填报技巧→

数码要闻

AMD RDNA3.5核显跑分喜人!非常接近RTX 2050

手机要闻

消息称苹果计划为 iPhone 17 系列开发一款更薄的机型

公开课

近视只是视力差?小心并发症

无障碍浏览 进入关怀版