×
Community Blog Java's Missing Feature: Extension Method

Java's Missing Feature: Extension Method

This article explains the extension method, why we need it, and how to implement it in Java.

By Mi Zhou (Zhiye)

1

What Is Extension Method?

An extension method means to add methods to an existing type without creating a new derived type, recompiling, or modifying the existing type. When calling an extension method, there is no significant difference compared to calling the method defined in the type.

Why Do We Need Extension Methods?

To implement this function: After extracting strings containing multiple product IDs from Redis (each product ID is separated by English commas), deduplicate product IDs (still can maintain the order of elements) and then use English commas to connect each product ID.

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

Traditional Syntax:

String itemIdStrs = String.join(",", new LinkedHashSet<>(Arrays.asList(str.split(","))));

Use Stream syntax:

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

Assume extension methods can be implemented in Java. For arrays, we add extension method toList (turning arrays into List ); for List, we add extension methods toSet (turning List into LinkedHashSet ); for Collection, we add extension methods join (connecting the string forms of elements in the collection with given connectors). Then, we can write code like this:

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

Now, you know why we need extension methods:

  • We can directly enhance an existing class library instead of using a tool class.
  • It is smoother and more comfortable to write code using type methods than using the tool class.
  • The code is easier to read because it is a chain call rather than a static method.

How to Implement Extension Methods in Java

I will introduce a new way: Manifold.

Preparations

The principle of Manifold is similar to Lombok, which is processed by the annotation processor during compilation. We need to install the Manifold IDEA plug-in to properly use Manifold in IDEA.

2

Then, add annotationProcessorPaths to the maven-compiler-plugin of the project pom.

<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>2023.1.10</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</arg>
                    </compilerArgs>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>systems.manifold</groupId>
                            <artifactId>manifold-ext</artifactId>
                            <version>${manifold.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

If you use Lombok in your project, you need to add Lombok to 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>

Writing Extension Methods

In JDK, the split method of String uses a string as a parameter, which is String[] split(String). Now let's add an extension method for String, String[] split(char): split by the given character.

Based on Manifold, write an extension method.

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 method
 */
@Extension
public final class StringExt {
    public static String[] split(@This String str, char separator) {
        return StringUtils.split(str, separator);
    }
}

We can find they are essentially static methods of tool class, but there are some requirements.

  1. The tool class needs to use the @Extension annotation of Manifold.
  2. In the static method, the parameter of the target type needs to use @This annotation.
  3. The name of the package where the tool class is located must end with extensions, and target type fully qualified class name.

Those familiar with C# should know this is an imitation of the C# extension method.

Regarding point 3, the reasons for this requirement are that Manifold wants to quickly find extension methods in the project, avoid scanning all classes in the project for annotations, and improve processing efficiency.

With the ability of extension methods, we can call them like this:

Amazing! Also, System.out.println(numStrs.toString()) prints the string form of the array object instead of the address of the array object. Looking at the decompiled App.class, it is found that the extension method call is replaced with the static method call.

3

The toString method of the array uses the extension method ManArrayExt.toString(@This Object array) defined by Manifold for arrays.

4

No more [Ljava.lang.String;@511 d50c0

Since the extension method call is replaced with the static method call at compile time, there is no problem with using Manifold's extension method even if the method calling object is null. The processed code passes null as a parameter to the corresponding static method. For example, let's extend 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 method
 */
@Extension
public final class CollectionExt {
    public static boolean isNullOrEmpty(@This Collection<?> coll) {
        return coll == null || coll.isEmpty();
    }
}

When calling:

List<String> list = getSomeNullableList();

// If list is null, it will enter the if block without triggering the null pointer exception.
if (list.isNullOrEmpty()) {
    // TODO
}

No more java.lang.NullPointerException.

Array Extension Methods

Arrays do not have a specific corresponding type in JDK. What package should the extension class defined for arrays be put into? Looking at the ManArrayExt source code, we find that Manifold specifically provides a class manifold.rt.api.Array to represent the array. For example, the method of toList provided for the array in ManArrayExt:

5

We see List<@Self(true) Object> syntax like this: @Self is used to indicate what type the annotated value should be. If it is @Self, which is @Self(false), the annotated value and the value of the @This annotation are the same types. @Self(true) indicates the type of element in the array.

For object arrays, we can see the toList method returns the corresponding List<T> (T is the type of the array element).

6

However, if it is a primitive type of array, the return value indicated by IDEA is:

7

But I'm using Java. How can type erasure generics have such a great feature like List<char>? You can only use native types to receive this return value. :)

8

We often see in various projects that we package an object into an Optional and then filter, map, etc. With @Self type mapping, you can add a practical method for 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 method
 */
@Extension
public final class ObjectExt {
    public static Optional<@Self Object> asOpt(@This Object obj) {
        return Optional.ofNullable(obj);
    }
}

Then, any object will have asOpt() methods. Previously, you need to package it.

Optional.ofNullable(someObj).filter(someFilter).map(someMapper).orElseGet(someSupplier);

Now, you can use Optional.

someObj.asOpt().filter(someFilter).map(someMapper).orElseGet(someSupplier);

Object is the parent class of all classes. You still need to consider whether this is appropriate.

Extending Static Methods

We all know Java 9 added factory methods to collections:

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);

If you're not using Java 9 or above, you'll have to use a library like Guava. However, ImmutableList.of can't compare to List.of.

Never mind. Manifold can do it. Extending the static method based on Manifold means adding @Extensionto the static method of the extended class.

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 method
 */
@Extension
public final class ListExt {
    
    /**
     * Returns an immutable List containing only one element.
     */
    @Extension
    public static <E> List<E> of(E element) {
        return Collections.singletonList(element);
    }
    
    /**
     * Returns an immutable List containing multiple elements.
     */
    @Extension
    @SafeVarargs
    public static <E> List<E> of(E... elements) {
        return Collections.unmodifiableList(Arrays.asList(elements));
    }
}

Then, you can deceive yourself that you have used the version after Java 8.

Also, since Object is the parent class of all classes, if you add static extension methods to Object, it means you can access this static method anywhere without import.

Suggestion

About Manifold

I started to pay attention to Manifold in 2019. At that time, the Manifold IDEA plug-in was not free yet, so I only made a simple attempt at that time. Recently, the IDEA plug-in has become free to use, so I can't wait to make the most of it. I have used Manifold in a project to implement the function of extension methods. If you have any suggestions or questions about its use, please discuss them with me.

Add Extension Methods Cautiously

If we decide to use Manifold to implement extension methods in our project, we must be cautious.

First of all, as mentioned above, it is necessary to be careful when adding extension methods to Object or other classes widely used in the project. It is best to discuss with the project team and let everyone decide together. Otherwise, it can be confusing.

In addition, if you want to add an extension method to a class, you must consider – Is the logic of this method included in the scope of the responsibility of this class? Is there any business custom logic? For example, the following method (determines whether a given string is a valid parameter).

public static boolean isValidParam(String str) {
    return StringUtils.isNotBlank(str) && !"null".equalsIgnoreCase(str);
}

isValidParam is not in the scope of responsibility of String, and isValidParam should continue to be placed in XxxBizUtils. If you change the method name to isNotBlankAndNotEqualsIgnoreCaseNullLiteral, it is ok. :) However, I advise you not to do so.

0 2 1
Share on

mizhou

2 posts | 0 followers

You may also like

Comments

mizhou

2 posts | 0 followers

Related Products