Search code examples
javamethodsenums

A generic method for judge whether some parameter are attributes of one of enum value in an enum class


When I'm developing a SpringBoot project, I find that I'm often need to write code to judge whether the parameters from the API are my expected attribute of the enum value or not.
Such as:

@Getter
public enum Sample2Enum {
    A("a"),
    B("b"),
    C("c"),
    ;

    private final String str;

    Sample2Enum(String str) {
        this.str = str;
    }
    
    public static boolean isAttributeEquals(String value) {
        boolean isAttributeEquals = false;
        for (Sample2Enum sample2Enum : Sample2Enum.values()) {
            if (sample2Enum.getStr().equals(value)) {
                isAttributeEquals = true;
                break;
            }
        }
        return isAttributeEquals;
    }
}

I think the implementation logic of the isAttributeEquals method in any enum class is similar.
So I want to explore a generic method that can judge whether some parameter are attributes of one of enum value in an enum class. In my opinion, the generic method is like this:

    /**
     * Judge whether the given parameter is an attribute of an enum value in the given enum class.
     * @param enumClass Target enum class
     * @param methodReference The judge method reference
     * @param values the parameters need to be checked
     * @return true if the parameter is an attribute of an enum value in the given enum class, false otherwise
     */
    public static <T> boolean isValidEnumAttribute(Class<T> enumClass, MethodReference methodReference, Object... values) {
        T[] enums = enumClass.getEnumConstants();
        boolean isEnum = false;
        for (T enumValue : enums) {
            isEnum = methodReference(values); // call method reference to judge whether the enum value's attribute matches the attribute
            if (isEnum) break;
        }
        return isEnum;
    }

Note. Because some enum value will have more than one attributes, the parameter values is corresponding the multiple attributes of one enum value.
The methodReference is a parameter I imaged. In my vision, the judge step should be left to the caller to decide.
Such as I can difine two methods in the Sample2Enum like this:

public boolean isAttributeEquals2(String value) {
        if (this.equals(Sample2Enum.B)) {
            return false;
        }
        return this.getStr().equals(value);
}
public boolean isAttributeEquals3(String value) {
        return this.getStr().equals(value);
}

And then, I can call the isValidEnumAttribute method like this (Just as an example, don't care about the details):

EnumUtils.isValidEnumAttribute(Sample2Enum.class, Sample2Enum::isAttributeEquals2, "a");
EnumUtils.isValidEnumAttribute(Sample2Enum.class, Sample2Enum::isAttributeEquals3, "b");

If this generic function has already been implemented, we just need to define some 'judge step' in the enum class, and then call the isValidEnumAttribute method.
But it is not that simple.
I don't know how to deliver the 'judge step method' from external to the isValidEnumAttribute.
The only easy way is seems like the caller need to hard code to give 'judge step method name' to isValidEnumAttribute method, and use reflect to get the real 'judge step method' in isValidEnumAttribute method. It works like this:

/**
 * Judge whether the given parameter is an attribute of an enum value in the given enum class.
 * @param enumClass Target enum class
 * @param methodName the name of the method to be called
 * @param values the parameters need to be checked
 * @return true if the parameter is an attribute of an enum value in the given enum class, false otherwise
 */
public static <T> boolean isValidEnumAttribute(Class<T> enumClass, String methodName, Object... values) throws Exception {
    T[] enums = enumClass.getEnumConstants();
    Class<?>[] params = new Class[values.length];
    for (int i = 0; i < values.length; i++) {
        params[i] = values[i].getClass();
    }
    Method method = enumClass.getMethod(methodName, params);
    boolean isEnum = false;
    for (T enumValue : enums) {
        isEnum = (boolean) method.invoke(enumValue, values);
        if (isEnum) break;
    }
    return isEnum;
}

The call is here:

System.out.println(EnumUtils.isValidEnumAttribute(Sample2Enum.class, "isAttributeEquals2", "a"));

This is not my expectation, I don't expect appear the hard code. Anyone know how to optimize that? Or who has a better way to implement my thought?
I also doubt that the isValidEnumAttribute method I imaged whether there will be performance penalty as a result. But now I just want to know there is a way to do my expect or not.


Solution

  • Doing this in exactly the way you want is not possible in Java, as that requires variadic generics. You want your method to take an arbitrary number of values, and you also want the type of the method reference to match the types of the values.

    I would capture the values parameter in a lambda passed to the methodReference parameter - remove the values parameter completely.

    public static <T extends Enum<?>> boolean isValidEnumAttribute(Class<T> enumClass, Predicate<T> methodReference) {
        T[] enums = enumClass.getEnumConstants();
        boolean isEnum = false;
        for (T enumValue : enums) {
            isEnum = methodReference.test(enumValue);
            if (isEnum) break;
        }
        return isEnum;
    }
    

    On the caller's side, instead of doing

    EnumUtils.isValidEnumAttribute(Sample2Enum.class, Sample2Enum::isAttributeEquals2, "a")
    

    It would do

    EnumUtils.isValidEnumAttribute(Sample2Enum.class, x -> x.isAttributeEquals2("a"))
    

    You can also give up on values being an arbitrary-length array. Let the caller create its own classes composing of all the values they want to match against. The signature of isValidEnumAttribute would look like this:

    public static <T extends Enum<?>, U> boolean isValidEnumAttribute(
        Class<T> enumClass, 
        BiPredicate<T, U> methodReference, 
        U values
    )
    

    Or, you can keep Object... values, but of course then the methods that the callers write can only work with Objects, and the methods would need to validate the types and length of the Object[] parameter.

    public static <T extends Enum<?>> boolean isValidEnumAttribute(
        Class<T> enumClass, 
        BiPredicate<T, Object[]> methodReference, 
        Object... values
    )