From Java to Kotlin: A Complete Guide with Language Comparisons

Introduction

Kotlin is a modern programming language released in 2016 by JetBrains. It is designed to be concise, safe, and pragmatic, with a strong focus on interoperability with Java code.

At Google I/O 2017, Google announced that Android development would be increasingly "Kotlin-first." While Android development using Java is still supported, newer Android documentation from Google typically only provides Kotlin code examples. Kotlin offers several advantages over Java: applications compile faster, deploy with a lighter footprint, and require less code to accomplish the same tasks.

These benefits motivated my decision to switch to Kotlin. I'm documenting my findings to help other Java developers quickly learn Kotlin's basics through this cheat sheet.

Naming Conventions

TypeJavaKotlin
Functions, properties, and variablescamelCasecamelCase
ConstantsSCREAMING_SNAKE_CASESCREAMING_SNAKE_CASE
Classes, interfaces, factory functionsPascalCasePascalCase
Package nameslowercaselowercase or camelCase in the case of multiple words
Test methodscamelCasemethod names with spaces or method_names_with_underscore

The naming conventions in Java and Kotlin are very similar. For more detailed information about Kotlin coding conventions, see the official documentation.

Java-Kotlin Interoperability

Kotlin was designed with Java interoperability in mind. You can easily call Java methods from Kotlin and vice versa, as both languages compile to bytecode in the .class file format.

Code Differences

Let's compare a simple "Hello World" program in both languages:

Java:

java

package example; class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }

Kotlin:

kotlin

package example fun main(args: Array<String>) { println("Hello, World!") } // omitting args parameter fun main() { println("Hello World!") }

Key differences to note:

  • The main function in Kotlin can be defined at the top level of the file. You don't need to wrap everything inside a class as you do in Java.
  • In Kotlin, the program arguments parameter (args) is optional, unlike Java where it's always required.
  • Kotlin doesn't require semicolons at the end of each line.
  • Arrays in Kotlin don't have special syntax. They appear as regular classes with generic arguments (e.g., Array<String>), though they compile to the same bytecode as Java arrays.

Variables

Data Types

Kotlin data types are essentially capitalized versions of their Java counterparts. Here are the most commonly used data types:

JavaKotlin
StringString
booleanBoolean
intInt
doubleDouble
charChar

Declaring Variables

In Kotlin, mutable variables are declared using the var keyword. The compiler can infer variable types automatically, so type declarations are optional. Similar functionality was introduced in Java 10 with its var keyword, which automatically infers variable types based on their initial values.

Java:

java

// Creating variables without specifying the type var myNum = 5; // int var myDoubleNum = 5.99; // double var myLetter = 'D'; // char var myBoolean = true; // boolean var myText = "Hello"; // String // Creating variables with explicit type int myNum = 5; // int double myDoubleNum = 5.99; // double char myLetter = 'D'; // char boolean myBoolean = true; // boolean String myText = "Hello"; // String

Kotlin:

kotlin

// Creating variables without specifying the type var myNum = 5 // Int var myDoubleNum = 5.99 // Double var myLetter = 'D' // Char var myBoolean = true // Boolean var myText = "Hello" // String // Creating variables with explicit type var myNum: Int = 5 // Int var myDoubleNum: Double = 5.99 // Double var myLetter: Char = 'D' // Char var myBoolean: Boolean = true // Boolean var myText: String = "Hello" // String

Important notes about Kotlin variables:

Once a variable's type is inferred, it cannot be changed:

kotlin

var string = 1 string = "abc" // Type mismatch: inferred type is String but Int was expected

This code fails because you cannot assign a String value to a variable inferred as Int.

When using var without an explicit type declaration, the variable must be initialized immediately:

kotlin

// Incorrect - declaration without initialization var name name = "Kotlin" // Correct approaches var name: String // Type specified, can be initialized later name = "Kotlin" // or var name = "Kotlin" // Type inferred from initialization

Declaring Constants

In Kotlin, immutable (read-only) variables are declared using the val keyword, which corresponds to Java's final keyword. As with var, Kotlin will automatically infer the type based on the assigned value.

Java:

java

final double PI = 3.14;

Kotlin:

kotlin

val a: Int = 1 // immediate assignment val b = 2 // `Int` type is inferred val c: Int // Type required when no initializer is provided c = 3 // deferred assignment

