This library provides some useful kotlin extension functions for implementing toString(), hashCode() and equals() without all of the boilerplate.

Overview

Kassava

Build Status License Download Awesome Kotlin Badge

This library provides some useful kotlin extension functions for implementing toString(), equals() and hashCode() without all of the boilerplate.

The main motivation for this library was for situations where you can't use data classes and are required to implement toString()/equals()/hashCode() by:

  • hand-crafting your own :(
  • using the IDE generated methods :(
  • using Apache Common's ToStringBuilder and EqualsBuilder
    • customizable toString() format (can replicate Kotlin's data class format)
    • reflectionEquals() and reflectionToString() are super easy, but have awful performance
    • normal builders are still easy, but require lots of boilerplate
  • using Guava's MoreObjects
    • toStringBuilder() performs better than Apache, but still requires the same boilerplate, and the format is different to the data class format (it uses braces instead of parentheses)
    • there's no equivalent builder for equals() (you're meant to use Java's Objects.equals() but that's lots of boilerplate)
    • it's a large library (2MB+) if you're not already using it
  • or...something else!

Kassava provides extension functions that you can use to write your equals(), toString() and hashCode() methods with no boilerplate (using the kotlinEquals(), kotlinToString() and kotlinHashCode() methods respectively).

It's also really tiny (about 6kB), as it doesn't depend on any other libraries (like Apache Commons, or Guava). A special shoutout to Guava is required though, as the implementation of kotlinToString() is based heavily on the logic in Guava's excellent ToStringHelper.

How does it perform? Check the benchmark results below!

Quick Start

repositories {
    jcenter()
}

dependencies {
    compile("au.com.console:kassava:2.1.0")
}

Simple Example

// 1. Import extension functions
import au.com.console.kassava.kotlinEquals
import au.com.console.kassava.kotlinHashCode
import au.com.console.kassava.kotlinToString

import java.util.Objects

class Employee(val name: String, val age: Int? = null) {

    // 2. Optionally define your properties for equals/toString in a companion object
    // (Kotlin will generate less KProperty classes, and you won't have array creation for every method call)
    companion object {
        private val properties = arrayOf(Employee::name, Employee::age)
    }

    // 3. Implement equals() by supplying the list of properties used to test equality
    override fun equals(other: Any?) = kotlinEquals(other = other, properties = properties)

    // 4. Implement toString() by supplying the list of properties to be included
    override fun toString() = kotlinToString(properties = properties)

    // 5. Implement hashCode() by supplying the list of properties to be included
    override fun hashCode() = kotlinHashCode(properties = properties)
}

Polymorphic Example

Implementing equals() with polymorphic classes (mixed type equality) is rarely done correctly, and is often the subject of heavy debate!

For a thorough explanation of the problem and possible solutions, please refer to both Angelika Langer's Secret of equals() article, and Artima's How to Write an Equality Method in Java article.

In a nutshell, using instanceof is too lenient (and leads to implementations of equals() that are not transitive), and using getClass() is too strict (classes that extend in a trivial way without adding fields are no longer candidates for equality).

Kassava supports a solution that originated in the Scala world, and is proposed in the linked Artima article (and also implemented in lombok for those interested). The implementation makes use of a new interface SupportsMixedTypeEquality with a canEquals() method to achieve this. The purpose of this method is, as stated in the Artima article:

...as soon as a class redefines equals (and hashCode), it should also explicitly state that objects of 
this class are never equal to objects of some superclass that implement a different equality method. 
This is achieved by adding a method canEqual to every class that redefines equals.

Take a look at the unit tests for the kotlinEquals() method, and you'll see how the Point/ColouredPoint/anonymous Point scenario in the article works (and how classes that extend in trivial ways - with no extra fields - can still be considered equal with this method).

Here is an example of it in use (in a typical kotlin sealed class example).

Note the use of superEquals, superHashCode and superToString in the subclasses - these are lambdas that allow you to reuse the logic in your parent class.

import au.com.console.kassava.kotlinEquals
import au.com.console.kassava.kotlinHashCode
import au.com.console.kassava.kotlinToString
import au.com.console.kassava.SupportsMixedTypeEquality

/**
 * Animal base class with Cat/Dog subclasses.
 */
sealed class Animal(val name: String) : SupportsMixedTypeEquality {

    override fun equals(other: Any?) = kotlinEquals(
        other = other,
        properties = properties
    )

    // only Animals can be compared to Animals
    override fun canEqual(other: Any?) = other is Animal

    override fun toString() = kotlinToString(properties = properties)

    override fun hashCode() = kotlinHashCode(properties = properties)

    companion object {
        private val properties = arrayOf(Animal::name)
    }

    class Cat(name: String, val mice: Int) : Animal(name = name) {
        
        override fun equals(other: Any?) = kotlinEquals(
            other = other,
            properties = properties,
            superEquals = { super.equals(other) }
        )

        // only Cats can be compared to Cats
        override fun canEqual(other: Any?) = other is Cat

        override fun toString() = kotlinToString(
            properties = properties,
            superToString = { super.toString() }
        )

        override fun hashCode() = kotlinHashCode(
            properties = properties,
            superHashCode = { super.hashCode() }
        )

        companion object {
            private val properties = arrayOf(Cat::mice)
        }
    }

    class Dog(name: String, val bones: Int, val balls: Int? = null) : Animal(name = name) {

        override fun equals(other: Any?) = kotlinEquals(
            other = other,
            properties = properties,
            superEquals = { super.equals(other) }
        )

        // only Dogs can be compared to Dogs
        override fun canEqual(other: Any?) = other is Dog

        override fun toString() = kotlinToString(
            properties = properties,
            superToString = { super.toString() }
        )

        override fun hashCode() = kotlinHashCode(
            properties = properties,
            superHashCode = { super.hashCode() }
        )

        companion object {
            private val properties = arrayOf(Dog::bones, Dog::balls)
        }
    }
}

Benchmarking

While Kassava's usage is very readable and maintainable, how does it perform against the alternatives?

A Kassava JMH benchmark project was created to test this. You can see the test class implements all the variations of toString() and equals(), including:

  • normal implementation (boring old IDE-generated style)
  • Objects implementation (same as above, but with Java's Objects.equals() and Objects.toString())
  • Apache implementation
  • Apache reflection implementation
  • Guava implementation (for toString() only, there's no equivalent for equals()
  • Kassava implementation (with reused properties array)
  • Kassava implementation (with new array of properties each time)

The benchmark (using Kassava 1.0.0) was run on Travis with 10 warmup iterations, 5 test iterations, 1 fork, and measuring average time in nanoseconds. The raw results of the benchmark are:

Benchmark                                           Mode  Cnt     Score    Error  Units
EqualsBenchmark.apacheEquals                        avgt    5     5.365 ±  2.447  ns/op
EqualsBenchmark.apacheReflectionEquals              avgt    5   569.729 ±  5.990  ns/op
EqualsBenchmark.kassavaEquals                       avgt    5    84.647 ±  0.429  ns/op
EqualsBenchmark.kassavaEqualsWithArrayCreation      avgt    5    87.274 ±  0.520  ns/op
EqualsBenchmark.manualEquals                        avgt    5     5.665 ±  0.081  ns/op
EqualsBenchmark.manualObjectsEquals                 avgt    5     6.866 ±  0.042  ns/op
ToStringBenchmark.apacheReflectionToString          avgt    5  1484.542 ± 28.615  ns/op
ToStringBenchmark.apacheToString                    avgt    5   922.272 ± 52.431  ns/op
ToStringBenchmark.guavaToString                     avgt    5   344.156 ±  6.403  ns/op
ToStringBenchmark.kassavaToString                   avgt    5   416.654 ± 10.255  ns/op
ToStringBenchmark.kassavaToStringWithArrayCreation  avgt    5   420.433 ±  7.425  ns/op
ToStringBenchmark.manualObjectsToString             avgt    5   140.707 ±  2.457  ns/op
ToStringBenchmark.manualToString                    avgt    5   118.061 ±  2.196  ns/op

TLDR:

  • Kassava's equals() implementation is definitely slower than manual/Apache/Guava (approx 15x slower), but nowhere near as bad as Apache's reflection implementation (approx 100x slower)
  • Kassava's toString() implementation is only slightly slower than Guava (and faster than Apache!)
  • Apache's equals() implementation is faster than the manual implementation??? (Magic?)

Contributing to the Project

If you'd like to contribute code to this project you can do so through GitHub by forking the repository and generating a pull request.

By contributing your code, you agree to license your contribution under the terms of the Apache License v2.0.

License

Copyright 2016 RES INFORMATION SERVICES PTY LTD

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Comments
  • Is the project alive?

    Is the project alive?

    Hello @jamesbassett

    I would like to know if this project is still alive, because I would like to make some contributions.

    My plan is:

    1. Actualice versions of Kassava Benchmark, to get that the benchmark project uses the same than this project. Also this will allow to use benchmark with JVM 11 (PR already pending #https://github.com/consoleau/kassava-benchmarks/pull/1)
    2. Add to Kassava Benchmark the tests for hashCode
    3. Improve Kassava performance a little. I did some tests in local and the equals method has less improvement, but I think that the hashCode and toString methods could be interesting. So I would like that you review the changes and give me your opinion.

    Thanks a lot!

    opened by alejandropg 2
  • Add kotlinHashCode method

    Add kotlinHashCode method

    Yes, implementing hashCode() is trivial thanks to Objects.hash(), but this method allows passing a list of properties. If you want to ensure equals() and hashCode() are based on the same set of properties (which makes holding up their contract easier), you can just pass the same stored list of properties to kotlinEquals() and kotlinHashCode() and not worry about missing a property.

    It's also somewhat verbose, especially for larger objects, to write a property name twice, once to retrieve the property for equals() if using kotlinEquals() and once to retrieve the value for hashCode() if using Objects.hash().

    opened by fdeitylink 1
  • Updated kotlinEquals() to use deep array equality

    Updated kotlinEquals() to use deep array equality

    The current implementation is only referential equality for array fields - this PR uses Objects.deepEquals() to perform a deep equality check for array fields.

    This seems like a good idea, but is there ever any time you wouldn't want this?

    opened by jamesbassett 0
  • Moving kassava from jcenter to another repository

    Moving kassava from jcenter to another repository

    Hey there, I just started using kassava in a project, but I want to continue to use it. Sadly bintray and jcenter are shutting down in February 2022. So it would bbe nice if you could move it to another repository.

    The Article: https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/

    Thank you so much 😄

    opened by Aristamo 0
  • Little performance improvement

    Little performance improvement

    Hi @jamesbassett

    Here is my little performance improvement.

    I did several benchmarks and the results are (more or less):

    • hashCode - 27%
    • toString - 13%
    • equals - some times something like 1% so it's basically the same

    My benchmarks to get this numbers (please review if I did the calculus correctly 😅)

    #
    # CURRENT VERSION
    #
    # Run complete. Total time: 00:50:11
    Benchmark                                           Mode  Cnt    Score   Error  Units
    EqualsBenchmark.apacheEquals                        avgt    5    3.308 ± 0.025  ns/op
    EqualsBenchmark.apacheReflectionEquals              avgt    5  468.797 ± 6.000  ns/op
    EqualsBenchmark.kassavaEquals                       avgt    5   65.873 ± 0.180  ns/op
    EqualsBenchmark.kassavaEqualsWithArrayCreation      avgt    5   68.474 ± 0.695  ns/op
    EqualsBenchmark.manualEquals                        avgt    5    3.717 ± 0.046  ns/op
    EqualsBenchmark.manualObjectsEquals                 avgt    5    4.041 ± 0.055  ns/op
    HashCodeBenchmark.apacheHashCode                    avgt    5    4.228 ± 0.124  ns/op
    HashCodeBenchmark.apacheReflectionHashCode          avgt    5  357.881 ± 4.408  ns/op
    HashCodeBenchmark.guavaHashCode                     avgt    5   18.931 ± 0.131  ns/op
    HashCodeBenchmark.kassavaHashCode                   avgt    5   48.910 ± 1.288  ns/op
    HashCodeBenchmark.kassavaHashCodeWithArrayCreation  avgt    5   50.653 ± 0.539  ns/op
    HashCodeBenchmark.manualHashCode                    avgt    5    3.242 ± 0.028  ns/op
    HashCodeBenchmark.manualObjectsHashCode             avgt    5   18.053 ± 0.305  ns/op
    ToStringBenchmark.apacheReflectionToString          avgt    5  829.341 ± 6.808  ns/op
    ToStringBenchmark.apacheToString                    avgt    5  453.315 ± 7.458  ns/op
    ToStringBenchmark.guavaToString                     avgt    5  132.879 ± 1.757  ns/op
    ToStringBenchmark.kassavaToString                   avgt    5  156.238 ± 5.518  ns/op
    ToStringBenchmark.kassavaToStringWithArrayCreation  avgt    5  157.057 ± 1.303  ns/op
    ToStringBenchmark.manualObjectsToString             avgt    5   65.010 ± 0.803  ns/op
    ToStringBenchmark.manualToString                    avgt    5   57.267 ± 0.520  ns/op
    
    
    #
    # NEW VERSION
    #
    # Run complete. Total time: 00:50:10
    
    Benchmark                                           Mode  Cnt    Score   Error  Units
    EqualsBenchmark.apacheEquals                        avgt    5    3.366 ± 0.085  ns/op
    EqualsBenchmark.apacheReflectionEquals              avgt    5  402.675 ± 3.926  ns/op
    EqualsBenchmark.kassavaEquals                       avgt    5   65.736 ± 2.350  ns/op
    EqualsBenchmark.kassavaEqualsWithArrayCreation      avgt    5   66.799 ± 1.101  ns/op
    EqualsBenchmark.manualEquals                        avgt    5    3.958 ± 0.029  ns/op
    EqualsBenchmark.manualObjectsEquals                 avgt    5    4.044 ± 0.059  ns/op
    HashCodeBenchmark.apacheHashCode                    avgt    5    4.250 ± 0.078  ns/op
    HashCodeBenchmark.apacheReflectionHashCode          avgt    5  342.284 ± 4.132  ns/op
    HashCodeBenchmark.guavaHashCode                     avgt    5   18.900 ± 0.488  ns/op
    HashCodeBenchmark.kassavaHashCode                   avgt    5   35.518 ± 0.360  ns/op
    HashCodeBenchmark.kassavaHashCodeWithArrayCreation  avgt    5   38.481 ± 0.531  ns/op
    HashCodeBenchmark.manualHashCode                    avgt    5    3.657 ± 0.030  ns/op
    HashCodeBenchmark.manualObjectsHashCode             avgt    5   18.480 ± 0.241  ns/op
    ToStringBenchmark.apacheReflectionToString          avgt    5  858.053 ± 8.194  ns/op
    ToStringBenchmark.apacheToString                    avgt    5  469.023 ± 7.078  ns/op
    ToStringBenchmark.guavaToString                     avgt    5  139.953 ± 1.442  ns/op
    ToStringBenchmark.kassavaToString                   avgt    5  135.790 ± 2.457  ns/op
    ToStringBenchmark.kassavaToStringWithArrayCreation  avgt    5  138.782 ± 2.446  ns/op
    ToStringBenchmark.manualObjectsToString             avgt    5   65.615 ± 1.744  ns/op
    ToStringBenchmark.manualToString                    avgt    5   57.693 ± 0.533  ns/op
    

    Review the changes and tell me what do you think.

    Thanks a lot!

    opened by alejandropg 4
  • Use callable instead of property?

    Use callable instead of property?

    Instead of providing a list of properties, can I refer to a member of the class that's a callable function, which will return a the value to use for comparisons?

    opened by thauk-copperleaf 2
Releases(v2.1.0)
Owner
Console Australia
Console Australia
Boilerplate code for implementing MVVM in Android using Jetpack libraries, coroutines, dependency injection and local persistance

MVVM Foundation This projects aims to speed up development of Android apps by providing a solid base to extend Libraries Jetpack Fragment Material3 :

Gabriel Gonzalez 2 Nov 10, 2022
A kotlin library of extension functions that add smalltalk style methods to objects.

KtTalk A kotlin library of extension functions that add smalltalk style methods to objects. Motivation Smalltalk is a pure OO language in which everyt

null 11 Oct 16, 2021
Extension functions over Android's callback-based APIs which allows writing them in a sequential way within coroutines or observe multiple callbacks through kotlin flow.

callback-ktx A lightweight Android library that wraps Android's callback-based APIs into suspending extension functions which allow writing them in a

Sagar Viradiya 171 Oct 31, 2022
A collection of useful Kotlin extension for Android

karamba A collection of useful Kotlin extension for Android Install Add to gradle in allprojects maven { url 'https://jitpack.io' } then add this com

Matteo Crippa 48 Dec 15, 2022
A collection of useful extension methods for Android

Kotlin Jetpack A collection of useful extension methods for Android Arguments Bindings Preferences Bindings Resources Bindings Arguments Bindings publ

Vladimir Mironov 187 Nov 10, 2022
Write a Ghidra Extension without using Java or Eclipse!

Ghidra Extension in Kotlin using IntelliJ IDEA Write a Ghidra Extension without using Java or Eclipse! Setup Hit Use this template at the top of the r

Florian Magin 7 Dec 15, 2022
A collection of small utility functions to make it easier to deal with some otherwise nullable APIs on Android.

requireKTX requireKTX is a collection of small utility functions to make it easier to deal with some otherwise nullable APIs on Android, using the sam

Márton Braun 82 Oct 1, 2022
WolfxPaper - A Paper fork designed for Wolfx Survial, may useful for some Semi-Vanilla Server

WolfxPaper A Paper fork designed for Wolfx Survial, may useful for some "Semi-Va

TenkyuChimata 1 Jan 19, 2022
Andorid app which provides a bunch of useful Linux commands.

Linux Command Library for Android The app currently has 3203 manual pages, 1351 one-line scripts and a bunch of general terminal tips. It works 100% o

Simon Schubert 276 Dec 31, 2022
Provides Kotlin libs and some features for building Kotlin plugins

Kotlin Plugin Provides Kotlin libs and some features for building awesome Kotlin plugins. Can be used instead of CreeperFace's KotlinLib (don't use to

null 3 Dec 24, 2021
A Kotlin library for reactive and boilerplate-free SharedPreferences in Android

KPreferences A Kotlin library for reactive and boilerplate-free Shared Preferences in Android. With KPreferences you can use Kotlin's marvelous delega

Mohamad Amin Mohamadi 19 Dec 16, 2020
👋 A common toolkit (utils) ⚒️ built to help you further reduce Kotlin boilerplate code and improve development efficiency. Do you think 'kotlin-stdlib' or 'android-ktx' is not sweet enough? You need this! 🍭

Toolkit [ ?? Work in progress ⛏ ?? ??️ ?? ] Snapshot version: repositories { maven("https://s01.oss.sonatype.org/content/repositories/snapshots") }

凛 35 Jul 23, 2022
An Android template project (in Kotlin) with boilerplate and current patterns.

android-starter-v4 An Android template project (in Kotlin) with boilerplate and plumbing, exploring current architecture patterns. A bit too much for

Matthias Urhahn 14 Nov 4, 2022
Minecraft 1.18.2 Backport of Petal, a performance-oriented fork of Purpur intended to increase performance for entity-heavy servers by implementing multi-threaded and asynchronous improvements.

Sakura Performance Minecraft JAR Sakura is a performance-oriented fork of Purpur intended to increase performance for entity-heavy servers by implemen

etil.sol 14 Nov 23, 2022
Android Ptrace Inject for all ABIs and all APIs. Help you inject Shared Library on Android.

Android Ptrace Inject 中文可以参考我的注释内容进行理解 我写的注释相对来说比较全面了 How to build Make sure you have CMake and Ninja in your PATH Edit CMakeLists.txt. Set ANDROID_ND

SsageParuders 65 Dec 19, 2022
A fork of our clean architecture boilerplate using the Model-View-Intent pattern

Android Clean Architecture MVI Boilerplate Note: This is a fork of our original Clean Architecture Boilerplate, except in this repo we have switched o

Buffer 974 Dec 29, 2022
A fork of our clean architecture boilerplate, this time using the Android Architecture Components

Android Clean Architecture Components Boilerplate Note: This is a fork of our original Clean Architecture Boilerplate, except in this repo we have swi

Buffer 1.3k Jan 3, 2023
An android boilerplate project using clean architecture

Android Clean Architecture Boilerplate Welcome ?? We hope this boilerplate is not only helpful to other developers, but also that it helps to educate

Buffer 3.6k Jan 4, 2023
Android-Boilerplate - Base project for android development with new technology

Android-Boilerplate Base project for android development with new technology, in

Muhammad Rizky Arifin 1 Aug 15, 2022