How to replace run-time instanceof check with compile-time generics validation

前端 未结 6 898
有刺的猬
有刺的猬 2021-01-22 11:52

Got a little puzzle for a true Java Generics specialist... ;)

Let\'s say I have the following two interfaces:

interface Processor {
    void process(Foo          


        
相关标签:
6条回答
  • 2021-01-22 12:21

    What you can do with generics is:

    interface Processor<T extends Foo> {
      void process(T foo);
    }
    
    interface Foo {
      Processor<? extends Foo> getProcessor();
    }
    

    then

    class SomeProcessor implements Processor<SomeFoo> {
      public void process(SomeFoo foo) {
        // TODO
      }
    }
    
    class SomeFoo implements Foo {
      public Processor<? super SomeFoo> getProcessor() {
        return processor;
      }
    }
    

    This obviously means that you need a counterpart Foo.setProcessor() with wildcards, which in turn means you end up with an unchecked cast somewhere. This is unsafe from the language perspective and there is no way to go around this.

    You may check the processor instantiation with super type tokens, however this will happen at runtime, so at compile time you can't guarantee that the API is misused. Just document it the best you can.

    This pastie illustrates my idea. You can't model this problem to have type safety at compile time because the Foo interface would need a way to declare that a method returns a generic type instantiated with the current interface implementation. This would break inheritance and can't be done in Java.

    However with super type tokens you can check at runtime if the processor is the right type, which is always guaranteed at the language level if you clearly state in the API doc that only the processor is authorized to call setProcessor() on the Foo instance. If the client programmer disobeys and calls setProcessor() with an incorrect type, your class will still throw an exception, but at runtime.

    Addendum: Why you shouldn't parameterize Foo

    I feel like adding a little paragraph to explain why I don't like meriton's answer (the currently accepted one), and all other answers involving parameterizing the type Foo so that it becomes Foo<T>.

    Software fails: that's not a bad or unusual thing, it just happens. The sooner it fails the better, so we can fix it and avoid losses (usually money, but really anything someone may care of). This is one compelling reason to choose Java today: since the types are checked at compile time, a whole class of bugs never reaches production.

    This is where generics come in: another whole class of bugs is left out. Back to your case, someone suggests to add a type parameter to Foo so you can have type safety at compile time. Using the meriton's implementation, however, one could write illogical code that bypasses compiler checks:

    class SomeFoo implements Foo<WrongFoo> {}
    

    One may argue that it's not a compiler's job to tell if a program is semantically correct - and I agree - but a good language lets the compiler spot problems and hint solutions. Using a type parameter here is just shouting up a wise piece of software.

    Instead of relying on fragile conventions, you'd better sit down, close your IDE and think how can improve your design so that client code doesn't fail. Eventually, this is the primary reason why software fails: not because a language is strongly or dynamically typed, but because developers misunderstand APIs. Using a strongly typed language is a way to prevent one type of misusages.

    Your interface is very strange because it defines a getProcessor() but doesn't tell anything about who is in charge of setting the processor. If it's the very same Foo which provides its own processor, then type safety is only broken by a really dumb developer; but since it can be checked at runtime (refer to my demo with super type tokens) it can be easily guaranteed with a good development process (unit tests). The conclusion doesn't change too much if you define a setProcessor() or equivalent.

    The API you are looking for is impossible to describe in Java - BTW I think the same holds true for each and every object oriented language, since it breaks a primary rule of inheritence, that parent classes don't know their children (this in turn brings polymorphism in). Using a wildcard (as I hinted in the demo) is the closest you can go using Java generics, provides type safety and is straightforward to understand.

    I encourage you to only add a type parameter to the processor, and write good documentation and unit tests for your code instead of forcing the Java rules in the name of a type safety that really doesn't buy anything here.

    0 讨论(0)
  • 2021-01-22 12:23

    All that recursive bounds stuff is not necessary from a type safety point of view. Just something like this is sufficient:

    interface Processor<F> {
        void process(F foo);
    }
    
    interface Foo<F> {
        Processor<F> getProcessor();
    }
    
    class SomeFoo implements Foo<SomeFoo> {
        @Override
        SomeProcessor getProcessor() { ... }
    }
    
    class SomeProcessor implements Processor<SomeFoo> {
        @Override
        void process(SomeFoo foo) { ... }
    }
    
    // in some other class:
    <F extends Foo<? super F>> void process(F foo) {
        foo.getProcessor().process(foo);
    }
    

    In other words: I'm looking for a way to define a function in an object such that it can only return another object that processes the type of object that contained the function. Note: I'm not just talking about processing some least common denominator super-class of the object containing the function (above: Foo), but that object's actual class (above: SomeFoo).

    That's not possible to declare in Java. Instead, as you see above, you can have a generic method outside the class (or be a static method of that class), which takes both the object and the processor, and enforces the types on both.


    Update: If you want to be able to call process with any Foo, then we can integrate meriton's idea of getThis() into this code also (again, without recursive bounds):

    interface Foo<F> {
        Processor<F> getProcessor();
        F getThis();
    }
    
    class SomeFoo implements Foo<SomeFoo> {
        SomeProcessor getProcessor() { ... }
        SomeFoo getThis() { return this; }
    }
    
    
    // in some other class:
    <F> void process(Foo<F> foo) {
        foo.getProcessor().process(foo.getThis());
    }
    
    0 讨论(0)
  • 2021-01-22 12:25

    This is uglier than I thought. My take:

    interface Processor<F extends Foo<F>> {
        void process(F foo);
    }
    
    interface Foo<F extends Foo<F>> {
        Processor<F> getProcessor();
    }
    
    interface SomeFoo extends Foo<SomeFoo> {
        @Override
        SomeProcessor getProcessor();
    }
    
    interface SomeProcessor extends Processor<SomeFoo> {
        @Override
        void process(SomeFoo foo);
    }
    

    Now, the following will compile:

    <F extends Foo<F>> void process(F foo) {
        foo.getProcessor().process(foo);
    }
    

    but

    void process(Foo<?> foo) {
        foo.getProcessor().process(foo);
    }
    

    doesn't, because the compiler can not know that actual type of the passed foo is a subtype of its type parameter, as somebody could write:

        class Bar implements Foo<SomeFoo> { ... }
    

    We can work around this by requiring the subtypes of foo to implement a conversion to their type parameter:

    abstract class Foo<F extends Foo<F>> {
        abstract Processor<F> getProcessor();
    
        abstract F getThis();
    }
    
    class SomeFoo extends Foo<SomeFoo> {
        @Override
        SomeFoo getThis() {
            return this;
        }
    
        @Override
        Processor<SomeFoo> getProcessor() {
            return new SomeProcessor();
        }
    }
    

    Now, we can write:

    <F extends Foo<F>> void process(Foo<F> foo) {
        foo.getProcessor().process(foo.getThis());
    }
    

    and invoke this with

    Foo<?> foo = ...;
    process(foo);
    

    To make it easy to use, I recommend moving the helper method into class Foo:

    abstract class Foo<F extends Foo<F>> {
        abstract Processor<F> getProcessor();
    
        abstract F getThis();
    
        void processWith(Processor<F> p) {
            p.process(getThis());
        }
    }
    

    Update: I think newaccts updated answer shows a more elegant solution, as it does not need the recursive type bounds.

    0 讨论(0)
  • 2021-01-22 12:27

    No, that's not what Java Generics were designed to do.

    Also, in my opinion, the need for the type check indicates a problem with the design. I'd suggest trying to re-design it so that this is not necessary.

    0 讨论(0)
  • 2021-01-22 12:28

    Here's your code fully "genericized", but with one slight change: The INSTANCE variable is not static.

    interface Processor<T extends Foo<T>> {
        void process(T foo);
    }
    
    interface Foo<T extends Foo<T>> {
        Processor<T> getProcessor();
    }
    
    static class SomeProcessor<T extends Foo<T>> implements Processor<T> {
    
        final SomeProcessor<T> INSTANCE = new SomeProcessor<T>();
    
        @Override
        public void process(T foo) {
            // it will only ever be a SomeFoo if T is SomeFoo
        }
    }
    
    class SomeFoo implements Foo<SomeFoo> {
        @Override
        public Processor<SomeFoo> getProcessor() {
            return new SomeProcessor<SomeFoo>().INSTANCE;
        }
    }
    

    No compiler errors or warnings.

    INSTANCE was made an instance variable because class types do not make it through to static anything. If you really only wanted one INSTANCE, use the singleton pattern on the class.

    0 讨论(0)
  • 2021-01-22 12:39
    interface Processor<F extends Foo<F>> {
        void process(F fooSubclass);
    }
    
    interface Foo<F extends Foo<F>> {
        Processor<F> getProcessor();
    }
    

    I haven't tested that this is precisely right, but this pattern of having the generic type refer to itself is probably about as close as you are going to get with compile-time Java generics.

    0 讨论(0)
提交回复
热议问题