Motivation

When you’re migrating from Java to Kotlin (or even just trying to access one language from the perspective of another) it’s not always clear what can be done in a better way. That’s especially the case if something is different in subtle way or was changed over time in Java itself. Official Kotlin documentation covers a couple of topics like Strings or formal comparison which is nice.

So main intention for "in Java and Kotlin" article series is to explore some topics that may differ from one language to another. To do that, comprehensive description of specific feature in both language will be done.

As the first example of things to improve, let’s compare how to check equality in Java and Kotlin.

Primitives

Java

In Java we have separate primitive types like int, double and others. Checking equality of them is as simple as i == 2:

public class Main {
    public static void main(String[] args) {
        int i = 2;
        System.out.println(i == 2);
    }
}

Also, for each primitive type we have corresponding types like Integer for int, Double for double and others. We can also use == operator for comparing primitive type and corresponding wrapper type:

public class Main {
    public static void main(String[] args) {
        int i = 2;
        Integer nullableI = 2;
        System.out.println(i == nullableI);
    }
}

However, you should be careful as wrapper type object can be equal to null and comparing that to primitive will cause NPE (as Java will try to unbox our Integer before comparing it to int)

public class Main {
    public static void main(String[] args) {
        int i = 2;
        Integer nullI = null;
        // will throw NPE due to unboxing of null value
        System.out.println(i == nullI);
    }
}

Also, you should be careful when comparing 2 wrapper types as that may work in some cases but not in all of them:

public class Main {
    public static void main(String[] args) {
        // true as 0 is inside of integer cache by default
        System.out.println(Integer.valueOf(0) == Integer.valueOf(0));
        // false as 10000 is outside of integer cache by default
System.out.println(Integer.valueOf(10000) == Integer.valueOf(10000));
    }
}

Next section Objects will cover proper equality check of non-primitive types in Java.

Kotlin

In Kotlin we don’t have explicit separation between primitive types and non-primitive ones.[1] For all basic types and their nullable variations == operator will work just fine:

fun main() {
    val i = 2
    val nullableI: Int? = 2
    val nullI: Int? = null
    println(i == 2)
    println(i == nullableI)
    println(i == nullI)
    println(nullableI == nullI)
}

Objects

Java

As mentioned in the previous section, == may not work for checking equality for non-primitive types. Reason for that is that == operator in Java checks identity for non-primitive types. It’s pretty common to have equal, but non-identical objects:

public class Main {
    record Foo(int i, String s) {
    }
    public static void main(String[] args) {
        Foo foo = new Foo(2, "abc");
        // false as objects are non-identical
        System.out.println(foo == new Foo(2, "abc"));
    }
}

To check equality you can use equals method:

public class Main {
    record Foo(int i, String s) {
    }
    public static void main(String[] args) {
        Foo foo = new Foo(2, "abc");
        // true as objects are equal
        System.out.println(foo.equals(new Foo(2, "abc")));
    }
}

However, you should be careful while using equals method as it will fail if you try to use it on null object

public class Main {
    record Foo(int i, String s) {
    }
    public static void main(String[] args) {
        Foo nullFoo = null;
        // will fail with NPE
        System.out.println(nullFoo.equals(new Foo(2, "abc")));
    }
}

Previously there was a convention that you can call equals on literal object:

public class Main {
    record Foo(int i, String s) {
    }
    public static void main(String[] args) {
        Foo nullFoo = null;
        // will not fail with NPE
        System.out.println(new Foo(2, "abc").equals(nullFoo));
    }
}

But since Java 1.7 we have another option: Objects.equals:

import java.util.Objects;

public class Main {
    record Foo(int i, String s) {
    }
    public static void main(String[] args) {
        Foo nullFoo = null;
        // will not fail with NPE
        System.out.println(Objects.equals(nullFoo, new Foo(2, "abc")));
    }
}

This option is more reliable but less convenient to use.

It is also worth noting that everything mentioned in this section applicable for collections and maps as well, so you can use .equals or Objects.equals to check lists, sets or maps:

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        var map = Map.of(1, "one", 2, "two");
        var list = List.of(1, 2, 3);
        var set = Set.of(2, 1, 3);
        // all three lines will be true
        System.out.println(map.equals(Map.of(2, "two", 1, "one")));
        System.out.println(list.equals(List.of(1, 2, 3)));
        System.out.println(Objects.equals(set, Set.of(1, 2, 3)));
    }
}

Kotlin

As it was the case for basic types, == operator handles everything related to equality check:

data class Foo(val i: Int, val s: String)

