This is a mobile version, full one is here.
Yegor Bugayenko
11 July 2017
How I Would Re-design equals()
I want to rant a bit about Java design, in particular about the methods
Object.equals()
and
Comparable.compareTo()
.
I’ve hated them for years, because, no matter how hard
I try to like them, the code inside looks ugly. Now I know what exactly
is wrong and how I would design this “object-to-object comparing” mechanism
better.
Say we have a simple primitive class Weight
, objects of which
represent the weight of something in kilos:
class Weight {
private int kilos;
Weight(int k) {
this.kilos = k;
}
}
Next, we want two objects of the same weight to be equal to each other:
new Weight(15).equals(new Weight(15));
Here is how such a method may look:
class Weight {
private int kilos;
Weight(int k) {
this.kilos = k;
}
public boolean equals(Object obj) {
if (!(obj instanceof Weight)) {
return false;
}
Weight weight = Weight.class.cast(obj);
return weight.kilos == this.kilos;
}
}
The ugly part here is, first of all, the
type casting
with instanceof
. The second problem
is that we touch the internals of the incoming object. This design makes
polymorphic behavior of the Weight
impossible. We simply can’t pass
anything else to the equals()
method, besides an instance of the
class Weight
. We can’t turn it into an interface and introduce
multiple implementations of it:
interface Weight {
boolean equals(Object obj);
}
This code will not work:
class DefaultWeight implements Weight {
// attribute and ctor skipped
public boolean equals(Object obj) {
if (!(obj instanceof Weight)) {
return false;
}
Weight weight = Weight.class.cast(obj);
return weight.kilos == this.kilos; // error here!
}
}
The problem is that one object decides for the other whether they are equal. This inevitably leads to a necessity to touch private attributes in order to do the actual comparison.
What is the solution?
This is what I’m offering. Any comparison, no matter what types we
are talking about, is about comparing two digital values. Either we
compare a weight with a weight, text with text, or a user with a user—our
CPUs can only compare numbers. Thus, we introduce a new interface
Digitizable
:
interface Digitizable {
byte[] digits();
}
Next, we introduce a new class Comparison
, which is the comparison of
two streams of bytes (I’m not sure the code is perfect, I tested it
here,
feel free to improve and contribute with a pull request):
class Comparison<T extends Digitizable> {
private T lt;
private T rt;
Comparison(T left, T right) {
this.lt = left;
this.rt = right;
}
int value() {
final byte[] left = this.lt.digits();
final byte[] right = this.rt.digits();
int result = 0;
int max = Math.max(left.length, right.length);
for (int idx = max; idx > 0; --idx) {
byte lft = 0;
if (idx <= left.length) {
lft = left[max - idx];
}
byte rht = 0;
if (idx <= right.length) {
rht = right[max - idx];
}
result = lft - rht;
if (result != 0) {
break;
}
}
return (int) Math.signum(result);
}
}
Now, we need Weight
to implement Digitizable
:
class Weight implements Digitizable {
private int kilos;
Weight(int k) {
this.kilos = k;
}
@Override
public byte[] digits() {
return ByteBuffer.allocate(4)
.putInt(this.kilos).array();
}
}
Finally, this is how we compare them:
int v = new Comparison<Weight>(
new Weight(400), new Weight(500)
).value();
This v
will either be -1
, 0
, or 1
. In this particular case it will be -1
,
because 400
is less than 500
.
No more violation of encapsulation, no more type casting, no more
ugly code inside those equals()
and compareTo()
methods.
The class Comparison
will work with all possible types. All our objects
need to do in order to become comparable is to implement Digitizable
and
“provide” their bytes for inspection/comparison.
This approach is actually very close to the printers I described earlier.