What is the Java type system for? – Case coverage

So what might a programming language with strong static named types be useful for? I recently asked an interview question about how the observer pattern could be implemented in a situation involving the occurrence of a number of different types of events. This answer was suggested:

interface EventListener {
    void eventOccurred(Event event);
}
 
class MyEventHandler implements EventListener {
    void eventOccurred(Event event) {
         if(event.getType() == EventType.subtypeOne) {
            doTypeOneThing((EventSubtypeOne) event);
         } else if(event.getType() == EventType.subtypeTwo){
            doTypeTwoThing((EventSubtypeTwo) event);
         } else {
            throw new RuntimeException();
         }
    }
}

There are a number of issues with this design, however the main issues that occurs in the kind of large programs I often work on is: when a new type is added how do you make sure that all of the types of events are handled in every implementation of EventListener?

The suggested solutions are often along the lines of writing test cases for each of your EventListener implementations and taking the absence of an exception as success. But then how do you make sure you have updated all of your tests correctly?

A simple, easy to understand, and Object Orientated way to resolve this problem is:

interface EventListener {
    void eventOneOccurred(EventOne event);
    void eventTwoOccurred(EventTwo event)
}
 
class MyEventHandler implements EventListener {
    void eventOneOccurred(EventOne event) {
            doTypeOneThing(event);
    }
 
    void eventTwoOccurred(EventTwo event) {
            doTypeTwoThing(event);
    }
}

Important points to notice about this solution:

  1. No casting. Since we never represent the event as a generalised abstraction, we never need to downcast the event. We don’t even need a base type for event unless we have some use for that abstraction in our system.
  2. No runtime error. As long as all the code is compiled together (or at least has binary compatibility) then no runtime error will occur because our type system proves that all the types of events are handled.
  3. Updates are easy. When we add a new event type, we add the new method to the interface and the compiler will tell us where all the implementations are! This is very fast if you have an IDE with incremental compilation and all the code. But it will work safely even in situations where the people implementing your interfaces are distributed and you have no possibility of updating their code. When they try to compile against the new version of your library they will immediately find out about the new feature (if they like it or not)!

Of course, you don’t always want these properties in every design. But when you do want your design to have these properties, this is how you can do it.