12

As of Java 8 you can have default or static methods implemented in Interfaces as the below

public interface DbValuesEnumIface<ID, T extends Enum<T>> {
   T fromId(ID id);

   ID getId();
   static String getDescriptionKey(){
      return "this is a test";
   }
}

I would like to declare the above with the static method having a signature that uses bounds defined by the implementing classes since the method's implementation should be the same for all,with the only thing different should be the generics declared, as such:

public interface DbValuesEnumIface<ID, T extends Enum<T>> {

   public static T fromId(ID id) {
        if (id == null) {
            return null;
        }
        for (T en : T.values()) {
            if (en.getId().equals(id)) {
                return en;
            }
        }
    }

    ID getId();

    String getDescriptionKey();
}
...
public enum Statuses implements DbValuesEnumIface<Integer,Statuses>

which breaks because T and ID are not static and cant be referenced from a static context.

So, how should the above be modified to compile successfully and if thats not possible, how the above should be implemented to achieve the desired purpose while avoiding code duplication within implementing classes .

Chetan Kinger
  • 15,069
  • 6
  • 45
  • 82
Leon
  • 408
  • 1
  • 6
  • 17
  • 1
    Not completely sure if this question specifically needs to be about Java 8 and interfaces. The same rules apply for classes in general as well when it comes to `static` methods trying to use a type parameter defined for the `class`? – Chetan Kinger Nov 22 '17 at 09:11
  • @CKing the ability to have static methods in interfaces is Java8 specific and the problem in hand occurred through using an interface with generics. So added tags accordingly. The ``` breaks because T and ID are not static and cant be referenced from a static context ``` should probably still occur if DbValuesEnumIface was a class I merely extended in a subclass though if thats what you mean. There may be some kinks unique to it being an interface in the example so thats why this info was added. – Leon Nov 22 '17 at 09:16
  • 1
    No kinks. Class or interface. Generic type parameters follow the same rules. Static is static and instance is instance. Moreover, `T.values` where `T` is a class or method type parameter (regardless of `static`) defies the syntax rules for the language. Please do post an example that considers the language rules specially when the question is not language agnostic? More importantly, a question that talks about compilation errors should surely eliminate all compilation errors not related to the direct question IMO. – Chetan Kinger Nov 22 '17 at 09:24
  • 3
    Actually, there is an interface specific difference. For ordinary classes, you can invoke a `static` method of `SuperClass` like `SubClass.method(…)`, raising the expectation that the parameterization of the inheritance (e.g. `class SubClass extends SuperClass`) can be used, which disappointingly doesn’t work. In contrast, `static` methods of interfaces are not inherited in the first place. So you can’t do `Statuses.fromId(…)`, but only `DbValuesEnumIface.fromId(…)`, which obviously needs an explicit hint about `Statuses` being the class to be searched. – Holger Nov 22 '17 at 10:21
  • @Holger "static methods of interfaces are not inherited in the first place" Exactly. While you can use default methods (that one can argue lead to same problems) and Java can resolve conflicts, static methods are treated differently. – Leon Nov 22 '17 at 10:28

3 Answers3

8

Since there is no relationship between static methods and the class’s type parameters, which describe how instances are parameterized, you have to make the static method generic on its own. The tricky part is to get the declarations right to describe all needed constraints. And, as this answer already explained, you need to add a Class parameter, as otherwise, the implementation has no chance to get hands on the actual type arguments:

public interface DbValuesEnumIface<ID, T extends Enum<T>> {

   public static <ID, T extends Enum<T> & DbValuesEnumIface<ID, T>>
   T fromId(ID id, Class<T> type) {

        if (id == null) {
            return null;
        }
        for (T en : type.getEnumConstants()) {
            if (en.getId().equals(id)) {
                return en;
            }
        }
        throw new NoSuchElementException();
    }

    ID getId();

    String getDescriptionKey();
}

Note that the type parameters of the static method are independent from the class’s type parameter. You may consider giving them different names for clarity.

So now, given you enum Statuses implements DbValuesEnumIface<Integer,Statuses> example, you can use the method like Statuses status = DbValuesEnumIface.fromId(42, Statuses.class);


Note that for default methods, it is possible to access the actual type, as a method providing the enum type will be provided by the implementation. You only have to declare the presence of the method within the interface:

public interface DbValuesEnumIface<ID, T extends Enum<T>&DbValuesEnumIface<ID,T>> {

    public default T fromId(ID id) {
        if (id == null) {
            return null;
        }
        for (T en : getDeclaringClass().getEnumConstants()) {
            if (en.getId().equals(id)) {
                return en;
            }
        }
        throw new NoSuchElementException();
    }

    //no needed to implement it, inherited by java.lang.Enum
    Class<T> getDeclaringClass();

