38

While assisting a student with a university project, we worked on a Java exercise provided by the university which defined a class for an address with the fields:

number
street
city
zipcode

And it specified that the equals logic should return true if the number and zip code match.

I was once taught that the equals method should only be doing an exact comparison between the objects (after checking the pointer), which makes some sense to me, but contradicts with the task they were given.

I can see why you would want to override the logic so that you can use things like list.contains() with your partial matching but I'm wondering if this is considered kosher, and if not why not?

4
  • 5
    Exact comparison of two objects is not what equals has to do, as it simply models what equality means in your universe. The address can also contain other members, like "name", that do not contribute to the address's "key" and aren't relevant when testing equality. Note however, that if city and street are deducible from number and zipcode, it could be useful to separate them into another map (if your objects were rows in a relational database, we could say the database violates 2NF), depending on the intended usage.
    – IS4
    Jan 1, 2020 at 19:24
  • 7
    Hi - mathematician here, so beware :-) To me, equality means 'isomorphism': loosely speaking, it means that two objects are 'essentially' the same, whatever one has decided that should mean. You very often use this to throw away details that are not relevant in you current context - in your example, city and street follow from your zip code, so can be disregarded. This is, in fact, one of the most important techniques in maths.
    – j4nd3r53n
    Jan 2, 2020 at 12:05
  • C# is better in this regard as it promotes the IEqualityComparer and IComparer interfaces throughout the base class library. Meaning that you can easily compare things based on a specific context, which is the only correct way to reason about equality. Lacking this functionality, consider injecting your own interface for equality comparisons where it makes sense.
    – vgru
    Jan 6, 2020 at 20:59
  • Here's my example : recently I made a project to calculate the optimal exam schedule, and I made Subject structs for that purpose. I knew that making the user type in the name and the code would be too tedious - and I don't really expect perfect data entry, especially for the name - so for purposes of the container I told operator== to just check for equality of the subject codes. Jan 12, 2020 at 4:38

10 Answers 10

90

Defining Equality For Two Objects

Equality can be arbitrarily defined for any two objects. There is no strict rule that forbids someone from defining any way they want. However, equality is often defined when it is meaningful for the domain rules of what is being implemented.

It is expected to follow the equivalence relation contract:

  • It is reflexive: for any non-null reference value x, x.equals(x) should return true.
  • It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value x, x.equals(null) should return false.

In your example, perhaps there is no need to distinguish two addresses that have the same zipcode and number as being different. There are domains that are perfectly reasonable to expect the following code to work:

Address a1 = new Address("123","000000-0","Street Name","City Name");
Address a2 = new Address("123","000000-0","Str33t N4me","C1ty N4me");
assert a1.equals(a2);

This can be useful, as you mentioned, for when you do not care about them being different objects - you only care about the values they hold. Perhaps zipcode + street number are enough for you to identify the correct address and the remaining information is "extra", and you don't want that extra information to affect your equality logic.

This could be a perfectly good modeling for a software. Just make sure there is some documentation or unit tests to ensure this behavior and that the public API reflects this use.


Do Not Forget About hashCode()

One additional detail relevant for implementation is the fact that many languages heavily use the concept of hash code. Those languages, java including, usually assume the following proposition:

If x.equals(y) then x.hashCode() and y.hashCode() are the same.

From the same link as before:

Note that it is generally necessary to override the hashCode method whenever this method (equals) is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

Note that having the same hashCode does not mean that two objects are equal!

In that sense, when one implements equality, one should also implement a hashCode() that follow the property mentioned above. This hashCode() is used by data structures for efficiency and guaranteeing upper bounds on the complexity of their operations.

Coming up with a good hash code function is hard and an entire topic on itself. Ideally the hashCode of two different objects should be different or have an even distribution among instance occurrences.

But keep in mind that the following simple implementation still fulfills the equality property, even though it is not a "good" hash function:

public int hashCode() {
    return 0;
}

A more common way of implementing hash code is to use the hash codes of the fields that define your equality and make a binary operation on them. In your example, zipcode and street number. It is often done like:

public int hashCode() {
    return this.zipCode.hashCode() ^ this.streetNumber.hashCode();
}

