Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

java

enums

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?

like image 749
m81 Avatar asked Oct 17 '19 23:10

m81


2 Answers

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

like image 105
GotoFinal Avatar answered Nov 15 '22 01:11

GotoFinal


If all implementations of an interface extend a certain class, then all instances of the interface are instances of the class as well; therefore, this interface must also extend this class (read more about is-a relationship and subtyping).

Since each type in the extends clause of an interface declaration must be an interface type, you can't make your interface extend Enum class; therefore, you can't prevent not-enum classes from implementing your interface.

You can't even achieve it by replacing interface ReadableEnum extends Enum with abstract class ReadableEnum extends Enum since enum types must not be declared abstract.


But you can still make it harder to implement ReadableEnum for not-enum classes by making it extend IEnum interface which consists of all public Enum methods:

public interface IEnum<E extends Enum<E>> extends Comparable<E> {
    String name();
    int ordinal();
    Class<E> getDeclaringClass();
}
interface ReadableEnum<E extends Enum<E> & ReadableEnum<E>> extends Readable, IEnum<E> {
    int read(CharBuffer cb) throws IOException;

    default int readAll(CharBuffer cb) throws IOException {
        return ReadableEnumUtils.readAll(getDeclaringClass(), cb);
    }
}

Now enums can implement ReadableEnum just by implementing read method, while other classes have to implement name, ordinal, getDeclaringClass and compareTo as well.

like image 24
Bananon Avatar answered Nov 15 '22 00:11

Bananon