Java 缺失的特性:扩展方法

简介: 本文介绍了如何使用 Manifold 在 Java 中实现扩展方法,来助力提升开发效率和代码可读性。

什么是扩展方法


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


为什么需要扩展方法


考虑要实现这样的功能:从 Redis 取出包含多个商品ID的字符串后(每个商品ID使用英文逗号分隔),先对商品ID进行去重(并能够维持元素的顺序),最后再使用英文逗号将各个商品ID进行连接。

// "123,456,123,789"Stringstr=redisService.get(someKey)

传统写法:

StringitemIdStrs=String.join(",", newLinkedHashSet<>(Arrays.asList(str.split(","))));

使用 Stream 写法:

StringitemIdStrs=Arrays.stream(str.split(",")).distinct().collect(Collectors.joining(","));

假设在 Java 中能实现扩展方法,并且我们为数组添加了扩展方法 toList(将数组变为 List),为 List 添加了扩展方法 toSet(将 List 变为 LinkedHashSet),为 Collection 添加了扩展方法 join(将集合中元素的字符串形式使用给定的连接符进行连接),那我们将可以这样写代码:

StringitemIdStrs=str.split(",").toList().toSet().join(",");

相信此刻你已经有了为什么需要扩展方法的答案:

  • 可以对现有的类库,进行直接增强,而不是使用工具类
  • 相比使用工具类,使用类型本身的方法写代码更流畅更舒适
  • 代码更容易阅读,因为是链式调用,而不是用静态方法套娃


在 Java 中怎么实现扩展方法


我们先来问问最近大火的 ChatGPT:



好吧,ChatGPT 认为 Java 里面的扩展方法就是通过工具类提供的静态方法 :)。



所以接下来我将介绍一种全新的黑科技:Manifold


准备条件


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



然后再在项目 pom 的 maven-compiler-plugin 中加入 annotationProcessorPaths

<projectxmlns="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>2023.1.3</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,编写扩展方法:

packagecom.alibaba.zhiye.extensions.java.lang.String;
importmanifold.ext.rt.api.Extension;
importmanifold.ext.rt.api.This;
importorg.apache.commons.lang3.StringUtils;
/*** String 的扩展方法*/@ExtensionpublicfinalclassStringExt {
publicstaticString[] split(@ThisStringstr, charseparator) {
returnStringUtils.split(str, separator);
    }
}

可以发现本质上还是工具类的静态方法,但是有一些要求:

  1. 工具类需要使用 Manifold 的 @Extension 注解
  2. 静态方法中,目标类型的参数,需要使用 @This 注解
  3. 工具类所在的包名,需要以 extensions.目标类型全限定类名 结尾


—— 用过 C# 的同学应该会会心一笑,这就是模仿的 C# 的扩展方法。


关于第 3 点,之所以有这个要求,是因为 Manifold 希望能快速找到项目中的扩展方法,避免对项目中所有的类进行注解扫描,提升处理的效率。


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

Xnip2023-01-11_10-54-36.png

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

image.png

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

image.png

