You all know those awesome colleagues who don't let go - after your team fixes a bug, they go the extra mile and make changes that no team member will fall into that trap again. They prevent future developers from making the same mistakes!
Well, Kotlin and IntelliJ are doing exactly that - for millions of developers worldwide! These are just a few examples of how Kotlin prevents bugs before you even make them.
No more switch/case
If you start with Java (or C or C++), you'll make this mistake a few times before learning how to avoid it. Let's see some Java code:
static void printOperatingSystem(OperatingSystem operatingSystem) {
switch (operatingSystem) {
case windows:
System.out.println("Windows");
case mac:
System.out.println("Mac");
case linux:
System.out.println("Linux");
}
}
If you call printOperatingSystem(OperatingSystem.linux)
, this will print Linux
. But if you call it with printOperatingSystem(OperatingSystem.windows)
, this will actually print:
Windows
Mac
Linux
That's because we need to put a break
at the end of each case to not fall into the next one. For many developers, this might seem like a simple mistake - because once you learn about it, you learn to avoid it, and it goes into your muscle memory.
But what if the language would not let you do these mistakes in the first place? Let's see how you would write this in Kotlin:
fun printOperatingSystem(operatingSystem: OperatingSystem) {
when (operatingSystem) {
OperatingSystem.windows -> println("Windows")
OperatingSystem.mac -> println("Mac")
OperatingSystem.linux -> println("Linux")
}
}
No more break
to forget, and it's easier to read as well.
Equality instead of instance check
In Java, if you use ==
, this will actually check the instance of an object - not the content. What's really weird is that when you try this out with a simple check like in the third line of the next example, this will actually 'work' - because Java will optimize the Strings to exist only once in memory:
String one = "1234567";
String two = "1234567";
System.out.println("one == two: " + (one == two)); // prints true
String three = two.replace('7', '8').replace('8', '7'); // that's still the same
System.out.println("two == three: " + (two == three)); // prints false
Modern IDE tools thankfully warn you about this by giving you a message String values are compared using '==', not 'equals()' and an auto-fix, but it would be nice if the language itself would choose the most-used equality operator as the default. Kotlin does:
val one = "1234567"
val two = "1234567"
println("one == two: " + (one == two)) // prints true
val three = two.replace('7', '8').replace('8', '7')
println("two == three: " + (two == three)) // true, as we would expect :-)
NullPointerExceptions
This is old news for Kotlin devs, but still: every time you avoid nullable types in your codebase prevents these bugs from happening. Every time you hit the .
on a variable or a long expression and Kotlin tells you: "Wait a second, actually that could be null!" is another bug being prevented.
This is a win-win-win: Users love stable applications, developers love if they don't have to go on a multi-hour bug hunt how that null
value actually made it into the database, and product owners love it as well when developers can use their time to implement new features (and not go on multi-hour bug-hunts instead).
Here's some Java code:
String city = user.getAddress().getCity();
But actually, the user does not have an address, so getAddress()
can return null. If you didn't develop the best practice to use @NonNull
and @Nullable
annotations, these bugs are going to happen. And they do - all the time. Tony Hoare did call the null reference a billion dollar mistake for a reason.
Kotlin will save you here:
The beauty about these prevented errors is how Kotlin and the IDE do this. They prevent you from making these mistakes while you are typing. And usually, it feels so nice when they offer you an auto-fix directly at your fingertips.
Coroutines
I have to say I made this mistake in Java quite more times than I thought I would. Let's see some Java code:
new Thread() {
@Override
public void run() {
// do something cool here
}
}.run();
Do you see the mistake? Of course, I called run()
instead of calling start()
. So the code will just do the thing you defined in run
- but it will not do that on the thread, like you expected it to. Today, IntelliJ and other IDEs give you a warning here - but wouldn't it be nice if you didn't need to learn the difference in the first place? What if the tooling would prevent you from making this mistake?
With Kotlin Coroutines, the IDE will write an error directly when you try to call a suspending function from your regular, non-suspending code:
EDIT: I want to highlight a good comment by AndroidDeveloperLB: Of course Coroutines are something very different than Java Threads. He mentioned that there is a nice thread() utility method in Kotlin's stdlib. With that, you can write the code above correctly like this (and automatically starting the thread as well):
thread {
// do something cool here
}
I think this is another great example how Kotlin makes existing APIs easier to handle - with less code and less possible bugs! β€οΈ
Code you don't have to write/generate is code without bugs (usually)
When we write code, we will make mistakes. That's even true for Kotlin code. π
But it's nice if we don't have to wrote some code (mostly boilerplate) in the first place. More often than you'd like, you'll find projects with code like the following (even if tools like Lombok exist, some projects don't use them).
Of course, this is a really bad legacy project, so let's say there are no unit tests either.
I added an error in the Java code β let's see if you can spot it!
class User {
private final String firstName;
private final String lastName;
private final Address address;
private final PhoneNumber phoneNumber;
User(String lastName, String firstName, Address address, PhoneNumber phoneNumber) {
this.firstName = firstName;
this.lastName = lastName;
this.address = address;
this.phoneNumber = phoneNumber;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return firstName;
}
public Address getAddress() {
return address;
}
public PhoneNumber getPhoneNumber() {
return phoneNumber;
}
@Override
public boolean equals(Object o) {
if (this == o) return false;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(firstName, user.firstName) &&
Objects.equals(address, user.address) &&
Objects.equals(phoneNumber, user.phoneNumber);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName, address, phoneNumber);
}
@Override
public String toString() {
return "User{" +
"firstName='" + lastName + '\'' +
", lastName='" + firstName + '\'' +
", address=" + address +
", phoneNumber=" + phoneNumber +
'}';
}
}
Did you find it? There, in the equals
method, the first line should actually return true
in the instance check. Because right now, it will wrongfully return false for the same instance. Wait, you found a different error? That's because I actually tricked you a little. There are actually two errors here! There, in the getLastName
method, I wrote return firstName
.
Oh wait, there are more: I exchanged firstName
and lastName
in the constructor as well. That's not an error in itself, but I'll guarantee you that some colleagues will have a small look at the class, see the members, and instantly conclude that firstName
is the first parameter of the constructor. And they'll wish me to the ground of the deepest sea when they find out what I've done. π
Oh, and in the toString()
method, this will actually print the last name as the first and vice-versa. Good luck finding that bug.
And did I mention that I removed the lastName
from the equals
method because that felt like a good idea to fix an entirely unrelated function, that now depends on this mistake to be there? These things happen all the time. At least I kept the hashCode()
function correctly. But with a developer like me, you can never know.
All in all, there are 5 errors in this code! And that with only 4 members. And in big projects, you will only find out about these bugs later on when some user reports a weird behavior. And then the bug-hunt begins.
With 20 or more members, it becomes extremely common to forget to re-generate this code, or manually add the appropriate lines in the specific functions. Sometimes, some of the necessary changes get lost due to someone not resolving a Git merge conflict properly. π±
Long story short: not having to write this code in the first place is a blessing! In Kotlin:
data class User(
val firstName: String,
val lastName: String,
val address: Address,
val phoneNumber: PhoneNumber
)
There. Constructor, equals
, hashCode
, toString
, getters, setters, copy
function, @NonNull
annotations - all done for you under the hood. Not giving someone like me the opportunity to mess this up. π
Sealed classes with when
In the Java switch/case
and if/else
world, it's easy to forget to handle a particular case. Let's say we have this Java code:
CreateUserResult result = createUser(credentials);
if (result instanceof Success) {
User user = ((Success) result).getUser();
System.out.println("User " + user.username + " created");
} else if (result instanceof OtherError) {
String message = ((OtherError) result).getMessage();
System.out.println("Other error occurred: " + message);
}
This creates a user, prints it out on success, and prints the error if it couldn't be created. The type CreateUserResult
is actually a Kotlin sealed class, and one developer added another case that is not handled yet by the Java code - but the IDE does not show any errors. The type is defined like so:
sealed class CreateUserResult {
class Success(val user: User) : CreateUserResult()
class OtherError(val message: String) : CreateUserResult()
class UserDoesAlreadyExist(val username: String) : CreateUserResult()
}
So there actually is another type, called UserDoesAlreadyExist
, which handles the special case that the username is taken already. Let's say the Java code above was written with a Kotlin when
statement, the developer who added UserDoesAlreadyExist
would have gotten an error instantly and could have fixed it directly:
val result = createUser(credentials)
val message = when (result) {
is Success -> "User ${result.user.username} created"
is OtherError -> "Other error occurred: ${result.message}"
}
println(message)
The when
is used as an expression here, and that way Kotlin says it must be exhaustive. So this will show up in your IDE:
Easy enough, the IDE can add the remaining branches automatically for you and it's easy to add the remaining code:
val message = when (result) {
is Success -> "User ${result.user.username} created"
is OtherError -> "Other error occurred: ${result.message}"
is UserDoesAlreadyExist -> "Error: user ${result.username} does already exist"
}
Another possible bug, prevented before it could even ship to production!
Final thoughts
These are only a few examples how Kotlin prevents bugs from happening. But actually, Kotlin is full of these. If you use Kotlin: what are your favorite examples of Kotlin preventing bugs before you could even write them? I'd be happy to read your comments!
If you didn't start using Kotlin yet, I hope you got a small taste of how it could make your developer life better! Because not adding bugs πππ in the first place is much better than having to spend days or weeks in hunting them down.
Have an awesome Kotlin! π
If you liked this post, please give it a β€οΈ, click the +FOLLOW
button below and follow me on Twitter! This will help me stay motivated to create many more of these posts! π
Cover photo by Louis Tsai on Unsplash.
Top comments (4)
About the Thread.start issue, I think you went too far. Kotlin coroutines are very different from Thread, and I'm not sure they are as much part of the language anyway...
If you want something more similar, it should have been.... "thread" .
You use it this way:
That's it.
You're right, Kotlin Coroutines are very different from Java Threads. For example, one can write a Coroutine that runs on the same main thread as the regular code (and doesn't use any other threads at all).
My point here was: Coroutines are quite becoming the default to do concurrency operations in Kotlin (especially since Java threads are not available on all platforms), and they fix things that have not been addressed before. Like the aforementioned example of calling a suspend function, but forgetting to handle it correctly.
I think your example is a really good one to show again how Kotlin makes existing Java-APIs easier to handle, and code easier to write, and fewer possible bugs. π
I'll link to your comment in the article! π
I kinda agree. I too resisted the switch to kotlin at first but I had to being an Android developer. And now I love it. The main thing is, this language makes me think harder, especially the null safety. Because it immediately tells me, I am forced to make changes to all the places right to the source, fixing where I can receive null where I should not. So now, my app is far more stable. In cases where sometimes backend fails to give the result, my app is able to handle it beautifully because of kotlin. Some of the bugs you mentioned, I have made countless of them in my entire Java development career.
Nice roundup! I recently looked into Apache Pulsar and look forward to tinkering with their Java API from within the safety of Kotlin's runtime π§ β π