When Ambiguous, Choose Clarity

Here is where I make a distinction about what one should expect regarding equality. Different people have different expectations regarding equality and if you are looking to follow the Principle of Least Astonishment you can consider other options to better describe your design.

Which of those should be considered equal?

Address a1 = new Address("123","000000-0","Street Name","City Name");
Address a2 = new Address("123","000000-0","Str33t N4me","C1ty N4me");
assert a1.equals(a2); // Are typos the same address?
Address a1 = new Address("123","000000-0","John Street","SpringField");
Address a2 = new Address("123","000000-0","John St.","SpringField");
assert a1.equals(a2); // Are abbreviations the same address?
Vector3 v1 = new Vector3(1.0f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);
assert v1.equals(v2); // Should two vectors that have the same values be the same?
Vector3 v1 = new Vector3(1.00000001f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);
assert v1.equals(v2); // What is the error tolerance?

A case could be made for each one of those being true or false. When in doubt, one can define a different relation that is clearer in the context of the domain.

For instance, you could define isSameLocation(Address a):

Address a1 = new Address("123","000000-0","John Street","SpringField");
Address a2 = new Address("123","000000-0","John St.","SpringField");

System.out.print(a1.equals(a2)); // false;
System.out.print(a1.isSameLocation(a2)); // true;

Or in the case of Vectors, isInRangeOf(Vector v, float range):

Vector3 v1 = new Vector3(1.000001f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);

System.out.print(v1.equals(v2)); // false;
System.out.print(v1.isInRangeOf(v2, 0.01f)); // true;

