Search code examples
javagenericstype-erasuretype-safetytype-systems

Is it possible to detect comparison of incompatible types with the Java type system?


I'm trying to write a helper function to compare two types in a typesafe way:

typesafeEquals("abc", new Integer(42)); // should not compile

My first straightforward attempt failed:

<T> boolean typesafeEquals(T x, T y) { // does not work!
    return x.equals(y);
}

Well, the problem is that T can be deduced to be an Object. Now, I wonder if it is impossible to implement typesafeEquals in the Java type system.

I know that there are tools like FindBugs find can warn about comparison of incompatible types. Anyway it would be interesting to either see a solution without external tools or an explanation why it is impossible.


Update: I think the answer is that it is impossible. I have no proof to support that claim, though. Only that it seems to be difficult to come up with a solution that works for all cases.

Some answers come close, but I believe the type system of Java does not support to solve the problem in all generality.


Solution

  • Is it possible to detect comparison of incompatible types with the Java type system?
    [...]
    I believe the type system of Java does not support to solve the problem in all generality.

    The type system alone cannot do that, at least not in a universal way that would work for all types in absolute generality, because it has no way to tell what your types do in their equals implementations (as already said in supercat's answer), a Foo could accept to be compared to a Bar, that logic is inscribed in the code itself, so, not available for the compiler to check.


    That being said, for a "restricted" definition of "type-safe", if you can declare what you expect for each use, your own approach was almost there, just make it static, call it using the class name, and explicitly specify the expected type:

    public class TypeSafe {
    
        public static <T> boolean areEqual(T x, T y) {
            return x.equals(y);
        }
    
        void test() {
            TypeSafe.areEqual("a", 1); // Compiles because no restriction is present.
                                       // Both are resolved to Serializable
                                       // [there is not only "Object" in common ;)]
    
            TypeSafe.<CharSequence>areEqual("a", 1); // Does not compile
            //                                   ^
            //          Found: int, required: java.lang.CharSequence
        }
    }
    

    Similar to Makoto's answer, without passing the type as argument, and to Jeremy's, without creating a new object. (upvoted both.)

    Although it would be slightly misleading because TypeSafe.<Number>areEqual(1f, 1d) compiles but returns false. It can give a false sense of meaning "are those 2 numbers equivalent?" (this is not what it does). So you have to know what you're doing...


    Now, even if you compare two Long values, one could be an epoch-based timestamp in milliseconds, and the other in seconds, 2 identical raw values would not be "equal".

    With Java 8 we have type annotations, and the compiler can be instrumented with an annotation processor to perform additional checks based on those annotations (see checker framework).

    Say we have these methods that return durations in milliseconds and in seconds, as specified with annotations @m and @s (provided by framework) :

    @m long getFooInMillis() { /* ... */ }
    @s long getBarInSeconds() { /* ... */ }
    

    (Of course in that case it's probably best to use proper types in the first place, like Instant or Duration... but please ignore that for a minute)

    With that, you can be even more specific about that constraint you pass as generic type argument to your method, using checker framework and annotations:

    long t1 = getFooInMillis();
    long t2 = getBarInSeconds();
    
    TypeSafe.<Long>areEqual(t1, t2);    // OK, just as we've seen earlier
    
    TypeSafe.<@m Long>areEqual(t1, t2); // Error: incompatible types in argument
    //                         ^^  ^^
    // found   : @UnknownUnits long
    // required: @m Long
    
    @m long t1m = getFooInMillis();
    @s long t2s = getBarInSeconds();
    @m long t2m = getFooInMillis();
    
    TypeSafe.<Long>areEqual(t1m, t2s);    // OK
    
    TypeSafe.<@m Long>areEqual(t1m, t2s); // Error: incompatible types in argument
    //                              ^^^
    // found   : @s long
    // required: @m Long
    
    TypeSafe.<@m Long>areEqual(t1m, t2m); // OK