Java 缺失的特性:扩展方法

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
注册配置 MSE Nacos/ZooKeeper,182元/月
简介: 扩展方法,就是能够向现有类型直接“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改现有类型。调用扩展方法的时候,与调用在类型中实际定义的方法相比没有明显的差异。

作者:周密(之叶)


什么是扩展方法


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


为什么需要扩展方法


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


1.png


好吧,ChatGPT 认为 Java 里面的扩展方法就是通过工具类提供的静态方法 :)。所以接下来我将介绍一种全新的黑科技:


Manifold(https://github.com/manifold-systems/manifold


准备条件


Manifold 的原理和 Lombok 是类似的,也是在编译期间通过注解处理器进行处理。所以要在 IDEA 中正确使用 Manifold,需要安装 Manifold IDEA 的插件:


2.png


然后再在项目 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 中,Stringsplit 方法,使用的是字符串作为参数,即 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 希望能快速找到项目中的扩展方法,避免对项目中所有的类进行注解扫描,提升处理的效率。


具备了扩展方法的能力,现在我们就可以这样调用了:



18

Amazing!而且你可以发现, System.out.println(numStrs.toString()) 打印的居然是数组对象的字符串形式 —— 而不是数组对象的地址。查看反编译后的 App.class,发现是将扩展方法的调用,替换为静态方法调用:


4.png


而数组的 toString 方法,使用的是 Manifold 为数组定义的扩展方法 ManArrayExt.toString(@This Object array)


5.png


[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 的方法:


6.png


我们看到 List<@Self(true) Object> 这样的写法:@Self 是用来表示被注解的值应该是什么类型,如果是 @Self,即 @Self(false),表示被注解的值和 @This 注解的值是同一个类型;@Self(true) 则表示是数组中元素的类型。


对于对象数组,我们可以看到 toList 方法返回的就是对应的 List<T>(T 为数组元素的类型):


7.png


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


8.png


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


9.png


—— 许个愿,希望 Project Valhalla 早日 GA。


我们经常在各个项目中看到,大家先把某个对象包装成 Optional,然后进行 filtermap 等。通过 @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,那是可以的 :) —— 不过劝你别这么做,容易被打。

相关文章
|
2月前
|
Java API 数据处理
Java新特性:使用Stream API重构你的数据处理
Java新特性:使用Stream API重构你的数据处理
Java API 开发者
105 0
|
4月前
|
并行计算 Java API
Java List 集合结合 Java 17 新特性与现代开发实践的深度解析及实战指南 Java List 集合
本文深入解析Java 17中List集合的现代用法,结合函数式编程、Stream API、密封类、模式匹配等新特性,通过实操案例讲解数据处理、并行计算、响应式编程等场景下的高级应用,帮助开发者提升集合操作效率与代码质量。
224 1
|
4月前
|
安全 Java 微服务
Java 最新技术和框架实操:涵盖 JDK 21 新特性与 Spring Security 6.x 安全框架搭建
本文系统整理了Java最新技术与主流框架实操内容,涵盖Java 17+新特性(如模式匹配、文本块、记录类)、Spring Boot 3微服务开发、响应式编程(WebFlux)、容器化部署(Docker+K8s)、测试与CI/CD实践,附完整代码示例和学习资源推荐,助你构建现代Java全栈开发能力。
550 0
|
4月前
|
缓存 安全 Java
Java 并发新特性实战教程之核心特性详解与项目实战
本教程深入解析Java 8至Java 19并发编程新特性,涵盖CompletableFuture异步编程、StampedLock读写锁、Flow API响应式流、VarHandle内存访问及结构化并发等核心技术。结合电商订单处理、缓存系统、实时数据流、高性能计数器与用户资料聚合等实战案例,帮助开发者高效构建高并发、低延迟、易维护的Java应用。适合中高级Java开发者提升并发编程能力。
143 0
|
4月前
|
安全 Java API
Java 17 及以上版本核心特性在现代开发实践中的深度应用与高效实践方法 Java 开发实践
本项目以“学生成绩管理系统”为例,深入实践Java 17+核心特性与现代开发技术。采用Spring Boot 3.1、WebFlux、R2DBC等构建响应式应用,结合Record类、模式匹配、Stream优化等新特性提升代码质量。涵盖容器化部署(Docker)、自动化测试、性能优化及安全加固,全面展示Java最新技术在实际项目中的应用,助力开发者掌握现代化Java开发方法。
222 1
|
4月前
|
IDE Java API
Java 17 新特性与微服务开发的实操指南
本内容涵盖Java 11至Java 17最新特性实战,包括var关键字、字符串增强、模块化系统、Stream API、异步编程、密封类等,并提供图书管理系统实战项目,帮助开发者掌握现代Java开发技巧与工具。
269 1
|
4月前
|
Java 数据库连接 API
Java 8 + 特性及 Spring Boot 与 Hibernate 等最新技术的实操内容详解
本内容涵盖Java 8+核心语法、Spring Boot与Hibernate实操,按考试考点分类整理,含技术详解与代码示例,助力掌握最新Java技术与应用。
155 2

热门文章

最新文章