Skip to content

Latest commit

 

History

History
187 lines (146 loc) · 7.96 KB

README.md

File metadata and controls

187 lines (146 loc) · 7.96 KB

Moroccode

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.

Why?

Mainly because HashKode is no longer being maintained.

From HashKode:

Java identifies objects by their hashcode and their equals 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 and equals can be tedious, verbose and is bug prone. HashKode provides concise ways to override these methods.

Features

  • Lightweight: 8 KB*
  • Fast
  • Expressive
  • Uses the standard library to compute hash codes: tested, high quality hashes
  • Able to list differences

Using Moroccode

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.

How to use


Hash codes

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.


Equality

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.

compareUsingFields - The concise way

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 ... }

compareUsing - The efficient way

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


Difference

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 Pairs (i.e. field to it.field).


Comparison to alternative libraries

We compare implementing equals using Moroccode to achieving the same with the following:

Results

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).

Benchmark setup

  • MacBook Pro (13-inch, 2018)
  • 2,3 GHz Quad-Core Intel Core i5
  • 16 GB 2133 MHz LPDDR3
  • macOS Catalina 10.15.2 (19C57)

Benchmark details

  • 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