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

前端 未结 6 897
有刺的猬
有刺的猬 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 {
      void process(T foo);
    }
    
    interface Foo {
      Processor getProcessor();
    }
    

    then

    class SomeProcessor implements Processor {
      public void process(SomeFoo foo) {
        // TODO
      }
    }
    
    class SomeFoo implements Foo {
      public Processor 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.

    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 {}
    

    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.

提交回复
热议问题