Reputation: 43979
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.
Upvotes: 9
Views: 2125
Reputation: 106440
Adding a third parameter - the Class<T>
type to your method - will help with this sort of issue. As you say, you can get an Object
from T
; unless you add bounds to it, it won't ever be able to identify anything other than Object
.
While adding the class type will force you to put it in every time you want to use the method, it enforces the compiler and implied contract that you're comparing them equal as if they are a certain class.
<T> boolean typesafeEquals(T x, T y, Class<T> clazz) {
return x.equals(y);
}
Do note that this kind of checking is a bit of an overreaction from the perspective of the developer. It should be the case that two objects which are not equivalent should simply return false
.
Given the requirement of it being a drop-in replacement for Object.equals
, that is simply impossible given the well-established loose type safety around the method as it is. While generics would be an ideal way to solve this, generics existed in Java 5, whereas equals
was there from the beginning.
Upvotes: 6
Reputation: 8833
You have several options here.
This is similar to passing directly to the method, except the extra scope prevents the compiler from cheesing the generic type to Object.
static interface ITypeSafe<T> {
public default boolean typeSafeEquals(T o) {
return equals(o);
};
}
static class TypeSafeEquals<T> {
public static <A, B extends A> boolean stypeSafeEquals(B o1, B o2) {
return o1.equals(o2);// tse(o2, o1);
}
private boolean typeSafeEquals(T o1, T o2) {
return o1.equals(o2);
}
}
You can also add a compiler hook using annotations, and then use annotations to flag for type safety. This probably won't work for large teams though as it does require extra project settings set to work, but by far the most versatile. (I can't include example here because it is complicated and I don't fully understand it.)
You can use development test lines like assert assert (o1.getClass() == o2.getClass());
to check during unit tests that always true assumptions aren't violated. (You should be doing this anyways) Just make sure these lines are executed during development and not during production. (This should be an easier alternative to the compiler hook)
Upvotes: 0
Reputation: 20467
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
Upvotes: 2
Reputation: 2370
What you've tried won't work because T
is ambiguous and the compiler will try to make the code compile (by interpreting it to be Object
). You need to force it a little bit by explicitly setting the type you want to check.
Makoto's answer is very good for that.
And, if you can cope with using another object, you can also use the following :
public class EqualsHelper<T> {
public boolean areEquals(T one, T another){
if(one == null){
return another == null;
}
return one.equals(another);
}
}
public static void main(String[] args) {
EqualsHelper<String> stringHelper = new EqualsHelper<>();
stringHelper.areEquals("hello","world"); // compile
stringHelper.areEquals("hello",1); // doesn't
}
Upvotes: 2
Reputation: 2776
First of all functions overloading :)
typesafeEquals(Integer, Integer);
typesafeEquals(int, int);
typesafeEquals(String, String);
...
Second is
<N extends Number> typesafeEquals(N, N);
<C extends CharSequence> typesafeEquals(C, C);
...
Upvotes: -1
Reputation: 81179
Asking an instance of Apple
whether it is equal to a particular instance of Orange
should not cause it any trouble. It should simply observe that because it does not consider itself equal to any objects which are not of type Apple
, and because it was given a reference to something that isn't of type Apple
, it does not consider itself equal to the passed in object.
Further, certain interfaces have contracts which require implementations them to consider themselves equal to any other implementations meeting certain criteria. It would thus be possible for two objects which are of unrelated types to both implement an interface which requires that the two objects report each other as equal. Even if the types of the references have no interface in common, unless at least one is sealed it would be possible for instances of types derived from them to share an interface which would compel a reciprocal equality relationship.
Consequently, while there are certainly cases where static analysis could reveal that the objects identified by references of two different classes cannot possibly consider themselves equal, the level of static analysis required would be far beyond anything the generic type system could possibly encapsulate [among other things, it would require examination of the code in the different types' equals
methods].
I would suggest that while it might in theory be nice if the compiler could identify cases where code tried to compare things that couldn't possibly be equal, the way that rules about equals
are defined (including the fact that it's permissible--and in some cases required--for instances of unrelated classes to compare themselves equal to each other) means that in general what you seek isn't possible. If it's possible to ask an instance of Apple
whether it's equal to any particular Fruit
, then it must be possible to ask it whether it's equal to any particular instance of any type derived from Fruit
. The Apple
shouldn't care if the question is silly; it should simply answer it.
Upvotes: 5
Reputation: 2841
What about this?
public static <T, E extends T> boolean typesafeEquals(T x, E y) {
return x.equals(y);
}
Of course it will compile for subclasses of T, but that kinda makes sense, by the principle of substitution. The only problem is that it enforces an order on the arguments, so it should be renamed it "isAssignable".
edit: Or if you go for strict equality, call it twice while switching arguments:
typesafeEquals("", 1); // doesn't compile
typesafeEquals(1, ""); // doesn't compile
typesafeEquals(base, child); // compile
typesafeEquals(child, base); // doesn't compile
Upvotes: 0