This way, you better describe your design intent for equality, and you avoid breaking future readers expectations regarding what your code actually does. (You can just take a look at all the slightly different answers to see how people's expectations varies regarding the equality relation of your example)

14
  • 14
    “return 0;” is by far not the dumbest way to implement a hash code, because it is a correct way. And if you never have more than a dozen instances of a class, it’s not actually that inefficient.
    – gnasher729
    Dec 30, 2019 at 21:14
  • 5
    I am pretty sure the answer was pretty generic to apply to both java and most other object oriented languages... but sure... changed the case, link and some other small things to reflect the java syntax. Also changed "dumbest" to "simplest". Dec 30, 2019 at 21:44
  • 17
    Although "return 0" is legal and less bad than something which isn't, I would just remove it from the answer. Invisibly degrading hash maps to O(n) searches is the sort of bug that lurks, being hard to notice even with thorough testing, and being hard to find when it causes issues at deployment scales. Yes, for small cases it's harmless, but there's no good reason to assume we've got small scales. You've already given a much more helpful and almost as straightforward alternative in your xor hash function, so just highlight that as the cannonical way to do it.
    – Josiah
    Dec 31, 2019 at 10:42
  • 9
    I note that any relation which satisfies the requirements you laid out is called an equivalence relation. That might help find more information about this topic. Dec 31, 2019 at 23:51
  • 5
    You could add that the recommended way in modern Java for defining hashCode over certain fields is Objects.hash(Object... values) - it is easy, null-safe and states clear intention.
    – Falco
    Jan 2, 2020 at 9:26
42

It is in the context of the university assignment where the task purpose is to explore and understand operator overriding. This seems like an example assignment that has enough implied purpose to make it appear as a worthwhile exercise at the time.

However, if this was a code review by me I would mark this up as a significant design flaw.

The problem is this. It enables syntactically clean code that looks obviously correct:

if (driverLocation.equals(parcel.deliveryAddress)) { parcel.deliver(); }

And based on other users comments, this code would produce correct outcomes in Brazil where zipcodes are unique to a street. However, if you then tried using this software in the USA where this assumption is no longer valid, this code still looks correct.

if this had been implemented as:

if (Address.isMatchNumberAndZipcode(driverLocation, parcel.deliveryAddress)) {
  parcel.deliver();
}

then a few years later, when a different brazilian developer is given the codebase and told that the software delivers parcels to the wrong addresses for their new customer in California, the now broken assumption is obvious in the code and is visible at the decision point on whether to deliver or not - which is likely to be the first place that the maintenance programmer looks at to see why the parcel is delivered to the wrong address.

Having non obvious logic hidden away in an operator overload will make the code fix take longer. To catch this issue in this code would probably take a session with a debugger stepping through the code.

8
  • 2
    I'd lean toward making the function take a bitwise enum like this: Address.IsMatch(driverLocation, parcel.deliveryAddress, AddressPart.Number | AddressPart.Zip) instead of baking the case into the function name. I find it easier to read the code that way, and it will make supporting rules for other locations easier in the future. Dec 31, 2019 at 19:02
  • 2
    @DavidConrad If you want to be pedantic, go the whole way: There is no user-defined operator overloading. + is overloaded for string-concatenation and arithmetic. Jan 1, 2020 at 1:34
  • 1
    Hiya David, okay I take the point that my code style and terminology was more c#.net than java, but I have understood and answered the question. Is operator logic in the equals() function a good idea? Not In professional code, it’s dangerous, and I have explained why. Jan 1, 2020 at 10:30
  • 1
    Informed decisions can be made - such as what constitutes address equality. Not every application will need to account for internationally different standards. Similarly, while 4.0 == 4 in every mathematical sense, it doesn't in physics, as the numbers showcase a different level of precision, which can be vital to the use case of the application (and thus equality can be overridden to not allow this "bad" equality in the given domain - regardless of whether this applies in domains unrelated to the current application)
    – Flater
    Jan 1, 2020 at 14:52
  • 1
    The problem is not that all types have an equality operator, but rather that they don't provide any means of querying a class whether two instances are guaranteed to be equivalent now and forever, or only as long as no attempt is made to modify either object. If one were to add a language distinction to distinguish a field that holds the only reference to an object that may be changed, one that holds a sharable reference to an immutable object, one that holds a view of an object owned by some other entity, etc. the compilers could auto-generate sensible cloning and equality-check methods...
    – supercat
    Jan 1, 2020 at 19:04
25

Equality is a matter of context. Whether or not two objects are considered to be equal is as much a question of context as it is of the two objects involved.

So, if in your context it makes sense to ignore city and street, then there is no problem to implement equality solely based on ZIP code and number. (As was pointed out in one of the comments, ZIP code and number are enough to uniquely identify an address in Brazil.)

Of course, you should make sure to follow the proper rules for overloading equality, such as making sure you also overload hashCode accordingly.

4
  • 3
    This is the only answer so far that raises the issue of universal [value] equality being an inherently flawed concept -- a matter that really ought not to be overlooked. Dec 31, 2019 at 15:10
  • 3
    @JohnBollinger: It's also the only answer that directly addresses the question that was asked. Dec 31, 2019 at 20:30
  • Agree, equality can be context-specific. I sometimes ponder what things would look like if hashCode() and equals() were methods in a Hashable interface, akin to Comparable.
    – erickson
    Dec 31, 2019 at 23:33
  • 1
    @erickson: That's how it's done in Haskell, for example. In Haskell, equality (the Eq typeclass), ordering (Ord), the ability to act as a key in a hashtable (Hashable), are all defined externally in typeclasses, and thus can be dependent on context. Dec 31, 2019 at 23:38
3

An equality operator will claim that two objects are equal if and only if they should be considered equal, due to whatever considerations that you find useful.

I’ll repeat that: Due to whatever considerations that you find useful.

The software developer is in the driver’s seat here. Apart from being consistent with obvious requirements (a=a, a=b implies b+a, a=b and b=c implies a=c) and consistency with the hash function) the equality operator can be whatever you like.

1

Although the given requirement contradicts the human sense it is OK to let only a subset of the objects properties define the meaning of "unique".

Problem here is that there is a technical relationship between equals() and hashcode() so that for two objects a and b of that type is deemed to be:
if a.equals(b) then a.hashcode()==b.hashcode()
If you have a subset of the properties defining your uniqueness conditions you must use the same subset to calculate the return value of hashcode().

After all the much more appropriate approach for the requirement may have been to implement Comparable or even a custom isSame() method.

1

Although many answers were given, my opinion still isn't present.

