This article looks at many categories of Java defects that Kotlin prevents in addition to null safety. My first article (5 cool thing about Kotlin) provided a brief introduction to Kotlin and showed a surprising impact on productivity.
It’s important to realize the difference in value in fixing a defect in one class versus preventing an entire category of Java defects from occurring in any class.
What we see today:
- Equality Defect Categories
- Data Class Defect Categories
- Switch Defect Categories
- Assignment Defect Categories
- Override Defect Categories
- Null Defect Categories
Let’s see How can you remove Java defects using Kotlin
Preventing defects reduces the amount of band-aid style hot-fixes which are known to cause spaghetti code.
Equality Defect Categories
A common source of defects in Java is caused by accidentally checking referential equality instead of true value equality. The examples below show how these types of defects can be introduced even by experienced developers.
The confusion stems from “==” having a dual purpose. Primitives must be compared using “==” and unfortunately you can also use “==” to check object referential equality. The line is further blurred since “==” works as expected for certain types of objects such as singletons, enums, Class instances, etc. so these defects can be sneaky.
Using “==” in Kotlin checks true value equality so it avoids these common categories of defects:
- Changing method return type from primitive to wrapper type still compiles but can break existing equality checks:
// Java // getAge() returned an int so this check was correct but // was later changed to return Integer if (brother.getAge() == sister.getAge()) // potential twins ...similarly for all 8 primitive types
- Refactoring by inlining code can break equality checks when autoboxing / unboxing is involved since it looks like we’re dealing with primitive types:
// Java int employeeAge = employee.getAge(); int supervisorAge = supervisor.getAge(); if (employeeAge == supervisorAge) // Refactor the above 3 lines and replace with this broken version: if (employee.getAge() == supervisor.getAge()) ...similarly for all 8 primitive types
- Object caching further complicates things since referential equality can work correctly during testing and fail with customer data:
// Java Integer first = 100; Integer second = 100; // Condition passes since these values use the Integer cache if (first == second) ... Integer third = 200; Integer fourth = 200; // Oops, condition fails since 200 is out of range of Integer cache if (third == fourth) ...factory pattern can cause similar caching problems with any class
- Checking referential equality when we shouldn’t. This can occur if we’re not careful. It can also be caused by more complex scenarios where it used to work correctly (eg. break checks by no longer interning strings):
// Java if (firstName == lastName) abortOperation("First name must be different than last name);
If you purposely want to check for referential equality, Kotlin has triple equals for that so it’s never accidental. A nice addition is that Kotlin eliminates the null check clutter when checking against potential null values due to its stronger type system (details in “Null Defect Categories” section below).
Data Class Defect Categories
Good design principles suggest packaging related data together. For example, a Person class stores various properties of a person:
// Kotlin data class Person(var name: String, val dateOfBirth: LocalDate)
That’s right, a single line of Kotlin fully defines the Person class. The data keyword generates getters, setters, toString, equals, hashCode, copy, and other functions to enable additional useful language features.
Recommended article : What is Nothing Type in Kotlin?
The equals and hashCode methods are important as we usually work with collections.
Defining an equivalent Person class in Java requires much more than 1 line:
Oops, I lied. The Java version is not quite equivalent since it’s missing an easy way to copy it (eg. clone or copy constructor). I also had to strip out all the nullability annotations and trivialize the Person class with only 2 properties to fit it in the screenshot but data classes typically contains at least a handful of properties. The size difference increases when you add JavaDocs on multiple methods versus the single Kotlin doc for the data class.
For developers : Top 20 Daily used Kotlin Code Snippet
Kotlin data classes are much easier to understand and automate most of the work which avoids these categories of defects:
- Checking referential equality in equals instead of true value equality for some of the properties.
- Missing Override annotation on equals method and incorrect method signature (defining the parameter as Person instead of Object).
- Non-final class with instanceof check in equals doesn’t account for subclasses. Eg. person.equals(manager) must provide the same result as manager.equals(person).
- Logic mistakes are common when implementing hashCode especially if some of the properties can be null.
- Implementing equals without implementing hashCode (or vice versa).
- Inconsistent equals & hashCode implementation. Two instances that are equal must always produce the same hashCode.
- Poor hashCode implementation can cause many collisions and introduce scalability issues.
- Missing nullability annotations or forgetting to guard against null parameters
- Another property is added in the future and equals / hashCode / toString methods are forgotten or not updated correctly.
Switch Defect Categories
Kotlin replaced the switch statement with a more powerful “when”:
// Kotlin Color priorityColor = when (priority) { LOW -> Color.GREEN MEDIUM, HIGH -> Color.YELLOW CRITICAL -> Color.RED }
Whenever we use “when” as an expression (such as the above example), the compiler ensures that all scenarios are covered and prevents the following defect categories:
- Forgetting a case (eg. missing an enum value).
- Adding a new enum value and forgetting to update all the switch statements (especially if the enum is used by different teams).
- Missing break is a common defect which causes accidental fall through:
// Java switch (priority) { case LOW: priorityColor = Color.GREEN; // Oops, forgot break case MEDIUM: ... }
We can also use “when” as a replacement for if-else chains which makes them much easier to follow and and less error prone:
// Kotlin fun isHappy(person: Person): Boolean { return when { person.isPresident -> false person.isSmart -> person.age < 10 else -> person.salary > 100000 } }
Assignment Defect Categories
Unlike Java, an assignment is a statement in Kotlin (which does not evaluate to a value) so it cannot be used in a condition. This prevents the following defect categories:
- Accidental boolean assignment in condition:
// Java boolean isEmployed = loadEmploymentStatus(person); // Incorrect check due to assignment if (isEmployed = person.isMarried()) // Employed and married or unemployed and single ... if (isEmployed) // this variable was accidentally modified
- More complex conditions can accidentally assign variables of any type:
// Java // Attempt to determine twins based on age boolean singleSetOfTwins = ((age1 = age2) != (age3 = age4))
Override Defect Categories
Kotlin made “override” a mandatory keyword when overriding methods which prevents the following categories of defects:
- Accidentally override superclass method by adding a method to a subclass.
- Adding a method to a base class not realizing that it won’t be executed because a subclass has a method with the same signature.
- Missing the override annotation and changing a subclass method signature not realizing that it will no longer override a superclass method.
- Changing the method signature in a base class without realizing that a subclass overrides it which is missing the override annotation.
- Missing the override annotation and using incorrect spelling or capitalization (eg. hashcode instead of hashCode) so it’s not overriding the superclass method.
Some more thing for you : Top 12 Advanced Kotlin Tips For Pro Developers
Null Defect Categories
Null is by far the most common cause of defects in Java and masquerades itself in many forms.
- A common practice is to validate parameters at entry points and pass them to helper functions with an implied contract that they’ve already been verified. It’s also a best practice to replace a large function with a function that calls a bunch of smaller private ones. This allows each function to be easily understood and verified so the concept of entry points is very common. These implicit contracts are accidentally violated when adding a feature or fixing a defect. The entry points are often modified or variables are re-assigned by calling other functions which might return null. In the simplest case, this can cause a null pointer exception. Unfortunately, the unexpected null can also manifest itself through strange side-effects.
- Autoboxing / unboxing. Here’s a sneaky null pointer exception that’s waiting to happen when the user isn’t in the map:
// Java public boolean makesOverAMillionDollars(String name) { Map<String, Long> salaries = getSalaries(); long salary = salaries.get(name);// Unbox null to long causes NPE return salary > 1000000; }
- A null Boolean is often interpreted as false.
// Java // This check is accidentally circumvented by a null value if (Boolean.TRUE.equals(isSuspiciousAction)) reportToAuthorities();
- A null String is often interpreted as empty (eg. user didn’t enter any value) when we may not have extracted the value correctly.
- A null Integer is sometimes interpreted as 0 which causes surprises (eg. database ResultSet).
- Data can accidentally be cleared by null values. Even if we write code to populate a variable with a non-null value before storing it, these instructions are sometimes preempted by exceptions.
- Null is the cause of other types of exceptions as well. It’s a best practice to throw an IllegalArgumentException when parameters don’t conform to the contract (eg. passing a null identifier when creating a BankAccount).
Incorrect use of null is the cause of roughly 30% of all defects in Java.
Kotlin prevents these categories of defects with its stronger type system that has nullability built-in. You are forced to make a decision for what should happen whenever a variable might be null. Unlike other languages, this has zero memory or runtime overhead which avoids scalability concerns.
- Nullable variables are allowed to be set to null:
// Kotlin // Nullable types are declared with "?" var spouseName: String? = null // allowed
- Variables that are not declared as nullable cannot become null:
// Kotlin var name: String = "Dan" name = null // Compiler error, name is not nullable var spouseName: String? = getSpouseName() // null if not married name = spouseName // Compiler error, spouseName might be null
- Compiler prevents calling methods on variables that might be null:
// Kotlin val spouse: Person? = getSpouseOf("Bob") // null if not married spouse.speak() // Compiler error, spouse might be null
- You can use a nullable variable directly if the compiler can prove that it’s never null:
// Kotlin if (spouse != null) { spouse.speak() // Allowed since it will never be null here }
- Kotlin has several shortcuts which make it easier to work with nullable types. This reduces null check clutter so we can focus on business logic:
// Kotlin // Safe call operator "?." evaluates to null if the variable // is null without attempting to call a method on it spouse?.speak() // Elvis operator "?:" specifies default when left side is null val spouseSalary = spouse?.salary ?: 0 // If spouse is null then "spouse?.salary" evaluates to null so // default to 0 for the spouse salary
This is different from tools like FindBugs which try to spot null errors because Kotlin takes the opposite approach. Rather than trying to spot null errors, the compiler only passes if it can prove that a variable will never be null whenever it’s used as a non-null value.
Summary
Kotlin avoids many of the popular Java defect categories . Although I had high expectations, I was shocked to find so many improvements everywhere I looked. To my continued surprise, I kept discovering additional categories of defects that Kotlin prevents. The language design really starts to shine when subjected to this level of scrutiny and we’re just getting started.
Share your thoughts