How Kotlin remove Java defects?

1
136

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:

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.

1Equality 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.

Equality Defect Categories
Equality Defect Categories

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

2Data 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.

Data Class Defect Categories
Data Class Defect Categories

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.

3Switch 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:

Switch Defect Categories
Switch 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
    }
}

4Assignment 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:

Assignment Defect Categories
Assignment 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))

5Override Defect Categories

Kotlin made “override” a mandatory keyword when overriding methods which prevents the following categories of defects:

Override Defect Categories
Override Defect Categories
  • 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

6Null Defect Categories

Null is by far the most common cause of defects in Java and masquerades itself in many forms.

Null Defect Categories
Null Defect Categories
  • 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.

7Summary

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

Loading Facebook Comments ...
Loading Disqus Comments ...