_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(","))));
使用写法:
String itemIdStrs = Arrays.stream(str.split(",")).distinct().collect(Collectors.joining(","));
假设在 Java 中能实现扩展方法,并且我们为数组添加了扩展方法(将数组变为List),为List添加了扩展方法toSet(将List变为),为添加了扩展方法join(将集合中元素的字符串形式使用给定的连接符进行连接),那我们将可以这样写代码:
String itemIdStrs = str.split(",").toList().toSet().join(",");
相信此刻你已经有了为什么需要扩展方法的答案:
在 Java 中怎么实现扩展方法
我们先来问问最近大火的 :
'/>
好吧, 认为 Java 里面的扩展方法就是通过工具类提供的静态方法 :)。所以接下来我将介绍一种全新的黑科技:
()
准备条件
的原理和 是类似的,也是在编译期间通过注解处理器进行处理。所以要在 IDEA 中正确使用 ,需要安装 IDEA 的插件:
_java自行车_java是什么'/>
然后再在项目 pom 的maven--中加入aths:
<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>
<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>
如果你的项目中使用了 ,需要把 也加入aths:
<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 中,的split方法,使用的是字符串作为参数,即[] split()。我们现在来为添加一个扩展方法[] split(char):按给定的字符进行分割。
基于 ,编写扩展方法:
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);
}
}
可以发现本质上还是工具类的静态方法,但是有一些要求:
工具类需要使用 的@注解
静态方法中,目标类型的参数,需要使用@This注解
工具类所在的包名,需要以.目标类型全限定类名结尾
—— 用过 C# 的同学应该会会心一笑,这就是模仿的 C# 的扩展方法。
关于第 3 点,之所以有这个要求,是因为 希望能快速找到项目中的扩展方法,避免对项目中所有的类进行注解扫描,提升处理的效率。
具备了扩展方法的能力,现在我们就可以这样调用了:
!而且你可以发现,.out.(.())打印的居然是数组对象的字符串形式 —— 而不是数组对象的地址。查看反编译后的 App.class,发现是将扩展方法的调用,替换为静态方法调用:
_java自行车_java是什么'/>
而数组的方法,使用的是 为数组定义的扩展方法.(@This array):
_java自行车_java是什么'/>
[Ljava.lang.;@什么的,,再也不见~
因为是在编译期将扩展方法的调用替换为静态方法调用,所以使用 的扩展方法,即使调用方法的对象是null也没有问题,因为处理后的代码是把null作为参数传递到对应的静态方法。比如我们对进行扩展:
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 的扩展方法
*/
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.,,再也不见~
数组扩展方法
JDK 中,数组并没有一个具体的对应类型,那为数组定义的扩展类,要放到什么包中呢?看下的源码,发现 专门提供了一个类.rt.api.Array,用来表示数组。比如中为数组提供的的方法:
_java自行车_java是什么'/>
我们看到List这样的写法:@Self是用来表示被注解的值应该是什么类型,如果是@Self,即@Self(false),表示被注解的值和@This注解的值是同一个类型;@Self(true)则表示是数组中元素的类型。
对于对象数组,我们可以看到方法返回的就是对应的List(T 为数组元素的类型):
_java自行车_java是什么'/>
但如果是原始类型数组,IDEA 指示的返回值是:
'/>
但是我用的是 Java 啊,擦除法泛型怎么可能拥有List这么伟大的功能 —— 所以你只能用原生类型来接收这个返回值 :)
_java是什么_java自行车'/>
—— 许个愿,希望 早日 GA。
我们经常在各个项目中看到,大家先把某个对象包装成,然后进行、map等。通过@Self的类型映射,你可以这样为加入一个非常实用的办法:
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);
你现在可以自然而然的使用:
someObj.asOpt().filter(someFilter).map(someMapper).orElseGet(someSupplier);
当然, 是所有的类的父类,这样做是否合适,还是需要谨慎的思考一下。
扩展静态方法
我们都知道 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 之类的库 —— 然而.of用起来终究是比不上List.of这样的正统来的自然。
没关系, 说:“无所谓,我会出手”。基于 扩展静态方法,就是在扩展类的静态方法上,也加上@:
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,因为是所有类的父类,如果你给添加静态扩展方法,那么意味着你可以在任何地方直接访问到这个静态方法,而不需要 —— 恭喜你,解锁了 “顶级函数”。
建议
关于
我从 2019 年开始关注 ,那时候 IDEA 插件还是收费的,所以当时只是做了简单的尝试。最近再看,IDEA 插件已经完全免费,所以迫不及待地想要物尽其用。目前我已经在一个项目中使用了 来实现扩展方法的功能 —— 当事人表示非常上瘾,已经离不开了。如果你有使用上的建议和疑问,欢迎和我一起讨论。
谨慎添加扩展方法
如果决定在项目中使用 实现扩展方法,那么我们一定要做到 “管住自己的手”。
首先,就是上文说的,给或者其他在项目中使用非常广泛的类添加扩展方法,一定要非常的慎重,最好是要和项目组的同学一起讨论,让大家一起决定,否则很容易让人迷惑。
另外,如果要给某个类添加扩展方法,一定要先认真思考一个问题:“这个方法的逻辑是不是在这个类的职责范围内,是否有掺杂业务自定义逻辑”。例如下面这个方法(判断给定的字符串是不是一个合法的参数):
public static boolean isValidParam(String str) {
return StringUtils.isNotBlank(str) && !"null".equalsIgnoreCase(str);
}
很明显,不是这个类的职责范围,应该把继续放在里面。当然,如果你把方法名改成ral,那是可以的 :) —— 不过劝你别这么做,容易被打。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
加入IP合伙人(站长加盟) | 全面包装你的品牌,搭建一个全自动交付的网赚资源独立站 | 晴天实测8个月运营已稳定月入3W+
限时特惠:本站每日持续更新海量内部创业教程,一年会员只需98元,全站资源免费无限制下载点击查看会员权益
站长微信: qtw123cn