I was once taught that the equals method should only be doing an exact comparison between the objects

Apart from what rules say, this definition is what people assume from their intiution when they talk about equality. Some answers say equality depends on context. They are right in a sense that objects can be equal even if not all of their fields match. But the common understanding of "is equal" should not be redefined too much.

Back to the topic, to me an address equal to another if it points to the same location.

In Germany there can be different specifications of a city, for example if a suburb is named. Then the city of an address in suburb SUB can be given as "Main city" only or "Main city, SUB" oder even only "SUB". Because giving the main city name is ok, all street names in a city and all its assigned suburbs must be unique.

Here the zip code is enough to tell the city, even if the city name varies.
But leaving the street is NOT unique, unless the zip code also points to one well known street, which it usually doesn't.
So it is not intuitive to consider two addresses equal if they can point to different locations whose difference consists of the ignored fields.

If there is a use case only requiring some but all fields, then the compare method doing so should be named appropriately. There is only one "is equal" method that should not be secretly turned into "is equal for only one special use case - but noone can see that".

That means, for the explained reasons I'd say...

but I'm wondering if this is considered kosher

Without knowledge if you accidentally are in a location where street names don't matter: no it isn't.
If you want to program something not only used in such a location: no it isn't.
If you want to give students a feeling of doing things right and keeping code comprehensible and logical: no it isn't.

1

It depends.

