共计 6697 个字符,预计需要花费 17 分钟才能阅读完成。
作者:周密(之叶)
什么是扩大办法
扩大办法,就是可能向现有类型间接“增加”办法,而无需创立新的派生类型、从新编译或以其余形式批改现有类型。调用扩大办法的时候,与调用在类型中理论定义的办法相比没有显著的差别。
为什么须要扩大办法
思考要实现这样的性能:从 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 的扩大办法
*/
@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>(T 为数组元素的类型):
但如果是原始类型数组,IDEA 批示的返回值是:
然而我用的是 Java 啊,擦除法泛型怎么可能领有 List<char> 这么平凡的性能 —— 所以你只能用原生类型来接管这个返回值 :)
—— 许个愿,心愿 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,那是能够的 :) —— 不过劝你别这么做,容易被打。