Rust versus Java static methods and invoking on generic type parameters

Viewed 147
  • In rust we can do something like this:

    trait MyTrait {
        fn my_func();
    }
    
    struct MyStruct {}
    
    impl MyTrait for MyStruct {
        fn my_func() {
            // this gets invoked
        }
    }
    
    fn my_func<B: MyTrait>() {
        // This here
        B::my_func();
    }
    
    fn main() {
        my_func::<MyStruct>();
    }
    
  • In java we can't do something like this:

    interface MyInterface {
        public static void myFunc() {
            // This gets invoked
        }
    }
    
    class MyClass implements MyInterface {
        public static void myFunc() {
    
        }
    }
    
    public class Main {
    
        static <T extends MyInterface> void myFunc() {
            // This here
            T.myFunc();
        }
    
        public static void main(String[] args) {
            Main.<MyClass>myFunc();
        }
    }
    

Now there are other questions about why java doesn't allow overriding/enforcing of static methods in java, but does this specific example not work because of monomorphization versus type erasure? I'm trying to understand the deeper reason why. My guess is that since there's only one implementation after type erasure, and static invocations need to be compiled in, it doesn't work. Is this the actual reason?

2 Answers

As you pointed out, type erasure takes place and Java generics can be considered syntactic sugar. It is used for type checking, but in the resulting byte code, it will be replaced.

If it is an unbound type, it would be replaced by Object, but in your example, it is a bound type (T extends MyInterface), so it will be replaced by MyInterface instead. That is why MyInterface.myFunc will be called.

Why does the compiler not resolve it like in Rust or C++? I assume it would be possible, so I can only speculate about the design decisions in Java. Java is by nature more dynamic then Rust. New classes can be defined during annotation processing, but they can also be dynamically created on the fly. That property does not play well with compile time resolution, as the compiler cannot know all classes at compile time.

Java is primarily a object-oriented language, while Rust and C++ put more emphasis on the compile phase. Although Java is statically typed, it is more dynamic and more is done at runtime. That makes it more flexible, but it sacrifices performance.

The "Java way" to express the Rust code that you provided, would be to use normal functions instead of static functions. The effect would be similar, but you end up with dynamic-dispatch. Rust like C++ follows the zero-overhead principle, so being forced to use a dynamic-dispatch would violate that principle. In Java, on the other hand, it is not a design goal.

Typically, the JIT should still be able to optimize it (i.e. eliminate the dynamic-dispatch and inline the code). But there is no guarantee like in the equivalent Rust (or C++), where everything is decided at compile time.

I wanted to state clearly that static is not inherited in java. So the approach would need an instantiation, be it a singleton.

interface MyInterface {
    public void myFunc() {
        // This gets invoked
    }
}

class MyClass implements MyInterface {
    @Override // Does not work with static.
    public void myFunc() {

    }
}

public class Main {

    static void myFunc(MyInterface obj) {
        // This here
        obj.myFunc();
    }

    public static void main(String[] args) {
        Main.myFunc(new MyClass());
    }
}

You could hide the instance obj, make it static or whatever.

It might be that lambdas are a better solution:

        Main.myFunc(MyClass::myStaticFunc);
Related