Is it a good idea ... ? It depends. It can be a good idea, if you are developing an application that will be used only once, for instance, in a univercity assignment (if you are going to throw the code away after assignment reviewed), or some migration utility (you migrate the legacy data once and don't need the utility any more).

But in the IT industry in many cases that would be a bad idea. Why? @Jörg W Mittag said Equality is a matter of context... if in your context it makes sense.... But often the same object is used in many different context that have different view on equality. Just a few examples of how differently can be defined equality of the same entity:

  • As equality of all attributes of two entities
  • As equality of primary keys of two entities
  • As equality of primary keys and versions of two entities
  • As equality of all "business" attributes except of primary key and version

If you implement in the equals() the logic for one particular context, it will be hard later on to use this object in other contexts, because many developers in the teams in your project will not exactly know the logic for which context exactly is implemented there and in which cases they can rely on it. In some cases they will incorrectly use it (like @Michael Shaw described), in other cases they will ignore the logic and implement their own methods for the same purpose (which may work differently from what you expected).

If your application is going to be used for a longer time like 2-3 years, there will be normally multiple new requirements, multiple changes and multiple contexts. And very probably there will be multiple different expectations on equality. That's why I'd suggest:

  • Implement equals() formally, without connection to the business context, means without any business logic, just as equality of all object attributes (of course hashCode/equals contract must be followed)
  • For every context provide a separate method that implements equality in the sense of this context, like isPrimaryKeyAndVersionEqual(), areBusinessAttributesEqual().

Then to find an object in particular context you just use corresponding method, as follows:

if (list.sream.anyMatch(e -> e.isPrimaryKeyAndVersionEqual(myElement))) ...

if (list.sream.anyMatch(e -> e.areBusinessAttributesEqual(myElement))) ...

Thus there will be less bugs in the code, code analysis will be easier, changing of the application for new requirements will be easier.

0

As others have mentioned, the exact semantics of object equality are a part of the business domain's definition. In this case, I don't think it's reasonable to have a "general" object like Address (containing number, street, city, zipcode) to having a very narrow definition of equality (which, as others have mentioned, works in Brazil but not in the US, for example).

Instead, I would have Address have value-like semantics for equality (defined by equality of all members). I would then either:

  1. Create a StreeNumberAndZip class (# TODO: bad name), which contains only a street, and a zipCode, and defines equals over those. Whenever you want to compare two Address objects in that particular way, you can do addressA.streetNumberAndZip().equals(addressB.streetNumberAndZip()), or...
  2. Create a AddressUtils class with a bool equalStreeNumberAndZipCode(Address a, Address b) method, which defines the narrow equality there.

In both cases, you still have access to use addressA.equals(addressB) for full equality checking.

For n fields of an object, there are 2^n different definitions of equality (each field can be included or excluded from the check). If you find yourself needing to check equality in many different ways, it might also be useful to have something like an enum AddressComponent. You could then have a bool addressComponentsAreEqual(EnumSet<AddressComponent> equatedComponents, Address a, Address b), so you can call something like

bool addressAreKindOfEqual = AddressUtils.addressComponentsAreEqual(
    new EnumSet.of(
        AddressComponent.streetNumber, 
        AddressComponent.zipCode,
    ),
    addressA, addressB
);

This is obviously a lot more typing, but it can save you from having an exponential explosion of equality-checking methods.

2
  • I like your overall approach, but I'm not sure about the StreetNumberAndZip class. What to do about valid differences? For example: Is 201 W WASHINGTON BLVD the same as 201 WEST WASHINGTON BOULEVARD? And if that location is in Los Angeles, are these three zip codes all valid: 90015-3514, 90015-0000, 90015? I realize that you were only proposing a high level approach, but there are probably enough edge cases to justify having the address components as separate classes. Or at least start out that way, and perhaps merge the components later, rather than the other way around.
    – skomisa
    Jan 2, 2020 at 6:45
  • I should have been more clear: that first approach isn't necessirly applicable everywhere. This example is clearly a wonky abstraction (given that I didn't have a name for the new aggregate, and just restored to naming it after its members), but perhaps OP's business domain has a class like a "postal region" or something, which was waiting to get out
    – Alexander
    Jan 2, 2020 at 11:40
0

Equality is subtle to get right and its importance is deceptively far-reaching. Especially in languages where implementing an equality operator suddenly means that your object is supposed to play nice with sets and maps.

In the overwhelming majority of cases, equality should be identity, meaning an object is equal to another if and only if it is the same piece of memory with the same address. The identity relation always respects all the conditions for a proper equality relation: reflexivity, transitivity etc. Identity is also the fastest way to compare any two things, as you merely compare the two pointers. Respecting the equivalence relation contracts is the single most important thing about any implementation of equality as failure to do so translates into bugs that are notoriously hard to diagnose.

The second way of implementing equals is to compare if the types match then compare every "owned" field of the object. This often ends up recursing far into the details of every object. If your object goes into data structures which call equals, equals will probably be what the data structure spends most of its time doing if you use this approach. There are other problems:

  • if the object changes, then the result of its comparison with other objects also changes, which breaks all sorts of assumptions that standard classes make about equality;
  • if your object is in a class/interface hierarchy, the only sane way to compare two objects in that hierarchy is if their concrete types match exactly (see Joshua Bloch's excellent Effective Java book for more details on this);
  • if you try to make the equality relationship very strict by including as many fields as possible, eventually you'll end up in a situation where your equality does not correspond to a business logic of "sameness".

The third way would be to select only the fields which are relevant for the business logic and ignore the rest. The likelihood of this approach being broken is arbitrarily close to 1. The first reason as mentioned by others is that a comparison that makes sense in one context doesn't necessarily make sense in all contexts. The language asks that you define one form equality though, so it better work in all contexts. For addresses, such a comparison logic simply doesn't exist. You can have specialized "those two addresses look identical" methods but you shouldn't risk any such method being the language-backed Only True Way To Compare as that will inevitably confuse readers.

I would also recommend taking a look at Falsehoods programmers believe about addresses: https://www.mjt.me.uk/posts/falsehoods-programmers-believe-about-addresses/ it's a fun read and might help you avoid some pitfalls.

0

Like others mentioned, on one hand equality is just a mathematical concept satisfying some properties (see e.g. Albuquerque's answer). On the other hand its semantic and implementation is determined by the context.

Regardless of the implementation details, take for example a class representing arithmetic expressions (like (1 + 3) * 5). If you implement an interpreter for such expressions using the standard evaluation rules for arithmetic expressions it makes sense to consider the respective instances for (1 + 3) * 5 and 10 + 10 to be equal. However, if you implement a pretty printer for such expressions above instances would not be considered equal, while (1 + 3) * 5 and (1+3)*5 would.

Not the answer you're looking for? Browse other questions tagged or ask your own question.