Important: While val creates an immutable reference, the object it refers to may still be mutable. This behavior is identical to Java's final keyword. The immutable reference only prevents reassignment of the variable; it doesn't affect the mutability of the object it references.

Best Practice: Prefer val over var. It's good practice to declare variables as immutable (val) by default and only use var when the value truly needs to be modified.

Type Casting

Kotlin provides the as operator for type casting and the is operator for type checking. The is operator serves the same purpose as Java's instanceof operator.

Java:

java

if (any instanceof String) { String s1 = (String) any; String s2 = String.toUpperCase(s1); }

Kotlin:

kotlin

if (any is String) { val s1 = any as String val s2 = s1.toUpperCase() } // Can be simplified to if (any is String) { any.toUpperCase() }

Kotlin introduces "smart casting": after checking a variable's type with the is operator, the compiler automatically casts that variable to the checked type within the scope of the condition. This is why you can directly call String methods on the any variable after the is String check, making the code more concise and readable.

String Manipulation

String Templates

String templates are string literals that contain embedded expressions. They allow you to insert a value inside a string literal by using a dollar sign ($) followed by a variable name.

kotlin

val name = "Kotlin" println("Hello, $name!")

If you need to include a more complex expression, such as a function call or an arithmetic operation, you must surround the expression with curly braces.

kotlin

println("3 + 2 is ${3 + 2}") println("result: ${functionCall()}")

This feature is not available in Java, where inserting values into string literals can be cumbersome.

Multiline Strings

In Kotlin, you can use triple-quoted strings to include special characters without escaping them. This is particularly useful for writing multiline string literals or regular expressions.

kotlin

val regex = """\d{2}\.\d{2}\.\d{2}""" val multilineMessage = """ Hello World Hello Dad Hello Mom """

Functions

To create a function in Kotlin, use the fun keyword. Below is an example of a function with two Int parameters and an Int return type.

Java:

java

int sum(int a, int b) { return a + b; }

Kotlin:

kotlin

fun sum(a: Int, b: Int): Int { return a + b }

If your function consists of a single expression, like the sum() function above, you can use an alternative syntax called a function with an expression body. The expression that the function returns is placed after the = sign. With this syntax, you can omit the return type because the Kotlin compiler can automatically infer it.

kotlin

fun sum(a: Int, b: Int): Int = a + b fun sum(a: Int, b: Int) = a + b // omitting the return type

However, it is still recommended to explicitly specify the return type for your function, especially if you are writing functions for public APIs, libraries, or non-private functions in general.

Unit Functions

For void methods (methods that do not return any value), use the Unit return type. Note that the Unit return type can be omitted.

Java:

java

void printSum(int a, int b) { System.out.println("sum of " + a + " and " + b + " is " + (a + b)); }

Kotlin:

kotlin

// using the Unit return type to declare void methods fun printSum(a: Int, b: Int): Unit { println("sum of $a and $b is ${a + b}") } // without using the Unit return type fun printSum(a: Int, b: Int) { println("sum of $a and $b is ${a + b}") }

In Kotlin, you can define functions anywhere. You can define them at the top level, as members of a class, or even within another function (nested functions). In Java, methods can only be defined within classes.

kotlin

// top-level function fun topLevel() = 1 // member function class A { fun member() = 2 } // local function fun other() { fun local() = 3 }

Calling Kotlin Functions from Java

Now, let’s discuss Java-Kotlin interoperability. You can easily call a top-level Kotlin function as a static function from Java.

MyFile.kt:

kotlin

package intro fun foo() = 0

Method 1: Import the MyFile.kt file as a class.

UsingFoo.java:

java

package other; import intro.MyFileKt; public class UsingFoo { public static void main(String[] args) { int i = MyFileKt.foo(); } }

In this method, you can use the @JvmName annotation to change the JVM name of the class containing top-level functions.

MyFile.kt:

kotlin

@file:JvmName("Util") // using JvmName annotation package intro fun foo() = 0

UsingFoo.java:

java

package other; import intro.Util; // now you use "Util" to call the top-level functions public class UsingFoo { public static void main(String[] args) { int i = Util.foo(); } }

Method 2: Use import static.

MyFile.kt:

kotlin

package intro fun foo() = 0