    ID getId();

    String getDescriptionKey();
}

However, the obvious disadvantage is that you need a target instance to invoke the method, i.e. Statuses status = Statuses.SOME_CONSTANT.fromId(42);

Holger
  • 285,553
  • 42
  • 434
  • 765
  • hm.. but that means that the generics declared at the interface level and the method level and un-related right? – Eugene Nov 22 '17 at 10:29
  • So if I get it right from yours and Eugene's answers, there is no way to possibly call fromID through Statuses and when calling the method through the Interface name, I must pass a 2nd argument in fromId for the class desired to be used. Right? – Leon Nov 22 '17 at 10:35
  • 2
    @Leon: yes. The only thing you can do, is to add a simple delegating method to each `enum`, e.g. add `public static Statuses fromId(Integer id) { return DbValuesEnumIface.fromId(id, Statuses.class); }` to `Statuses`, which is still better than repeating the entire implementation in each `enum`… – Holger Nov 22 '17 at 11:05
  • 1
    @Eugene: right, the method’s type parameters are unrelated (that’s why I even suggested giving them different names); it’s the constraints, i.e. `X extends Enum&DbValuesEnumIface` which enforce type arguments to have a compatible inheritance, as only `enum` types implementing `DbValuesEnumIface` with a proper parameterization can fulfill the constraints. – Holger Nov 22 '17 at 11:08
  • 2
    @Eugene: I just realized that there is a solution for `default` methods that doesn’t need the additional `Class` parameter (updated my answer), however, I don’t know whether having to invoke the method on an existing instance is better than providing a class literal as argument… – Holger Nov 22 '17 at 11:24
  • Thank you both for your detailed answers, you ve been very informative. – Leon Nov 22 '17 at 11:34
  • @Holger that's very neat! what I don't like (is that the very first time on your answers? :) ) is that you sort of say `Fruit apple = Fruit.banana.getId(42)` if I understood everything correctly... – Eugene Nov 22 '17 at 11:37
  • @Holger besides being an interface and anyone could extend this... unless we will finally get sealed interfaces in jdk-10. Your addition deserves a plus 2, unfortunately SO does not allow me to do this. – Eugene Nov 22 '17 at 11:48
  • 2
    @Eugene: anyone can implement the interface, but no-one can provide a type for `T` that isn’t an `enum` *and* an appropriate implementation of this interface at the same time. So if you create an inappropriate interface, it would still have to point to an appropriate implementation of the interface for `T`. You can’t write bullet-proof software, but you shouldn’t even try. Guiding the implementors into the right direction is enough. – Holger Nov 22 '17 at 12:18
2

There is no easy way as far as I can tell, first you need to change your method to default, you can read more here of why you can't use generics in a static context.

But even if you change it to default things are still not going to work, since you need to pass an instance or class type of the enum to that method, something like this:

public default T fromId(ID id, Class<T> t) {
        if (id == null) {
            return null;
        }
        for (T en : t.getEnumConstants()) {
            // dome something
        }
        return null;
}

Now you are hitting another problem, inside fromId - the only thing that you know is that T extends an enum - not your enum may be, thus getId (which seems that your enums have) are simply not known by the compiler.

I don't know an easy way to make this work besides declaring an interface, like :

interface IID {
    public int getId();
} 

making your enum implement it:

static enum My implements IID {
    A {

        @Override
        public int getId() {
            // TODO Auto-generated method stub
            return 0;
        }

    };
}

and change the declaration to:

public interface DbValuesEnumIface<ID, T extends Enum<My> & IID>
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • Very thorough answer, I ll read the link provided as well as your suggestion. If nothing better comes up today I ll make sure to accept this answer. Thank you. – Leon Nov 22 '17 at 09:50
  • 3
    If you have the `Class` parameter, you already have what you need, there is no reason to change the method from `static` to `default`. You only need to declare that the `Class` must be an `enum` and implement the desired interface and ironically, you show how to do such things in your last line. – Holger Nov 22 '17 at 10:13
1

You can change from static to default and it will compile successfully.

default EvaluationStatuses fromId(Integer id)

fhery021
  • 317
  • 2
  • 7
  • My bad, I accidentally pasted the wrong method signature. Right one within the interface should be T fromId(ID id) . I did try to change static to default but it doesnt recognize the values() method in for (T en : T.values()) . You may be on to something here though, I ll look into this approach some more as well. – Leon Nov 22 '17 at 09:11
  • 1
    @Leon It doesn't recognise the values method because `T` is a type parameter and not a reference. This has got nothing to do with interfaces or default methods or static methods as such. Perhaps read up on generics syntax first and move on to more trickier topics such as this one? – Chetan Kinger Nov 22 '17 at 09:27