Make a wheel for Java - Chain

Recently issued Extension method of simulating C# in Java "This article simulates the extension method for Java. But after all, without syntax support, it is still inconvenient to use, especially when it is necessary to continuously interleave the "extension methods" implemented in different classes, switching objects is very cumbersome. As mentioned earlier, the reason why I thought of studying "extension method" is actually just for "chain call".

Then, why not start from the original requirements and only solve the problem of chain call instead of considering broader extension methods? The previous article has studied the implementation of chain call through Builder mode. This method needs to define its own extension method class (i.e. Builder), which is still cumbersome.

Chain prototype

The main feature of Chain call is that after using an object, you can continue to use the object... All the time. You see, there are only two things here: one is to provide an object; The second is to use this object -- isn't this Supplier and Consumer? Java provides exactly two functional interfaces with the same name in the java.util.function package. In this way, we can define a Chain class and continuously "consume" it from a Supplier. Such a simple Chain prototype comes out:

public class Chain<T> {
    private final T value;

    public Chain(Supplier<? extends T> supplier) {
        this.value = supplier.get();
    }

    public Chain<T> consume(Consumer<? super T> consumer) {
        consumer.accept(this.value);
        return this;
    }
}

Now, if we have a Person class, it has some behavior methods:

class Person {
    public void talk() { }
    public void walk(String target) { }
    public void eat() { }
    public void sleep() { }
}

It is the business scenario in the previous article: after negotiation, go out, eat, come back and sleep. The non chain trial call is as follows:

public static void main(String[] args) {
    var person = new Person();
    person.talk();
    person.walk("Hotel");
    person.eat();
    person.walk("home");
    person.sleep();
}

If Chain is used to string together, it is:

public static void main(String[] args) {
    new Chain<>(Person::new).consume(Person::talk)
        .consume(p -> p.walk("Hotel"))
        .consume(Person::eat)
        .consume(p -> p.walk("home"))
        .consume(Person::sleep);
}

The Chain encapsulation has been completed above. It is still quite simple, but there are two small problems:

  1. There are too many words in consumption (), which is troublesome to write. If you change the name, do is very appropriate, but it's a keyword... It's better to change it to act;
  2. Link calls are often used in expressions. There is a return value returned, so you have to add a Chain::getValue()

Perfect Chain

In fact, in the process of chain call, it is not necessarily just "consumption". It may also need "conversion". In the words of programmers, it is map() - pass in the current object as a parameter and get another object after calculation. You may use map() more in java stream, but the scenario here is more like Optional::map.

Let's take a look at the of Optional::map source code:

public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

It can be seen that the logic of this map() is very simple, which is to encapsulate the result of Function operation into an Optional object. We can also do this in Chain:

public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
    return new Chain<>(() -> mapper.apply(value));
}

It is found here that although the idea of using Supplier is correct, it is not easy to construct the Chain object directly from the "value" - of course, you can add an overload of the constructor to solve this problem, but I want to write two static methods to implement it like option and hide the constructor at the same time. The modified complete Chain is as follows:

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class Chain<T> {
    private final T value;

    public static <T> Chain<T> of(T value) {
        return new Chain<>(value);
    }

    public static <T> Chain<T> from(Supplier<T> supplier) {
        return new Chain<>(supplier.get());
    }

    private Chain(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public Chain<T> act(Consumer<? super T> consumer) {
        consumer.accept(this.value);
        return this;
    }

    public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
        return Chain.of(mapper.apply(value));
    }
}

Continue to transform Chain

map() always returns a new Chain object. If there are many map steps in a process, many Chain objects will be generated. Can it be solved in a Chain object?

Generic types are used to define the Chain, and the generic types will be erased after compilation, which is not much different from our direct definition of value as Object. In that case, is it feasible to directly replace the value instead of generating a new Chain Object when map()—— It does work. However, on the one hand, we need to continue to use generics to constrain consumer s and mapper s. On the other hand, we need to carry out forced type conversion internally, and ensure that there will be no problems with this conversion.

Theoretically, Chain handles Chain calls, one ring after another, and the results of each ring are saved in value for the beginning of the next ring. Therefore, under generic constraints, no matter how it changes, there will be no problem. If the theory is feasible, it is better to practice:

// Because there are a lot of type conversions (logical validation is feasible), you need to ignore the relevant warnings
@SuppressWarnings("unchecked")
public class Chain<T> {
    // Declare value as an Object type to reference various types of values
    // At the same time, remove the final modification to make it variable
    private Object value;

    public static <T> Chain<T> of(T value) {
        return new Chain<>(value);
    }

    public static <T> Chain<T> from(Supplier<T> supplier) {
        return new Chain<>(supplier.get());
    }

    private Chain(T value) {
        this.value = value;
    }

    public T getValue() {
        // Where value is used, it is necessary to convert value to the generic parameter type of chain < >, the same below
        return (T) value;
    }

    public Chain<T> act(Consumer<? super T> consumer) {
        consumer.accept((T) this.value);
        return this;
    }

    public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
        // mapper's calculation result doesn't matter what type it is. You can assign value to Object type
        this.value = mapper.apply((T) value);
        // Although the returned Chain is still itself (just this object), the generic parameter must be changed to U
        // After changing the type, the subsequent operations will be based on the U type
        return (Chain<U>) this;
    }
}

The last sentence of type conversion (chain < U >) this is very spiritual. You can do this in Java (because there is type erasure), but you can't do it in C# anyway!

Write another piece of code to test:

public static void main(String[] args) {
    // Note: the operation of String will generate a new String object, so use map
    Chain.of("     Hello World  ")
        .map(String::trim)
        .map(String::toLowerCase)
        // ↓ split String into String [], where incompatible types are converted
        .map(s ->s.split("\s+"))
        // ↓ consume this String [], and print it out in sequence
        .act(ss -> Arrays.stream(ss).forEach(System.out::println));
}

The output is as expected:

hello
world

epilogue

In order to solve the problem of chain call, we studied the extension method in the last article, which is a little "excessive". This time, when we return to the source, we will handle the chain call.

If you have ideas in the research process, you might as well give it a try. If you find a similar processing method in the JDK, don't be afraid to look at the source code - after all, OpenJDK is open source!

Another point is that the type erasure feature of Java generics sometimes does bring inconvenience, but sometimes it is really convenient!

Tags: Java

Posted on Sat, 04 Dec 2021 17:09:32 -0500 by tmc01