UsingFoo.java:

java

package other; import static intro.MyFileKt.*; public class UsingFoo { public static void main(String[] args) { foo(); } }

Default Arguments

In Kotlin, function parameters can have default values. A default value is defined using = after the type. This feature is not supported in Java.

kotlin

fun powerOf(num: Int, exponent: Int = 2) = num.toDouble().pow(exponent)

To achieve the same behavior in Java, you would need to use overloaded functions.

java

double powerOf(int num, int exponent) { return Math.pow(num, exponent); } // use overloaded method to handle default arguments double powerOf(int num) { return Math.pow(num, 2); }

Now, an interesting question arises: How do you call a Kotlin function with default arguments from Java?

Method 1: Manually Provide Values for All Arguments

This approach works because you explicitly provide values for all arguments, bypassing the need for default values.

MyFile.kt:

kotlin

fun sum(a: Int = 0, b: Int = 0, c: Int = 0)

UseSum.java:

java

// providing values for all arguments sum(1, 2, 3);

Method 2: Use the @JvmOverloads Annotation

You can add the @JvmOverloads annotation to your Kotlin function to simplify calling it from Java.

MyFile.kt:

kotlin

@JvmOverloads fun sum(a: Int = 0, b: Int = 0, c: Int = 0)

UseSum.java:

java

// default values are used sum(1);

Under the hood, the @JvmOverloads annotation generates multiple overloaded functions for the Kotlin sum() method. These correspond to the following method headers:

java

public static final int sum(int a, int b, int c) public static final int sum(int a, int b) public static final int sum(int a) public static final int sum()

Named Arguments

Named arguments free you from having to remember the order of parameters in a method call. Each argument can be specified by its corresponding parameter name. This is a feature of Kotlin and is not supported in Java.

kotlin

