Moroccode is a small Kotlin utility to help you create easy and bug-free hash codes and equality checks. It is inspired by, and aims to improve, HashKode.
The functionality of both libraries is comparable, with Moroccode achieving slightly better performance in some cases, and being more concise in others. It is currently among the smallest, fastest and most concise of such libraries.
Mainly because HashKode is no longer being maintained.
From HashKode:
Java identifies objects by their
hashcode
and theirequals
method. If you do not override these methods, the JVM can only check objects for referential equality, not structural equality. (Read more)
Kotlin generates these methods automatically when you create a
data class
, but sometimes this is not preferred.
Implementing
hashcode
andequals
can be tedious, verbose and is bug prone. HashKode provides concise ways to override these methods.
- Lightweight: 8 KB*
- Fast
- Expressive
- Uses the standard library to compute hash codes: tested, high quality hashes
- Able to list differences
Moroccode can be downloaded from the Maven Central Repository.
Using Gradle with Kotlin DSL:
implementation("io.github.gabrielshanahan", "moroccode", "1.0.0")
Using Gradle with Groovy:
implementation 'io.github.gabrielshanahan:moroccode:1.0.0'
Using Maven
<dependency>
<groupId>io.github.gabrielshanahan</groupId>
<artifactId>moroccode</artifactId>
<version>1.0.0</version>
<type>pom.sha512</type>
</dependency>
Moroccode is released under the MIT license.
Moroccode makes creating hashes of your fields extremely convenient: just pass your fields to hash
.
override fun hashCode() = hash(
field1, field2, field3, field4
)
hash
uses Java's Objects.hashCode
and Kotlin's Array.contentHashCode
to calculate the hashes.
Checking two objects for structural equality can be done by overriding the builtin equals
method and using Moroccode's
equality builder functions. In our benchmarks, the performance difference between the two approaches is, in relative terms,
about 18%, which corresponds to a whopping 4.25 ns. For most applications, we advise to use the more expressive of the
two, compareUsingFields
.
override fun equals(other: Any?) = compareUsingFields(other) { fields { field1 } }
override fun equals(other: Any?) = compareUsingFields(other) { fields { field1 } and { field2 } and ... }
That's it.
It is also possible to use getters in place of the lambda's, however this makes the code more verbose and there is no performance benefit.
override fun equals(other: Any?) = compareUsingFields(other) { fields(MyClass::field1) }
override fun equals(other: Any?) = compareUsingFields(other) { fields(MyClass::field1) and MyClass::field2 and ... }
override fun equals(other: Any?): Boolean = compareUsing(other) { field1 == it.field1 && field2 == it.field2 && ... }
This method allows custom equality definitions - there is nothing stopping you from writing field1 == it.field1 || field2 == it.field2
Moroccode can list differences between fields of objects.
val test1 = Dummy(f1 = "Hello", f2 = 5)
val test2 = Dummy(f1 = "World", f2 = 5)
val diff = test1.diffByFields(test2) { listOf(f1, f2) }
diff
will be a list containing a single FieldDifference
object, since one different field was found.
FieldDifference
is a data class containing two pairs of the form object -> object.field_value
- one for the receiver
and one for the argument of diffByFields
.
A custom comparison can be done by using diffBy
:
val test1 = Dummy(f1 = "Hello", f2 = 5)
val test2 = Dummy(f1 = "World", f2 = 5)
val diffSame = test1.diffBy(test2) {
if (f1.length != it.f1.length) listOf(f1 to it.f1) else emptyList()
}
The lambda argument should return a list of all differences, encoded as Pair
s (i.e. field to it.field
).
We compare implementing equals
using Moroccode to achieving the same with the following:
- HashKode
- compareUsing (most efficient)
- compareFields (most concise)
- Apache Commons Lang
- nikarh/equals-builder
- consoleau/kassava
Library name | # of characters* | Speed | Size |
---|---|---|---|
Moroccode - compareUsing | 83 | 23.266 ± 0.500 ns/op | 8 KB |
Moroccode - compareUsingFields using getters | 132 (75 from class name) | 27.601 ± 0.602 ns/op | 8 KB |
Moroccode - compareUsingFields using fields | 55 | 27.520 ± 0.612 ns/op | 8 KB |
HashKode - compareFields (no longer maintained) | 177 (75 from class name) | 25.459 ± 0.478 ns/op | 15 KB |
HashKode - compareUsing (no longer maintained) | 115 | 23.297 ± 0.234 ns/op | 15 KB |
Apache Commons Lang | 157 (15 from class name) | 28.679 ± 0.361 ns/op | 492 KB |
nikarh/equals-builder | 186 (75 from class name) | 26.706 ± 0.200 ns/op | 4 KB |
consoleau/kassava | 135 (75 from class name) | 131.282 ± 2.324 ns/op | 5 KB |
* Characters where counted as number of characters necessary to implement the equals method, excluding whitespace and a single count of every fields name, since this is by definition a necessary minimum every implementation must have (in this instances, the field names where anInt, aString, aDouble, aBool and aList, for a total of 29 characters. See the benchmark for the code that was used.
If the implementation needed to use the class name, typically when referencing getters, it is noted (in this case, the class was BenchmarkObject, which is 15 characters long).
- MacBook Pro (13-inch, 2018)
- 2,3 GHz Quad-Core Intel Core i5
- 16 GB 2133 MHz LPDDR3
- macOS Catalina 10.15.2 (19C57)
- Object with 5 fields:
- Integer (index)
- String (
Hello World
) - Double (
Math.PI
) - Boolean (
true
) - List (
["A", "B", "C", "D", "E"]
)
- Benchmark implemented using JMH with default settings.
- The entire code is available here