Java 8: Self-made polysaccharide switch

background

JDK 12 and JDK 13 have been released, along with many minor improvements to Java syntax, such as switch, which we are very familiar with:

Before JDK12

switch (type) {
    case "all":
        System.out.println("List all posts");
        break;
    case "auditing":
        System.out.println("List posts in review");
        break;
    case "accepted":
        System.out.println("List approved posts");
        break;
    case "rejected":
        System.out.println("List posts that have not been approved");
        break;
    default:
        System.out.println("parameter'type'Error, please check");
        break;
}

JDK12

switch (type) {
    case "all" -> System.out.println("List all posts");
    case "auditing" -> System.out.println("List posts in review");
    case "accepted" -> System.out.println("List approved posts");
    case "rejected" -> System.out.println("List posts that have not been approved");
    default -> System.out.println("parameter'type'Error, please check");
}

JDK13

String value = switch (i) {
    case  0 -> "zero"
    case  1 -> "one"
    case  2 -> "two"
    default -> "many"
};

The new features are great, but today the most popular version in the industry is still JDK8, so it seems that there is no way to use such a comfortable switch in the production environment. Fortunately, we still have Lambda, which is called "do it yourself and have enough food and clothing". Let's try to make something similar to JDK12 & JDK13's swtich, so as to give us plain coding life and add some sugar.

Realization

switch of standard JDK12

First, we define a Switch class, and then it receives a generic parameter, similar to the Java switch statement, which needs to accept a parameter.

public class Switch<T> {
    
    private final T input;
    
    private Switch(T input) {
        this.input = input;
    }
    
    public static <T> Switch<T> on(T input) {
        return new Switch<>(input);
    }   
}

By using the static method on, we can construct a Switch. Then, we define a Predicate to represent the current condition:

public class Switch<T> {

    private final T input;

    private Predicate<T> condition;

    private Switch(T input) {
        this.input = input;
    }

    public static <T> Switch<T> on(T input) {
        return new Switch<>(input);
    }

    public Switch<T> is(T target) {
        // Determine whether the target to be detected is equal to target
        condition = Predicate.isEqual(target);
        return this;
    }
}

The function of is method is to define the current conditional as judging whether the target to be detected is equal to the target to be transmitted. Since conditions are introduced, we can naturally let users define conditions themselves:

public Switch<T> when(Predicate<T> condition) {
    Objects.requireNonNull(condition);

    this.condition = condition;
    return this;
}

Then we can define the case... break function in the switch statement:

public Switch<T> thenAccept(Consumer<T> action) {
    Objects.requireNonNull(action);

    if (condition == null) {
        throw new IllegalStateException("A condition must be set first.");
    }

    if (condition.test(input)) {
        action.accept(input);
    }

    return this;
}

Is something wrong? Yes, switch ing can only satisfy one case. If we set the conditions ourselves, there may be situations where multiple conditions are satisfied - that's not what we expected. We can define a boolean tag to indicate whether there is a certain satisfying condition set by the user. If there is a satisfying condition, the chain call after the consumer method is directly short-circuited.

public class Switch<T> {
        
      ...
      
    /**
     * Are there any conditions that have been met?
     */
    private boolean accepted;

    public Switch<T> is(T target) {
        if (!accepted) {
            // Determine whether the target to be detected is equal to target
            condition = Predicate.isEqual(target);
        }

        return this;
    }

    public Switch<T> when(Predicate<T> condition) {
        Objects.requireNonNull(condition);

        if (!accepted) {
            this.condition = condition;
        }

        return this;
    }

    public Switch<T> thenAccept(Consumer<T> action) {
        Objects.requireNonNull(action);

        if (condition == null) {
            throw new IllegalStateException("A condition must be set first.");
        }

        if (!accepted && condition.test(input)) {
            action.accept(input);

            // The condition that the marker has been satisfied already exists
            accepted = true;
        }

        return this;
    }
}

What seems to be missing? Yes, switch also has a default... break. We define an elseAccept method that is called if and only if no previous condition is satisfied:

public void elseAccept(Consumer<T> action) {
    Objects.requireNonNull(action);

    // No condition has ever been met before.
    if (!accepted) {
        action.accept(input);
    }
}

OK, let's write a little demo to compare and feel:

public class Demo {

    public static void main(String[] args) {
        // Get a parameter passed by the front end
        String type = getType();

        switch (type) {
            case "all":
                System.out.println("List all posts");
                break;
            case "auditing":
                System.out.println("List posts in review");
                break;
            case "accepted":
                System.out.println("List approved posts");
                break;
            case "rejected":
                System.out.println("List posts that have not been approved");
                break;
            default:
                System.out.println("parameter'type'Error, please check");
                break;
        }

        Switch.on(type)
                .is("all")
                .thenAccept(t -> System.out.println("List all posts"))
                .is("auditing")
                .thenAccept(t -> System.out.println("List posts in review"))
                .is("accepted")
                .thenAccept(t -> System.out.println("List approved posts"))
                .is("rejected")
                .thenAccept(t -> System.out.println("List posts that have not been approved"))
                .elseAccept(t -> System.out.println("parameter'type'Error, please check"));

    }
}

