javastatic-methods

Dispatching static methods on generic types


I want to achieve something like this:

interface Parent {
     static int hi() { return 1; }
}

class Child implements Parent {
     static int hi() { return 0; }
}

class Child2 implements Parent {
    static int hi() { return 3; }
}

class Connector<T extends Parent> {
    int getHi() { return T.hi(); }
}

class Main {
    public static void main(String[] args) {
        Connector<Child> c = new Connector();
        System.out.println(c.getHi()); // want it to print 0, but prints 1
        
        Connector<Child2> c2 = new Connector();
        System.out.println(c2.getHi()); // want it to print 3, but prints 1
    }
}

where the type could be inferred by Java's type system and as a result call the correct getHi() method.

Currently, the way I work around this is by handling it in the runtime by doing:

interface Parent {}

class Child implements Parent {
     static int hi() { return 0; }
}

class Child2 implements Parent {
    static int hi() { return 3; }
}

class Connector {
    static <T extends Parent> int getHi(Class<T> classType) {
        try {
            return (int) MethodHandles.lookup().findStatic(classType, "hi", MethodType.methodType(int.class)).invokeExact();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

class Main {
    public static void main(String[] args) {
        System.out.println(Connector.getHi(Child.class)); // prints 0
        System.out.println(Connector.getHi(Child2.class)); // prints 3
    }
}

Ideally, I would like to move this to something like the first option, where I handle static method calling in compile time (since MethodHandles.lookup().findStatic() has quite a bit of runtime overhead). Is there any way to do something like the first option?


Solution

  • You want factories.

    They have an (undeserved) negative public opinion, and their name is unfortunate; it suggests their primary (or even only) purpose is to make objects when that's not at all required.

    What you're doing here is to add the concept of a type hierarchy to things that do not require an instance as a context.

    Java doesn't allow this. static elements are fully opted out of the type system. You should consider all static stuff as if it lived entirely outside of all types. The one and only reason that the java compiler forces you into putting static stuff inside types, is because of namespacing - in java, types also serve as the primary/only namespace vehicle. To refer to a thing in java you must start with that things type; the lang spec does not have any way to refer to anything else. Hence, static things have to be in types so that you can refer to them. And that is the one and only relation they have to the type they are in.

    Hence why your code as above does not work and cannot work without reflection, which is ugly code (in the objective sense that reflective code has no type checking at all; any errors you make or any inconsistencies introduced due to code changes later won't show up as errors unless you run into a bug or have a unit test that happens to trigger that bug). Also, you indicated you didn't want it, so we can move away from that as a feasible solution here.

    Factories are the solution:

    1. If only non-static things can partake in type hierarchies, because static things effectively don't participate in the type they are declared in, and type hierarchies, obviously only apply to types,
    2. But non-static things necessarily require an instance in order to use them, then...
    3. Therefore one must create an instance that represents the class as a whole.
    4. ... and an instance of java.lang.Class is no good, as you can't actually write any code in that; it's a final class in java.lang, you cannot modify it.

    Therefore 5. Factories.

    A factory is simply the type itself one abstraction level higher: Imagine:

    abstract class Animal {}
    class Dog extends Animal { ... }
    class Cat extends Animal { ... }
    
    Dog fido = new Dog();
    Cat gizmo = new Cat();
    

    Here we specific animals fido, and gizmo. They are animals. You can hug them and feed them. Then we have a type hierarchy that describes the class they belong to.

    If that 'thing that describes the class they belong to' needs to become an instance in order to solve our issues, then we'd create:

    abstract class AnimalDescriptor<A extends Animal> {}
    class DogDescriptor extends AnimalDescriptor<Dog> {}
    class CatDescriptor extends AnimalDescriptor<Cat> {}
    
    DogDescriptor DOG_DESCRIPTOR = new DogDescriptor();
    

    Where an AnimalDescriptor is the type, and instances of it are equivalent to 'the concept of a dog'. DOG_DESCRIPTOR isn't an animal you can feed and hug; it's the concept 'dogs' as an instance.

    One extremely obvious thing one might want to ask 'the concept 'dogs' as an instance' is 'make me a new dog'. Because constructors are essentially static methods (you call them without needing an instance of the class, and they cannot be inherited at all - just like static methods), and often one runs into the problem of needing to abstractize the act of making new instances, then the above is required, and the obvious name for it all is Factory.

    But the concept is more generic than that. For example, you often see this is a trivial code example:

    public class Animal {
      public abstract String noise();
    }
    
    public class Dog extends Animal {
      @Override public String noise() {
        return "Woof!";
      }
    }
    

    But this is ridiculous code. Totally weird. Why is Dog.noise() an instance method here? It has nothing to do with fido. There is no need to have an actual dog to know what noises dogs make. The code says: All dogs bark. There is no .setNoise() or any other way to modify the response. The actual dog instance you invoke noise() on is irrelevant.

    Hence, the better way to do this is to use the double hierarchy and stick noise() in DogDescriptor.

    The nature of these type descriptors is that they tend to be singletons: The concept 'DogDescriptor' as a class does lend itself to an instance, but only one of them. However, even that depends on the situation; sometimes you can have multiple different 'takes' on the concept.

    Going back to your example, you'd end up at something like:

    interface Parent {
      // hi has no business here.
    }
    
    class Child implements Parent {
      // hi has no business here.
    }
    
    class Child2 implements Parent {
      // hi has no business here either
    }
    
    abstract class Connector<T extends Parent> {
        int getHi() {
          // default impl
          return 1;
        }
    }
    
    class Child1Connector extends Connector<Child1> {
      public static final Child1Connector INSTANCE = new Child1Connector();
      private Child1Connector() {}
      int getHi() {
        return 0;
      }
    }
    
    class Child2Connector extends Connector<Child2> {
      public static final Child2Connector INSTANCE = new Child2Connector();
      private Child2Connector() {}
      int getHi() {
        return 3;
      }
    }
    
    class Main {
        public static void main(String[] args) {
            var c = Child1Connector.INSTANCE;
            System.out.println(c.getHi()); // prints 0 as desired.
            
            var c2 = Child2Connector.INSTANCE;
            System.out.println(c.getHi()); // prints 3 as desired.
        }
    }
    

    And of course you could write:

    class IntentionallyLeaveItUnbound {
      void example1(Connector<?> c) {
         System.out.println(c.getHi());
      }
    
      <T extends Parent> T example2(Connector<T> c) {
        return c.create();
      }
    }
    

    Where create() is specced as public T create().

    All type checked, no need to involve reflection.