Equality vs. Identity
Alexei touched on this topic by mentioning hash tables, but I would like to spell it out more explicitly, because this is a critical and fundamental topic in software engineering, and essential for every programmer to know and understand.
Every high level programming language has a mechanism for comparing two values for "equality". But every PL with reference types must also provide one more ability: comparing whether two references point to the same object. In Java, D, C#, Kotlin, and many other similar languages, the ==
operator tests for identity (do these two references point to the same address?), while the equals()
method tests for equivalence (may I regard these expressions as having the same value?). Like I said, understanding this distinction and when to use which operator is absolutely essential to writing correct code.
A non-programmer (especially a mathematician, perhaps), might assume that ==
is the more useful function, because pure math and common experience can usually make do with identity (all instances of the number $\pi$ are identical in math). The reality is that in the majority of production code, equals()
far outnumbers ==
. That's because the majority of objects in production code are mutable and lack referential transparency.
Strings
To understand the above, we need look no further than strings. In Java, strings are immutable, but still require comparison via equals()
rather than ==
. To see why, consider this Java code fragment:
void areEqual() {
String a = "hello";
String b = "hello";
if (a == b) System.out.println("Same");
else System.out.println("Different");
}
Now, as the naive reader expects, the function above will print Same
, but only because the strings in question are literals, and thus, the compiler will generate code which causes a
and b
to point to the same memory address. One small change will break this code:
void areEqual() {
String a = "hello";
String b = "hel";
String c = b + "lo";
if (a == c) System.out.println("Same");
else System.out.println("Different");
}
This function will print Different
, even though we could print out a
and c
and they would look the same to anyone who stared at them. In this case, we defeat the string interning mechanism, and c
resolves to a different object than a
. Under the covers, the first example might produce something like: a == 0x1234abcd; b == 0x1234abcd
while the second example might produce: a = 0x1234abcd; b == 12349876
. So the values of the strings are equivalent, but the addresses of the strings are quite distinct.
This is important, because if one has a container of strings, and one wishes to see if some new string exists in that container, in virtually every real-world program, the programmer wants to know if an equivalent string exists in the container (i.e., an object in the same equivalence class, or, the same sequence of characters), rather than the identical string.
Business Objects
The discussion above might lead one to assume that identity vs. equality is just an esoteric implementation detail of programming languages with reference semantics, and that we can simply switch to calling equals()
everywhere and be done with it. But it's not so simple. Suppose we have a shopping website, and a large catalog with millions of items and their descriptions. An item description might be a surprisingly complex composite object, consisting of a short text blurb in addition to structured data about the item (its weight, shipping box dimensions, manufacturer, etc.). However, there may be duplicates within the catalog. When this happens, we can save precious memory by reusing the same object when two descriptions are equivalent even though they are not identical. Now, if we have defined the naive equals()
method on the class ItemDescription
which compares every field one by one, then this seems like another boring application of equals()
.
But it's not that simple. You see, data like this will almost certainly come from a database, and any good DBA will demand that every table contain a primary key, which will usually be a synthetic autoincrement value. So, it is very, very likely, that there will exist two sets of records in the item catalog which are the same up to their PKey. An app which stores huge portions of the catalog does not want to waste memory storing these duplicates separately. Thus, it is useful to define an additional equality operator that detects this "equality-up-to-pkey". That is, we wish to traffic in the equivalence class of ItemDescription - PKey
. One way to do this would be to define another method on the ItemDescription
class which implements this equivalence class, and use that comparator on a collection of unique ItemDescription
. This will ensure there is only one copy of each equivalent ItemDescription
in the in-memory collection. This kind of equivalence class occurs frequently in the industrial programming world.
Inheritance
One of the defining characteristics of Object-Oriented Programming (OOP) is the mechanism of "inheritance". When a type Child
"inherits" from a type Parent
, we say that a Child
"is-a" Parent
. This notion is neither identity nor the naive field-by-field notion of equivalence that is commonly assumed. The "is-a" relation really means: "can be substituted for". That is, any code which expects a value of type Parent
will gladly accept a value of type Child
. But what does that mean? What if the Child
class introduces new fields which don't exist in Parent
? Well, those fields are ignored. When you pass a Child
in as a Parent
, only the Parent
portion of the object is considered.
Here is a small code example:
class Animal {
protected String sound = "<gurgle>";
public Animal(String sound) {
this.sound = sound;
}
...
}
class Bird extends Animal {
int wings = 2;
public Bird() {
super("<tweet>");
}
...
}
class Pigeon extends Bird {
String trait = "annoying";
...
}
class Dog extends Animal {
int legs = 4;
public Dog() {
super("<woof>");
}
...
}
void tickle(Animal animal) {
System.out.println("You tickle the animal, and it goes: " + animal.sound);
}
Note that you are free to pass an Animal
, Bird
, Pigeon
or Dog
to tickle()
. It will accept any of them. This fact alone illustrates that "is-a" defines an equivalence class over types, given that the type system requires an argument's type to match the type of the parameter. Given that almost every major programming language with mutable data supports OOP features, one could say that equivalence classes are again pervasive in the type system.
Conclusion
There are many more examples of equivalence classes used commonly within professional software engineering, but hopefully this is enough to get you started.