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