作者:缜密(之叶)
什么是扩展办法
扩展办法,便是能够向现有类型直接“增加”办法,而无需创立新的派生类型、重新编译或以其他办法修正现有类型。调用扩展办法的时分,与调用在类型中实际界说的办法比较没有显着的差异。
为什么需求扩展办法
考虑要完成这样的功用:从 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(github.com/manifold-sy…)
准备条件
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 的扩展办法
*/
@Extension
public final class StringExt {
public static String[] split(@This String str, char separator) {
return StringUtils.split(str, separator);
}
}
能够发现本质上仍是东西类的静态办法,可是有一些要求:
-
东西类需求运用 Manifold 的@Extension注解
-
静态办法中,方针类型的参数,需求运用@This注解
-
东西类所在的包名,需求以 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<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 为数组元素的类型):
但假如是原始类型数组,IDEA 指示的回来值是:
可是我用的是 Java 啊,擦除法泛型怎么可能拥有List这么伟大的功用 —— 所以你只能用原生类型来接收这个回来值 :)
—— 许个愿,希望Project Valhalla 早日 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<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 扩展办法
*/
@Extension
public 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,那是能够的 :) —— 不过劝你别这么做,简单被打。