fun powerOf(num: Int, exponent: Int = 2) { /* ... */ } fun main() { powerOf(exponent = 3, num = 2) // using named arguments to pass arguments }

Extension Functions

Extension functions allow you to extend a class with new functionality. They are defined outside the class but can be called as if they were regular member functions of the class. This feature is not available in Java.

kotlin

// The following is an extension function that extends the String class, // acting as a member function of the String class. // In this case, the String class is referred to as the **Receiver,** // and the receiver can be accessed via the **this** keyword. fun String.lastChar() = this.get(this.length - 1) // The **this** keyword can be omitted fun String.lastChar() = get(length - 1) // Example of using the extension function val c: Char = "abc".lastChar()

Important Notes About Extension Functions:

  1. Import Requirement: Extension functions must be imported before they can be used.
  2. No Access to Private Members: Extension functions do not have access to private members of the receiver class.
  3. Static Under the Hood: Since extension functions are compiled into static Java methods, they cannot be overridden.
  4. Member Precedence: Extensions do not hide original members of the receiver class. If an extension function has the same signature as a member function, the member function takes precedence. Members always win.
  5. Overloading Members: Extensions can be used to overload member functions.

Infix Extension Functions

Extension functions can be defined as infix, allowing you to call them in an infix form by omitting the dot and parentheses. A common example is the to() function, which is widely used to create pairs.

kotlin

infix fun <A, B> A.to(that: B) = Pair(this, that)

As a result, you can choose to omit the dot and parentheses when using the to() function.

kotlin

// traditional way "Apple".to("Fruit") // infix way "Apple" to "Fruit"

Calling Kotlin Extension Functions from Java

Under the hood, Kotlin extension functions are compiled into static methods. Consider the following example:

StringUtil.kt:

kotlin

fun String.repeat(n: Int): String { val sb = StringBuilder(n * length) for (i in 1..n) { sb.append(this) } return sb.toString() }

To use this extension function in Java:

java

StringUtilKt.repeat("ab", 3);

Note that the receiver (the String in this case) is passed as the first parameter when calling extension functions from Java.

Conditions

If Expression

In Java, if is a statement. However, in Kotlin, if is an expression, meaning it returns a value. As a result, Kotlin does not need a ternary operator because the ordinary if expression serves this purpose.

Java:

java

int max = (a > b) ? a : b;

Kotlin:

kotlin

val max = if (a > b) a else b

Note that when using if as an expression, the else branch is mandatory.

You can also use the Kotlin if expression as a "statement," where the return value (of type Unit) is ignored.

kotlin

// use expression as a "statement" val max: Int if (a > b) { max = a } else { max = b }

Branches of an if expression can be blocks. In this case, the last expression in the block is the value returned.

kotlin

val max = if (a > b) { print("a is max") a // return a as the value of this block } else { print("b is max") b // return b as the value of this block }

When Expression

In Kotlin, when is often compared to switch in Java or other C-like languages. However, when offers significantly more functionality than the traditional switch.

Usage 1: Using when as switch

Java:

java

public enum Color { RED, BLUE, GREEN } public static void printColorName(Color color) { switch(color) { case BLUE -> System.out.println("Blue color"); case RED -> System.out.println("Red color"); case GREEN -> System.out.println("Green color"); default -> System.out.println("Unexpected value: " + color); } }

Kotlin:

kotlin

enum class Color { RED, BLUE, GREEN } fun printColorName(color: Color) { when(color) { Color.BLUE -> println("Blue color") Color.RED -> println("Red color") Color.GREEN -> println("Green color") else -> println("Unexpected value: $color") } }

In Kotlin, you can use expressions as branch conditions. This is particularly interesting because, in Java, switch only supports constants of types like byte, short, char, int, String, and enumerated types.

kotlin

fun mix(c1: Color, c2: Color) = when(setOf(c1, c2)) { setOf(RED, YELLOW) -> ORANGE setOf(YELLOW, BLUE) -> GREEN setOf(BLUE, VIOLET) -> INDIGO else -> throw Exception("Dirty color") }

Usage 2: Check Multiple Values

You can list multiple values separated by commas to check if the when argument matches any of them. This is not possible with switch in Java.

kotlin

fun respondToInput(input: String) = when(input) { "y", "yes" -> "I'm glad you agree" "n", "no" -> "Sorry to hear that" else -> "I don't understand you" }

Usage 3: Check Types

Suppose you have the following type hierarchy:

Type Hierarchy

If you have a variable of type Pet, you might need to check whether it is of subtype Cat or Dog. In Java, you would use if statements with the instanceof keyword.

java

if (pet instanceof Cat) { ((Cat) pet).meow(); } else if (pet instanceof Dog) { ((Dog) pet).woof(); }

In Kotlin, you can perform type checking with when.

kotlin

when(pet) { is Cat -> pet.meow() is Dog -> pet.woof() }

Note that in Kotlin:

  • The is keyword is equivalent to instanceof in Java.
  • You do not need to explicitly cast the variable after checking its type. It is automatically smart-cast to the correct type.

Usage 4: Alternative to if Statements

You can use when as an alternative to if expressions by omitting the when argument and using Boolean expressions as branch conditions.

kotlin

val coldness = when { degrees < 5 -> "freezing" degrees < 15 -> "cold" degrees < 30 -> "normal" else -> "hot" }

Tip: Capturing when Subject in a Variable

Starting from Kotlin 1.3, you can capture the when subject in a variable by introducing a new variable inside the parentheses using the val keyword.

kotlin

// prior to Kotlin 1.3 val pet = getMyFavouritePet() when(pet) { is Cat -> pet.meow() is Dog -> pet.woof() } // starting Kotlin 1.3 when(val pet = getMyFavouritePet()) { is Cat -> pet.meow() is Dog -> pet.woof() }

Loops

For Loops

Iterating Over a Range of Numbers

Java:

java

for (int i = 0; i < 10; i++) { System.out.print(i); // outputs 0123456789 }

Kotlin:

kotlin

for (i in 0..9) { print(i) // outputs 0123456789 } for (i in 0 until 9) { print(i) // outputs 012345678 } for (i in 8 downTo 1) { print(i) // outputs 87654321 } for (i in 8 downTo 1 step 2) { print(i) // outputs 8642 }

Notes:

  • You can omit the type in the for loop’s parentheses.
  • 1..9 includes the upper bound 9, whereas 1 until 9 excludes the upper bound 9.
  • downTo is equivalent to the .. operator but in reverse order.
  • You can use the step keyword to specify the step size for each iteration.

Iterating Over Each Character in a String

Java:

java

for (char ch : "abc".toCharArray()) { System.out.print(ch + 1); }

Kotlin:

kotlin

for (ch in "abc") { print(ch + 1) // output bcd }

Iterating Over a Map

Kotlin:

kotlin

val map = mapOf(1 to "one", 2 to "two", 3 to "three") for ((key, value) in map) { println("$key => $value") }

Iterating Over a Collection with Index

Kotlin:

kotlin

val list = listOf("a", "b", "c") for ((index, element) in list.withIndex()) { println("$index: $element") }

The in Operator

In Kotlin, the in operator has multiple use cases. The first, as shown earlier, is for iterating over ranges in for loops. Secondly, the in operator can be used to evaluate whether a value belongs to a range or collection. This is a convenient feature with no direct equivalent in Java.

Usage 1: Checking for Belonging in a Range

kotlin

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z' fun main() { println(isLetter('q')) // true println(isLetter('*')) // false }

In this example, the in operator checks whether a character belongs to the range 'a'..'z' or 'A'..'Z'. Note that c in 'a'..'z' is equivalent to 'a' <= c && c <= 'z' in Java, as both are Boolean expressions.

You can also use the NOT operator (!) to check for non-belonging.

kotlin

fun isNotDigit(c: Char) = c !in '0'..'9' fun main() { println(isNotDigit('x')) // true println(isNotDigit('5')) // false }

Usage 2: Checking for Belonging in a Range of Strings

The in operator can also be used to check for belonging in a range of strings.

kotlin

// Since 13 in 0..20 // is equivalent to 0 <= 13 && 13 <= 20 // Thus "ball" in "a".."k" // is equivalent to "a" <= "ball" && "ball" <= "k" // or "a".compareTo("ball") <= 0 && "ball".compareTo("k") <= 0 // where the strings are compared in alphabetical order

This approach works for any objects that implement the Comparable interface.

Usage 3: Checking for Belonging in a Collection

The in operator can also be used to check if an element belongs to a collection.

kotlin

if (element in list) {...} // which is equivalent to if (list.contains(element)) {...}

Nullability

Fun fact: the Null Pointer Exception (NPE) is often referred to as the "billion-dollar mistake," a term coined by its inventor, Tony Hoare. This is because NPEs are notoriously difficult to detect during development, as they don’t cause errors at compile time, making them a significant threat in production.

Kotlin addresses this issue by being a null-safe language. Its type system is designed to eliminate the danger of null references, which is one of the reasons Kotlin has gained such popularity. Let’s explore some of the features that make Kotlin null-safe.

Nullable and Non-Nullable Types

Kotlin distinguishes between nullable types and non-nullable types. If you declare a variable with a regular type, such as String, it can only hold non-null values. Assigning null to such a variable will result in a compile-time error. To store a null reference, you must explicitly declare the type as nullable by appending a ? to the type.

kotlin

val name: String = "Ali" // OK // compile-time error val name: String = null // use nullable type to store a null reference val name: String? = null

When you try to dereference a variable with a nullable type, Kotlin will enforce checks at compile time, helping to prevent NPEs from occurring.

Under the hood, nullable and non-nullable types are implemented using the @Nullable and @NotNull annotations. As a result, there is no performance overhead.

Dereferencing Nullable Types

Kotlin provides several ways to safely dereference variables with nullable types. For the following examples, let’s assume you have a nullable String variable, s:

kotlin

val s: String?

1. Using Traditional if-else

You can use traditional conditional checks to ensure safe dereferencing:

kotlin

// using traditional conditional checking to check for null value if (s != null) { print(s.length) }

2. Using Safe Access with ?. Syntax

The safe access operator (?.) allows you to safely dereference a nullable variable. If the variable is null, the result of the expression will be null. Otherwise, the original value will be used.

kotlin

// using safe access to dereference nullable types safely: // if s is null, null will be the value of s?.length // if s is not null, the original value of s will be used print(s?.length) // the above is equivalent to: print(if (s != null) s.length else null)

3. Using the Elvis Operator with ?: Syntax

The Elvis operator (?:) provides a fallback value if the expression on the left is null. If the expression is not null, the original value is used.

kotlin

// using elvis operator to dereference nullable types safely: // if s is null, "s is null" will be printed // if s is not null, the original value of s will be printed print(s ?: "s is null") // the above is equivalent to: print(if (s != null) s else "s is null")

Notes:

  • The Elvis operator has a very low precedence level, even lower than + or -. Therefore, always use parentheses when necessary.
  • If you explicitly check for a null reference and throw an exception or return when the expression is null, Kotlin will automatically smart-cast the variable from a nullable type to a non-nullable type. This means you can safely access the variable in subsequent code without using the safe access or Elvis operator.

kotlin

// explicitly check for null value if (s == null) return // s is smart-cast to a non-nullable type // thus, no need to use safe access/elvis operator // the below code can be compiled without error print(s.length)

Not-Null Assertion

You can assert that a variable with a nullable type is not null by using the !! syntax. After the not-null assertion, the variable will be smart-cast to a non-nullable type.

kotlin

fun main() { // use not-null assertion to get rid of the compile-time error s!! print(s.length) }

However, this practice is strongly discouraged. You should use the not-null assertion with caution, as it can expose your program to NPEs in production.

Safe Cast

In a previous section, we discussed how you can use the as operator for type casting. However, if the cast fails, a ClassCastException is thrown. Kotlin provides a safe cast operator (as?) that returns null instead of throwing an exception if the cast fails.

kotlin

// using normal type cast val num: Int = 12 val s: String = num as String // throws ClassCastException // using safe cast val num: Int = 12 val s: String? = num as? String // returns null if the cast fails

Note that since null is returned if the cast fails, you must use a nullable type (e.g., String?) when using the safe cast operator.

Functional Programming

Lambdas

A lambda is an anonymous function that can be used as an expression. It allows you to pass functions as arguments to other functions. Lambdas are often used to replace the need for anonymous classes, which were commonly used in older versions of Java for the same purpose.

Lambda Syntax

In Kotlin, lambdas are always enclosed in curly braces. To create a lambda, you specify the parameters, followed by an arrow (->), and then the lambda’s body. Here’s an example of a lambda:

kotlin

{ x: Int, y: Int -> x + y }

Passing Lambdas as Arguments

To pass a lambda expression as an argument to a function, you can place the entire lambda inside the parentheses:

kotlin

list.any({ i: Int -> i > 0 })

If the lambda is the last argument of the function, you can move it outside the parentheses:

kotlin

list.any() { i: Int -> i > 0 }

If the parentheses are empty and the lambda is the only argument, you can omit the parentheses entirely:

kotlin

list.any { i: Int -> i > 0 }

Type Inference

If the type of the parameter is clear from the context, you can omit the type declaration. The Kotlin compiler can infer the type automatically:

kotlin

list.any { i -> i > 0 }

Single Parameter Simplification

If the lambda has only one parameter, you can omit the parameter declaration entirely. In this case, the default name it is assigned to the parameter:

kotlin

list.any { it > 0 }

Multiline Lambdas

You can include multiple lines of logic within a lambda’s body. The last expression in the lambda will be the result of the lambda expression:

kotlin

list.any { println("processing $it") it > 0 }

Destructuring in Lambdas

If your lambda takes a pair of values as an argument (e.g., a map entry), you can use destructuring syntax to access each value separately:

kotlin

// without destructuring declaration map.mapValues { entry -> "${entry.key} -> ${entry.value}" } // with destructuring declaration map.mapValues { (key, value) -> "$key -> $value" }

Ignoring Unused Parameters

If a parameter is unused, you can omit its name by using an underscore (_). This improves readability and avoids the need to invent names for unused parameters:

kotlin

map.mapValues { (_, value) -> "$value" }

Conclusion

Kotlin is a modern, expressive, and concise programming language that offers significant advantages over Java, especially for developers looking to write safer, more readable, and more maintainable code. From null safety and extension functions to lambdas and functional programming, Kotlin provides powerful features that streamline development and reduce boilerplate code.

For Java developers, transitioning to Kotlin is relatively smooth, thanks to its seamless interoperability with Java. Whether you're working on Android development, server-side applications, or any other domain, Kotlin's rich feature set and developer-friendly syntax make it a compelling choice.

If you're a Java developer, now is the perfect time to dive into Kotlin. Start by experimenting with its features, integrating it into your existing projects, and exploring how it can enhance your productivity and code quality. The more you use Kotlin, the more you'll appreciate its elegance and efficiency.

Happy coding, and welcome to the world of Kotlin!