Can a Java interface be defined such that only Enums can extend it?

mchen.ja :

There's no particular reason I want to do this - I'm just wondering if it is possible. If it helps, here's a fictional situation in which it could be used:

Imagine a type of Enum which is used as a read-only data source, such that each value of the Enum contains distinct content. The Enum implements Readable. Now, suppose we want a method that reads all values of the Enum into a single buffer. That could be implemented as a static utility method in a helper class (see below).

public class ReadableEnumUtils {
    /** reads data from all enum values into the charbuffer */
    public static <T extends Enum<T> & Readable> int readAll(Class<T> clazz, CharBuffer cb) throws IOException {
        int total = 0;
        for (T e : clazz.getEnumConstants()) {
            int intermediate = e.read(cb);
            if (intermediate < 0) {
                throw new IllegalArgumentException("The enum value \'" + e.name() + "\' had no data to read.");
            }
            total += intermediate;
        }
        return total;
    }
}

Preferably, that method would be declared in an interface, but that could be confusing, since it wouldn't be immediately obvious that non-Enum classes should not implement such a method. Ideally, the interface could be defined in such a way that the compiler would ensure it was only implemented by subclasses of Enum. Here's an example of what that interface could possibly look like:

interface ReadableEnum extends Readable {
    int read(CharBuffer cb) throws IOException;

    int readAll(CharBuffer cb) throws IOException;
}

I don't think it's possible to make the compiler ensure that ReadableEnum is only implemented by subclasses of Enum - is that correct?

GotoFinal :

Java by default does not support anything like that, you ask why not with link to specification, but there is no special reason why, just no one decided to add such feature, you could propose it yourself - but then you will probably learn that they don't think it's something needed and will not add this to the language.

But java provides pretty powerful option to implement this by yourself: annotation processing.
I've created simple java 8 maven project with annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface EnumInterface {}

And with special processor

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.util.*;

@SupportedAnnotationTypes("com.gotofinal.enuminterface.EnumInterface")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EnumInterfaceProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Messager messager = processingEnv.getMessager();
        Types typeUtils = processingEnv.getTypeUtils();

        // first we scan for all interfaces marked with this annotation
        List<TypeElement> enumOnlyInterfaces = new ArrayList<>();
        for (Element rootElement : roundEnv.getRootElements()) { // getRootElements should return all types being compiled
            if (! (rootElement instanceof TypeElement)) {
                continue;
            }
            TypeMirror typeMirror = rootElement.asType();
            // we check if this class have our annotation, we could also here check if this is an interface (by checking if it does not extend Object directly) and throw error otherwise
            if (rootElement.getAnnotation(EnumInterface.class) != null) {
                enumOnlyInterfaces.add((TypeElement) rootElement);
            }
        }

        // and now we scan for any non enum types that implement this interface
        for (Element rootElement : roundEnv.getRootElements()) {
            if (! (rootElement instanceof TypeElement)) {
                continue;
            }
            TypeElement type = findImplementedInterface(rootElement.asType(), enumOnlyInterfaces, typeUtils);
            if (type == null) {
                continue;
            }
            if (! (rootElement.asType() instanceof DeclaredType)) {
                continue;
            }

            // it's fine if it is an enum
            if (this.isEnum(rootElement.asType(), typeUtils)) {
                continue;
            }

            // and we print error to compiler
            messager.printMessage(Diagnostic.Kind.ERROR, "Interface " + type.getQualifiedName()
                                                                 + " can't be used on non enum class: " + ((TypeElement) rootElement).getQualifiedName());
        }
        return false;
    }

    public TypeElement findImplementedInterface(TypeMirror type, List<TypeElement> interfaces, Types types) {
        for (TypeElement anInterface : interfaces) {
            // types.isSubtype(typeA, typeA) would return true, so we need to add this equals check
            if (!anInterface.asType().equals(type) && types.isSubtype(type, anInterface.asType())) {
                return anInterface;
            }
        }
        return null;
    }

    // maybe there is better way to do this... but I just scan recursively for a subtype with java.lang.Enum name, so it's not perfect but should be enough.
    public boolean isEnum(TypeMirror type, Types types) {
        for (TypeMirror directSupertype : types.directSupertypes(type)) {
            TypeElement element = (TypeElement) ((DeclaredType) directSupertype).asElement();
            if (element.getQualifiedName().contentEquals("java.lang.Enum")) {
                return true;
            }
            if (isEnum(directSupertype, types)) {
                return true;
            }
        }
        return false;
    }
}

And register it in META-INF/services/javax.annotation.processing.Processor file:

com.gotofinal.enuminterface.EnumInterfaceProcessor

This code could be probably improved a lot, I've never wrote any annotation processor before. But when we will create another maven project and declare this one as dependency and write code like this:

@EnumInterface
interface TestInterface {}

enum TestEnum implements TestInterface {}

class TestClass implements TestInterface {}

We will not be able to compile it with error:

Interface com.gotofinal.enuminterface.TestInterface can't be used on non enum class: com.gotofinal.enuminterface.TestClass

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=304976&siteId=1