[Ljava.lang.String;@511d50c0 什么的,Goodbye,再也不见~


因为是在编译期将扩展方法的调用替换为静态方法调用,所以使用 Manifold 的扩展方法,即使调用方法的对象是 null 也没有问题,因为处理后的代码是把 null 作为参数传递到对应的静态方法。比如我们对 Collection 进行扩展:

packagecom.alibaba.zhiye.extensions.java.util.Collection;
importmanifold.ext.rt.api.Extension;
importmanifold.ext.rt.api.This;
importjava.util.Collection;
/*** Collection 的扩展方法*/@ExtensionpublicfinalclassCollectionExt {
publicstaticbooleanisNullOrEmpty(@ThisCollection<?>coll) {
returncoll==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 的方法:

image.png

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


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

image.png

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

image.png

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

image.png

—— 许个愿:Project Valhalla 会在 Java21 中 GA。


我们经常在各个项目中看到,大家先把某个对象包装成 Optional,然后进行 filtermap 等。通过 @Self 的类型映射,你可以这样为 Object 加入一个非常实用的办法:

packagecom.alibaba.zhiye.extensions.java.lang.Object;
importmanifold.ext.rt.api.Extension;
importmanifold.ext.rt.api.Self;
importmanifold.ext.rt.api.This;
importjava.util.Optional;
/*** Object 的扩展方法*/@ExtensionpublicfinalclassObjectExt {
publicstaticOptional<@SelfObject>asOpt(@ThisObjectobj) {
returnOptional.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

packagecom.alibaba.aladdin.app.extensions.java.util.List;
importmanifold.ext.rt.api.Extension;
importmanifold.ext.rt.api.This;
importjava.util.Arrays;
importjava.util.Collections;
importjava.util.List;
/*** List 扩展方法*/@ExtensionpublicfinalclassListExt {
/*** 返回只包含一个元素的不可变 List*/@Extensionpublicstatic<E>List<E>of(Eelement) {
returnCollections.singletonList(element);
    }
/*** 返回包含多个元素的不可变 List*/@Extension@SafeVarargspublicstatic<E>List<E>of(E... elements) {
returnCollections.unmodifiableList(Arrays.asList(elements));
    }
}

然后你就可以欺骗自己已经用上了 Java8 之后的版本 —— 你发任你发,我用 Java8。


BTW,因为 Object 是所有类的父类,如果你给 Object 添加静态扩展方法,那么意味着你可以在任何地方直接访问到这个静态方法,而不需要 import —— 恭喜你,解锁了 “顶级函数”。


建议


关于 Manifold


我从 2019 年开始关注 Manifold,那时候 Manifold IDEA 插件还是收费的,所以当时只是做了简单的尝试。最近再看,IDEA 插件已经完全免费,所以迫不及待地想要物尽其用。目前我已经在一个项目中使用了 Manifold 来实现扩展方法的功能 —— 当事人表示非常上瘾,已经离不开了。如果你有使用上的建议和疑问,欢迎和我一起讨论。


谨慎添加扩展方法


如果决定在项目中使用 Manifold 实现扩展方法,那么我们一定要做到 “管住自己的手”


首先,就是上文说的,给 Object 或者其他在项目中使用非常广泛的类添加扩展方法,一定要非常的慎重,最好是要和项目组的同学一起讨论,让大家一起决定,否则很容易让人迷惑(骂人)。


另外,如果要给某个类添加扩展方法,一定要先认真思考一个问题:“这个方法的逻辑是不是在这个类的职责范围内,是否有掺杂业务自定义逻辑”。例如下面这个方法(判断给定的字符串是不是一个合法的参数):

publicstaticbooleanisValidParam(Stringstr) {
returnStringUtils.isNotBlank(str) &&!"null".equalsIgnoreCase(str);
}

很明显,isValidParam 不是 String 这个类的职责范围,应该把 isValidParam 继续放在 XxxBizUtils 里面。当然,如果你把方法名改成 isNotBlankAndNotEqualsIgnoreCaseNullLiteral,那是可以的 :) —— 不过劝你别这么做,容易被打。

目录
相关文章
|
2月前
|
存储 安全 Java
Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
【10月更文挑战第17天】Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
78 2
|
2月前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
48 3
|
2月前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
37 2
|
2月前
|
存储 算法 Java
Java Set因其“无重复”特性在集合框架中独树一帜
【10月更文挑战第14天】Java Set因其“无重复”特性在集合框架中独树一帜。本文深入解析Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定的数据结构(哈希表、红黑树)确保元素唯一性,并提供最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的`hashCode()`与`equals()`方法。
36 3
|
2月前
|
安全 Java API
Java 17新特性让你的代码起飞!
【10月更文挑战第4天】自Java 8发布以来,Java语言经历了多次重大更新,每一次都引入了令人兴奋的新特性,极大地提升了开发效率和代码质量。本文将带你从Java 8一路走到Java 17,探索那些能让你的代码起飞的关键特性。
93 1
|
2月前
|
编解码 Oracle Java
java9到java17的新特性学习--github新项目
本文宣布了一个名为"JavaLearnNote"的新GitHub项目,该项目旨在帮助Java开发者深入理解和掌握从Java 9到Java 17的每个版本的关键新特性,并通过实战演示、社区支持和持续更新来促进学习。
90 3
|
12天前
|
存储 Java 开发者
什么是java的Compact Strings特性,什么情况下使用
Java 9引入了紧凑字符串特性,优化了字符串的内存使用。它通过将字符串从UTF-16字符数组改为字节数组存储,根据内容选择更节省内存的编码方式,通常能节省10%至15%的内存。
|
21天前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
37 6
|
1月前
|
分布式计算 Java API
Java 8引入了流处理和函数式编程两大新特性
Java 8引入了流处理和函数式编程两大新特性。流处理提供了一种声明式的数据处理方式,使代码更简洁易读;函数式编程通过Lambda表达式和函数式接口,简化了代码书写,提高了灵活性。此外,Java 8还引入了Optional类、新的日期时间API等,进一步增强了编程能力。这些新特性使开发者能够编写更高效、更清晰的代码。
33 4
|
2月前
|
存储 Java API
优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。
【10月更文挑战第19天】本文介绍了如何优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。内容包括Map的初始化、使用Stream API处理Map、利用merge方法、使用ComputeIfAbsent和ComputeIfPresent,以及Map的默认方法。这些技巧不仅提高了代码的可读性和维护性,还提升了开发效率。
96 3
下一篇
DataWorks