Choosing Between Identity and Structural Equality in Ceylon Classes
Ceylon's == operator calls equals(), while === compares identity. Learn when to override equals() and hash for structural equality, with a working example and validation steps.
24 Jun 2026, 22:10 UTC

The Equality Decision in Ceylon
In Ceylon, the == operator does not compare object identity. It calls the equals() method inherited from ceylon.language::Object, whose default implementation compares identity. A separate operator, ===, always compares identity and cannot be overridden. This gives you an explicit decision to make for every class you write: keep the default identity-based equality, or override equals() and hash to get structural (field-based) equality.
The takeaway: override equals() and hash for immutable data carriers that will be compared by content or used as keys in sets and maps. Keep the default identity equality for entities whose identity is meaningful, such as services or stateful resources.
Comparison of the Two Options
| Aspect | Default (identity) | Overridden equals()/hash |
|---|---|---|
a == b semantics | Same instance only | Field-by-field comparison you define |
a === b semantics | Identity (unchanged) | Identity (unchanged) |
Behavior in Set/Map keys | Distinct instances never collide | Equal instances treated as one key |
| Code required | None | Must override both equals() and hash consistently |
| Good fit | Entities, services, mutable resources | Coordinates, money, ranges, DTO-like carriers |
Trade-offs
Consistency between equals and hash
If you override equals() without overriding hash, two instances that compare equal can still land in different hash buckets, and Set membership checks will behave incorrectly. The language documentation and the Object contract require that equal objects produce equal hashes. Always override the pair together, and derive the hash from the same fields used in the equality check.
Mutability risk
If equality depends on a mutable field and that field changes while the instance is a key in a hash-based collection, lookups will fail silently. Restrict custom equality to classes whose equality-relevant fields are immutable (declared without variable).
Type checking in equals
The parameter of equals() is Object?. A correct implementation narrows the type before comparing fields, typically with if (is MyClass that), and returns false for anything else, including null.
Concrete Implementation
The following sketch shows a class with structural equality and a check demonstrating both operators. Confirm exact syntax against your installed Ceylon compiler version; the language's tooling has not been actively developed for years, so treat this as a guide to the documented semantics rather than a tested listing.
class Point(shared Integer x, shared Integer y) {
shared actual Boolean equals(Object that) {
if (is Point that) {
return x == that.x && y == that.y;
}
return false;
}
shared actual Integer hash {
return 31 * x + y;
}
}
shared void run() {
value a = Point(1, 2);
value b = Point(1, 2);
print(a == b); // structural: expected true
print(a === b); // identity: expected false
print(a === a); // identity: expected true
}How to validate
- Where to run: In a Ceylon module source file, compiled with the
ceylon compileCLI and executed withceylon run <module.name>, replacing<module.name>with your module's declared name. You need the Ceylon SDK on your PATH and write access to the module output directory. - Expected result: The three print statements produce
true,false,true. If the first printsfalse, yourequals()override is not being picked up—check theshared actualannotations and the parameter type. - Collection check: Add both
aandbto aHashSetand confirm its size is 1. A size of 2 indicateshashandequals()are inconsistent.
Limitations and Caveats
Overriding equality interacts with inheritance: if a subclass adds fields, the parent's equals() may consider a subclass instance equal to a parent instance, breaking symmetry. A common conservative approach is to only override equality in classes not intended for extension, or to compare runtime types strictly.
Also note that Ceylon has no user-defined value-type construct: the value keyword is a local declaration modifier for type inference, not a type definition. Structural equality in Ceylon is achieved only by overriding equals() and hash on ordinary classes, exactly as shown above. Finally, given the project's slowed development, verify compiler behavior on your installed version before relying on any specific tooling behavior.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.