Why doesn’t .equals() behave like a virtual method?

When I add a .equals() method to a class in Apex, it seems that I’m hiding and not overriding the version in Object. This is evidenced by the fact that I don’t have to use the override keyword in my class definition. So, I can write:

public virtual class MyClass {
    public Boolean equals(Object other) {
        System.debug('Called my equals');
        return false;
    }
}

And then it doesn’t behave like a virtual function. If I call .equals() on a reference to MyClass, it calls my version of it. If I call it on an Object reference to an instance of MyClass, it calls the Object version of it i.e.

MyClass m = new MyClass();
Object o = new MyClass();

m.equals('a'); // Prints 'Called my equals'
o.equals('a'); // Prints nothing

This has real and confusing consequences. For example, when we use get(field) on an SObject to get values that happen to be String, we might want to use .equals() to do a case-sensitive comparison. But, since get(field) is typed as returning an Object, the comparison gets done in a case-insensitive way. So, the following fails

System.assert(
        !new Account(Name = 'a')
        .get(Account.Name)
        .equals(new Account(Name = 'A').get(Account.Name)),
        'They are different cases!'
        );

And it can be fixed with a cast!

System.assert(
        !((String)new Account(Name = 'a')
        .get(Account.Name))
        .equals(new Account(Name = 'A').get(Account.Name)),
        'They are different cases!'
        );

What’s going on?

Answer

The Object “class” isn’t a real class, which causes all kinds of problems. This was obviously the first object that was ever written, as all objects derive from it implicitly. It’s implemented by the ANY object internally, and this code is frustratingly broken.

For example, String.toString() works fine most of the time, but if you use String.join() on a list of objects, you get the default String.toString() implementation, even if you’ve overridden it with a custom definition.

public class Demo {
    public override String toString() {
        return 'hello world';
    }
}
System.debug(
    String.join(new Object[] { new Demo() },'')
); // Outputs anon$Demo in Execute Anonymous

The same is true for equals() and hashCode(). There are times when you’ll get the expected output, and times when you won’t, as above.

There’s not much we can do about this. ANY is fundamentally broken in a few different ways, notably its ability to cause random Internal Server Errors in some edge cases, as well as overrides being completely ignored by some system functions.

You’ll find all kinds of edge cases in Apex where this happens, and it bleeds over to things like Visualforce and Lightning from time to time. We don’t know “why” this happens, or if it will ever be fixed, but the best advice I have is to only use these overrides if absolutely necessary, and only in documented ways.

For example:

public class Demo {
    public Boolean equals(Object o) {
        return false;
    }
}
Demo x = new Demo();
System.debug(
    x == x
);

Currently returns… an Internal Server Error, at least in my org. This is because, again, it’s not a “real” object, so trying to monkey with the internals causes problems. You can only use equals() for its one intended use case, which is for inclusion in a Map key or Set.

Anything else outside of the documentation is not supported, and may break in amusing, hard-to-fix ways. Hopefully one day, salesforce.com will get around to fixing Object, but until then, try to carefully follow the documentation.

If you don’t follow documented procedures, you will run into many of the weird edge cases that exist in Apex.

Attribution
Source : Link , Question Author : Aidan , Answer Author : sfdcfox

Leave a Comment