作者:缜密(之叶)

什么是扩展办法

扩展办法,便是能够向现有类型直接“增加”办法,而无需创立新的派生类型、重新编译或以其他办法修正现有类型。调用扩展办法的时分,与调用在类型中实际界说的办法比较没有显着的差异。

为什么需求扩展办法

考虑要完成这样的功用:从 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:

Java 缺失的特性:扩展方法

好吧,ChatGPT 认为 Java 里边的扩展办法便是经过东西类供给的静态办法 :)。所以接下来我将介绍一种全新的黑科技:

Manifold(github.com/manifold-sy…)

准备条件

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

Java 缺失的特性:扩展方法

然后再在项目 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);
    }
}

能够发现本质上仍是东西类的静态办法,可是有一些要求:

  1. 东西类需求运用 Manifold 的@Extension注解

  2. 静态办法中,方针类型的参数,需求运用@This注解

  3. 东西类所在的包名,需求以 extensions.方针类型全限定类名 结束

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

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

具备了扩展办法的才能,现在咱们就能够这样调用了:

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

Java 缺失的特性:扩展方法

而数组的toString办法,运用的是 Manifold 为数组界说的扩展办法ManArrayExt.toString(@This Object array):

Java 缺失的特性:扩展方法

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

Java 缺失的特性:扩展方法

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

对于目标数组,咱们能够看到toList办法回来的便是对应的List(T 为数组元素的类型):

Java 缺失的特性:扩展方法

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

Java 缺失的特性:扩展方法

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

Java 缺失的特性:扩展方法

—— 许个愿,希望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,那是能够的 :) —— 不过劝你别这么做,简单被打。