fun main() {
    val foo = Foo(2, "abc")
    val nullableFoo: Foo? = Foo(2, "abc")
    val nullFoo: Foo? = null
    println(foo == Foo(2, "abc"))
    println(nullableFoo == foo)
    println(nullFoo == foo)
    println(nullableFoo == nullFoo)
}

Similar to Java, collections and maps also can be checked for equality in the same way:

fun main() {
    val map = mapOf(1 to "one", 2 to "two")
    val list = listOf(1, 2, 3)
    val set = setOf(2, 1, 3)
    // all three lines will be true
    println(map == mapOf(2 to "two", 1 to "one"))
    println(list == listOf(1, 2, 3))
    println(set == setOf(1, 2, 3))
}

Arrays

Java

If you are worked with arrays in Java before you might know that they don’t have "proper" equals/hashcode so neither using == nor equals method will not work:

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        // false, non-identical
        System.out.println(arr == new int[] {1, 2, 3});
        // false, arrays don't have proper equals
        System.out.println(arr.equals(new int[] {1, 2, 3}));
    }
}

To compare content of your array you need to use Arrays.equals:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        // true
        System.out.println(Arrays.equals(arr, new int[] {1, 2, 3}));
    }
}

Kotlin

If you are using array types (like Array, IntArray and others) you will face the same issue as in Java:

fun main() {
    val arr = intArrayOf(1, 2, 3)
    // false, arrays don't have proper equals
    println(arr == intArrayOf(1, 2, 3))
}

You can avoid the issue by using contentEquals extension function.[2]:

fun main() {
    val arr = intArrayOf(1, 2, 3)
    // true
    println(arr.contentEquals(intArrayOf(1, 2, 3)))
}

Note - you may prefer to just avoid the issue completely by using List or MutableList whenever possible (same is true for Java)

Nested Arrays

Java

While checking equality of nested arrays neither equals method nor Arrays.equals will not work:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[][] matrix = {{1, 2}, {3, 4}};
        // false, arrays don't have proper equals
        System.out.println(arr.equals(new int[][] {{1, 2}, {3, 4}}));
        // false, shallow content check doesn't work for nested arrays
        System.out.println(Arrays.equals(arr, new int[][] {{1, 2}, {3, 4}}));
    }
}

You can use Arrays.deepEquals to check equality properly:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[][] matrix = {{1, 2}, {3, 4}};
        // true
        System.out.println(Arrays.deepEquals(arr, new int[][] {{1, 2}, {3, 4}}));
    }
}

Kotlin

Same issue is applicable for Kotlin, neither == operator nor contentEquals are applicable for nested arrays:

fun main() {
    val matrix = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4))
    // false, arrays don't have proper equals
    println(matrix == arrayOf(intArrayOf(1, 2), intArrayOf(3, 4)))
    // false, shallow content check doesn't work for nested arrays
    println(matrix.contentEquals(arrayOf(intArrayOf(1, 2), intArrayOf(3, 4))))
}

You can use contentDeepEquals extension function to avoid the problem:

fun main() {
    val matrix = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4))
    // true
    println(matrix.contentDeepEquals(arrayOf(intArrayOf(1, 2), intArrayOf(3, 4))))
}

Conclusion

In general Kotlin solves a lot of complexity that we can see in Java related to equality checks. All == operators, equals methods, Objects.equals functions became just == operators in Kotlin which is far more convenient. Arrays and nested arrays are still require some workarounds, but they are a bit more convenient (no need for imports, extension functions instead of usual function with 2 arguments)

I hope you find this article useful.

Side note 1: Identity check in Kotlin

You may have a situation when you want to compare identity of some objects. Because in Kotlin == operator is now taken for equality check, new === operator is introduced for identity check purposes:

data class Foo(val i: Int, val s: String)

fun main() {
    val foo = Foo(2, "abc")
    val otherFoo = Foo(2, "abc")
    val oneMoreFoo = foo

    // true as objects are equal
    println(foo == otherFoo)
    // false as objects are non-identical
    println(foo === otherFoo)
    // true as objects are identical
    println(foo === oneMoreFoo)
}

Side note 2: Usage of equals in Kotlin

While it’s not necessary to do so, you still can use .equals to check equality. One thing to notice - nullable types don’t have .equals function. The following code is not compilable:

fun main() {
    val i = 2
    val nullableI: Int? = 2
    println(nullableI.equals(i))
}

But in general, using equals function instead of == operator is not really needed in Kotlin


1. details on how primitive types looks under the hood can be found here: kotlinlang.org/docs/basic-types.html#numbers-representation-on-the-jvm
2. Technically you can also use Arrays.equals from Java but there are not a lot of reasons to do such thing