Java的缺失之处:扩展方法的探讨

发表时间: 2023-02-17 17:05

简介: 本文介绍了如何使用 Manifold 在 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


准备条件

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.34</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 注解
  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 的扩展方法 */@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) 则表示是数组中元素的类型。


点击查看原文,获取更多福利!

https://developer.aliyun.com/article/1135549?utm_content=g_1000368282


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。