Our Switch doesn't look as simple as JDK12's Switch, but it's simpler than the Switch statement before JDK12 -- and chain calls, with Lambda, are more comfortable to write. ~More importantly, we all know that Switch statements support a limited number of types (integers, enumerations, characters, strings), and our custom Swatch supports any type, more than Lambda. Such as:

Object value = getValue();

Switch.on(value)
        .is(null)
        .thenAccept(v -> System.out.println("value is null"))
        .is(123)
        .thenAccept(v -> System.out.println("value is 123"))
        .is("abc")
        .thenAccept(v -> System.out.println("value is abc"))
        .is(Arrays.asList(1, 2, 3))
        .thenAccept(v -> System.out.println("value is [1, 2, 3]"))
        .elseAccept(v -> System.out.println("Unknown value"));

And we also support custom conditional statements, so obviously our Switch can be used instead of if-else statements:

Object value = getValue();

Switch.on(value)
        .is(null)
        .thenAccept(v -> System.out.println("value is null"))
        .when(v -> v instanceof Integer)
        .thenAccept(v -> System.out.println("value is Integer"))
        .when(v -> v instanceof String)
        .thenAccept(v -> System.out.println("value is String"))
        .when(v -> v instanceof Boolean)
        .thenAccept(v -> System.out.println("value is Boolean"))
        .elseAccept(v -> System.out.println("Unknown type of value"));

// Equivalent if-else
if (value == null) {
    System.out.println("value is null");
} else if (value instanceof Integer) {
    System.out.println("value is Integer");
} else if (value instanceof String) {
    System.out.println("value is String");
} else if (value instanceof Boolean) {
    System.out.println("value is Boolean");
} else {
    System.out.println("Unknown type of value");
}

As for which is better to use and to read more comfortably, "benevolence sees the benevolence, wise men see wisdom".

Swatch of JDK13

In JDK13, the function of evaluating switch statement is given. Of course, we can easily transform our Switch to support this function. First, add the generic type of the return value:

public class Switch<T, R> {

    private final T input;
    private R output;

    /**
     * Does the output already exist?
     */
    private boolean outputted;
}

Then, two methods are added to evaluate when the conditions are satisfied and when the conditions are not satisfied:

public Switch<T, R> thenGet(R value) {
    if (condition == null) {
        throw new IllegalStateException("A condition must be set first.");
    }

    // Meeting conditions
    if (!outputted && condition.test(input)) {
        output = value;
      
        // Markup already exists for output
        outputted = true;
    }

    return this;
}

public R elseGet(R value) {
    return outputted ? output : value;
}

Similarly, write a demo to see the effect:

int k = getNum();

String value = Switch.on(k)
        .is(0).thenGet("zero")
        .is(1).thenGet("one")
        .is(2).thenGet("two")
        .elseGet("many");

System.out.println(value);

However, compilation is not possible -- because Java can't derive the type of return value -- well, Java's generics... no way, add something to "hint":

public static <T, R> Switch<T, R> on(T input, Class<R> outType) {
    return new Switch<>(input);
}

Tell the Java compiler what type we want to return:

int num = getNum();

String value = Switch.on(num, String.class)
        .is(0).thenGet("zero")
        .is(1).thenGet("one")
        .is(2).thenGet("two")
        .elseGet("many");

System.out.println(value);

JDK13:

int num = getNum();

String value = switch (num) {
    case  0 -> "zero"
    case  1 -> "one"
    case  2 -> "two"
    default -> "many"
};

System.out.println(value);

In addition to specifying the type of return value, the two functions are the same; although not as concise as JDK13, our Swash looks very straightforward. And we can introduce functions to further enhance the evaluation function of our Switch:

public Switch<T, R> thenApply(Function<T, R> mapper) {
    Objects.requireNonNull(mapper);

    if (condition == null) {
        throw new IllegalStateException("A condition must be set first.");
    }

    if (!outputted && condition.test(input)) {
        output = mapper.apply(input);
        
        // Markup already exists for output
        outputted = true;
    }
    
    return this;
}

Write a demo:

long getLongValueWithIfElse(Object value) {

    if (value instanceof Long) {
        return ((Long) value);
    }

    if (value instanceof Integer) {
        return ((Integer) value).longValue();
    }

    if (value instanceof String) {
        return Long.parseLong((String) value);
    }

    if (value instanceof BigInteger) {
        return ((BigInteger) value).longValue();
    }

    if (value instanceof BigDecimal) {
        return ((BigDecimal) value).longValue();
    }

    return Long.MIN_VALUE;
}

long getLongValueWithSwitch(Object value) {
    return Switch.on(value, Long.class)

            .when(Long.class::isInstance)
            .thenApply(Long.class::cast)
            
            .when(Integer.class::isInstance)
            .thenApply(v -> ((Integer) v).longValue())

            .when(String.class::isInstance)
            .thenApply(v -> Long.parseLong((String) v))

            .when(BigInteger.class::isInstance)
            .thenApply(v -> ((BigInteger) v).longValue())

            .when(BigDecimal.class::isInstance)
            .thenApply(v -> ((BigDecimal) v).longValue())

            .elseGet(Long.MAX_VALUE);
}

extend

What improvements and ideas do you have? Welcome comments and exchanges.

Tags: Java Lambda JDK

Posted on Wed, 09 Oct 2019 04:51:14 -0700 by master82