Search code examples
javacasting

Why is casting needed


The following works as intended:

public boolean equals(Object otherObject) {
    if (otherObject instanceof Employee) {
        Employee toCompare = (Employee) otherObject;
        if (this.getLastName().equals(toCompare.getLastName()) && this.idNumber == toCompare.idNumber) return true;
    }
    return false;
} 

My question is why doesn't this work also:

public boolean equals(Object otherObject) {
    if (otherObject instanceof Employee) {            
        if (this.getLastName().equals(otherObject.getLastName()) && 
           this.idNumber == otherObject.idNumber) return true;
    }
    return false;
}  

What was the reason they left this out of the compiler? Or, what does toCompare have that otherObject doesn't have? They both point to the same location so what's the difference?


Solution

  • What you're talking about is so-called flow type tightening. You want this:

    Object foo = ...;
    if (foo instanceof String) {
      foo.toLowerCase();
    }
    

    To work because specifically within the confines of that if block, foo, surely, is a String.

    There are some fundamental problems with that idea:

    • Imagine foo is not a local variable but a field. Surely this should still work. Except... fields can be changed on the fly by other threads. So should the language not apply this principle for fields, but do apply it for method-locals? That sounds like you're just opening the door for somebody else to ask an incredulous question about why in the blazes there's a difference.
    • That would mean if you then change foo inside the if, things get a bit bizarre. What should happen here:
    Object foo = ...;
    if (foo instanceof String) {
      if (Math.random() < 0.00005) foo = 5;
      foo.toLowerCase();
    }
    

    That clearly should not work - its unlikely but possible that foo isn't a string when you call foo.toLowerCase(). So here it wouldn't work?

    You hopefully see the general issue of taking inference too far. It makes trying to debug the situation when you write some code and it doesn't do what you wanted it to incredibly complicated. It makes the compiler extremely complicated.

    Now, that doesn't mean that it's a bad idea to add it. This is merely an explanation for something much, much simpler:

    • It is understandable that the JDK engineers, at the time java 1.0 was released, set it up this way.

    Even if you think it is wrong, that's not the point. The point is simply that it is understandable that the choice was made not to flow type this structure.

    Which gets us to the next point:

    ... and now we are stuck with it

    Introducing flow typing now would break backwards compatibility. Weird, but true - that's because in java, method parameters are part of a method's identity, and thus, changing types can make the exact same code now refer to a completely different method. This is current, vanilla java, try it out if you like:

    class Test {
      public static void main(String[] args) {
        Object a = "foo";
        String b = "foo";
        System.out.println(test(a));
        System.out.println(test(b));
      }
    
      public static void test(Object x) {
        System.out.println("foo(Object)");
      }
    
      public static void test(String x) {
        System.out.println("foo(String)");
      }
    }
    

    This will print foo(Object), then foo(String). You might find that weird - in both cases, the object passed is of type String. That's because which of the two methods to go with is decided by the compiler, not at runtime. Java does have dynamic typing but it applies solely to when you override methods, which requires not just that the name is identical, but also that the parameter types are. Not the case here.

    We can do another deep dive on why one might design a language like that, but at some point we'll be here, 4871 years from now, with you asking 'sooo, about that big bang'. It certainly is well beyond the scope of this question.

    At any rate, given that this is how java works, adding flow typing breaks things. After all, imagine in the above code I did:

    Object q = "Hello";
    if (q instanceof String) {
      test(q);
    }
    

    Then current java would print test(Object) whereas if you add flow typing, you get test(String) and that is not backwards compatible.

    The good news

    Just because other languages did it this way doesn't mean java has to follow and break stuff, nor that java just has to sit there and regret.

    You just add the feature differently. Which is where patterns come in. New java features, which is a vastly more complicated feature than just this, but one of the many, many things it can do, is:

    Object foo = "x";
    if (foo instanceof String bar) {
      bar.toLowerCase();
    }
    

    This works fine, and avoids the backwards incompatibility issues. It also avoids the confusion about '.. but what if foo is a field that gets changed halfway through (irrelevant; the test and assignment to variable bar is atomic)', it avoids the problem if reassigning it (feel free, but variable foo is of type Object and variable bar is of type String, and nothing is going to change this, so there is no confusion there either).

    It also allows deconstruction, pattern matching, and far, far more.

    If you'd like to know more, these things are discussed at extreme length (you'll spend WEEKS reading if you want to know all the details) on the openjdk mailing lists. valhalla-dev, amber-dev, core-lib-dev might be interesting for this. Here is the main page of the amber-dev mailing list.