- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Refactor Java Code to Idiomatic Kotlin
In this lab, you'll refactor a working JVM CLI application from verbose, Java-style Kotlin into concise, idiomatic Kotlin — replacing boilerplate patterns with data classes, primary constructors, Kotlin properties, and safe-call operators. You'll apply Kotlin's null safety, collection extensions, and expression-based control flow to simplify real code while keeping all existing tests green. By the end, you'll have the hands-on fluency to recognize and eliminate common Java migration anti-patterns in your own Kotlin codebases.
Lab Info
Table of Contents
-
Challenge
Step 1: Explore the codebase
Kotlin was designed to reduce Java boilerplate, but migrations don't always get there. Production codebases often land in an in-between state: valid Kotlin that still follows Java patterns, with verbose class declarations, manual null guards, and imperative loops all doing work that idiomatic Kotlin can express in far fewer lines. Knowing how to recognize and eliminate those patterns is the skill that turns a working port into genuinely idiomatic Kotlin.
In this lab, you'll work with a running JVM CLI inventory application written in that Java-style Kotlin. The application processes and reports on a list of inventory items. You'll refactor it step by step, converting verbose class declarations to data classes, replacing explicit getter and setter methods with Kotlin property access, applying null-safe operators in place of manual guards, and swapping imperative loops for collection extension functions.
To start, you'll run the application to observe its current output and establish a green test baseline you'll carry as a safety net through every step.
The
applicationdirectory already contains a runnable CLI application, a JUnit 5 behavioral test suite, and a Gradle Kotlin DSL build.info> Feeling stuck? Check out the matching
solution/stepN/folder for the step you're on to see a working implementation. Give it a try on your own first. The solution folder is your safety net, not your starting point.The Gradle run task
The Gradle
applicationplugin adds aruntask that compiles the project and launches the application's main function. Running it now gives you a concrete picture of the output the CLI produces. That output is the behavior you must preserve through every refactoring step that follows.warning> The lab environment on the right may take up to a minute to finish loading. Continue after the terminal and project files appear. ### The behavioral test suite as a safety net
The existing JUnit 5 behavioral test suite exercises the application's public interface across a range of inputs. Running it before touching any code confirms that all tests start green. A verified baseline means any failure you see in a later step was introduced by a change in that step, not by a problem that existed before you started. info> This lab experience was developed by the Pluralsight team using an internally developed AI tool. All sections were verified by human experts for accuracy prior to publication. However, content may still contain errors or inaccuracies, and we recommend independent verification.
To report a problem or provide feedback, click here. Feedback may be used to improve accuracy in accordance with our Privacy Policy. -
Challenge
Step 2: Refactor classes and constructors
With a green test baseline established, you can refactor with confidence. The model classes in this codebase are the most visible source of Java-style boilerplate: verbose constructors that manually assign parameters to backing fields, explicit
getandsetmethods for every property, and none of the structural equality,copy(), ortoString()behavior that Kotlin'sdata classgenerates automatically.In this step, you'll replace that ceremony with concise data class declarations and Kotlin property access, then confirm the behavioral tests still pass.
Data classes and primary constructors
A Kotlin
data classcondenses a verbose class declaration into a single line. Adding thedatakeyword and declaring all fields as primary constructor parameters causes the compiler to generateequals(),hashCode(),toString(), andcopy()automatically.Any class used primarily as a value holder with no custom lifecycle is a strong candidate for this conversion.
// Java-style class class Widget { private var label: String = "" constructor(label: String) { this.label = label } fun getLabel(): String = label } // Idiomatic Kotlin data class data class Widget(val label: String) ``` ### Kotlin property access When a class declares its fields in a primary constructor, those fields become Kotlin properties accessed directly by name. There is no need for explicit `getX()` or `setX()` method declarations. Call sites that previously used `item.getName()` become `item.name`, and `item.setName(value)` becomes `item.name = value` for mutable properties. ### Verifying structural alignment after the class refactoring The data class conversion changes how property access works across the codebase. Running the full behavioral suite after both Task 2.1 and Task 2.2 confirms that the model type and every file that references it are aligned correctly. -
Challenge
Step 3: Apply null safety patterns
The data classes and Kotlin properties are in place. The next layer of Java-style boilerplate is in the application logic: explicit
if (x != null)guards and.equals()method calls that Java's type system forced but Kotlin's null-safety types and operators make unnecessary. In data classes,==delegates to the generatedequals()implementation, making it the idiomatic replacement for any.equals()call sites.In this step, you'll replace null-check guards with safe-call (
?.) and Elvis (?:) operators, and swap.equals()calls for Kotlin's==operator, letting the compiler track nullability rather than doing it by hand.Safe-call and Elvis operators
Kotlin's safe-call operator (
?.) accesses a property or calls a method only when the receiver is non-null, producingnullotherwise. The Elvis operator (?:) provides a fallback value when the left side evaluates tonull.Together, they replace the common pattern of declaring a mutable variable, branching into two assignment blocks based on a null check, and arriving at the same result.
// Java-style null guard var result: String if (thing != null) { result = thing.label } else { result = "default" } // Idiomatic Kotlin val result = thing?.label ?: "default" ``` ### Structural equality in Kotlin Kotlin's `==` operator calls the `equals()` implementation of the type on the left. For data classes, that implementation is generated automatically from the primary constructor properties. Using `==` is the idiomatic replacement for any `.equals()` call site, and it reads more naturally than a method call. ### Confirming null safety preserves behavior Safe-call and Elvis operators preserve the same semantics as the explicit null checks they replace. Running the behavioral suite after both tasks confirms the operators produce the results the tests expect. -
Challenge
Step 4: Modernize collections and control flow
Null safety is now handled idiomatically, and the class and accessor boilerplate is gone. The remaining Java-style patterns are in how the service code processes its data: imperative
forloops that accumulate results into mutable lists, and anif/elsechain that assigns a value to avaracross multiple branches where Kotlin supports expressions.In this step, you'll replace the loop-based accumulation patterns with Kotlin collection extension functions, and convert the statement-style conditional assignment to a
whenexpression, producing code that reads linearly from top to bottom.Collection extension functions
Kotlin's standard library provides extension functions on collection types that replace common accumulation patterns.
filterselects elements matching a predicate,sumOfcomputes a numeric sum from each element,maxByOrNullfinds the element with the highest value for a key, andgroupBypartitions a collection into groups keyed by a property.Each of these replaces a corresponding
forloop that built a mutable result manually.// Java-style accumulation val result = mutableListOf<Widget>() for (w in widgets) { if (w.active) result.add(w) } // Idiomatic Kotlin val result = widgets.filter { it.active }### `when` as an expressionHint: how to use groupBy when you also need to format the groups
groupByreturns aMapfrom key to a list of matching elements. You can chainmapon that result to transform each entry into a formatted string, thenjoinToStringto combine them.val lines = collection.groupBy { it.property } .map { (key, members) -> "$key: ${members.size}" } .joinToString(separator)In Kotlin,
whencan be used as an expression whose result you assign directly to aval. This replaces the common pattern of declaring avar, then assigning it inside each branch of anif/elsechain.Using
whenas an expression also gives the compiler a complete view of all branches, which it uses to enforce that the result is always assigned.// Java-style conditional assignment to var var tier: String if (count > 100) { tier = "large" } else if (count > 10) { tier = "medium" } else { tier = "small" } // Idiomatic Kotlin: when expression assigned to val val tier = when { count > 100 -> "large" count > 10 -> "medium" else -> "small" } ``` ### Confirming the modernized service preserves output Collection extension functions and `when` expressions change how results are computed, not what those results are. Running the full behavioral suite after both tasks confirms that every computation still produces the expected output. -
Challenge
Step 5: Validate the refactor
The behavioral tests have passed after every refactoring step. At this point the codebase is structurally complete. The lab's validation checks will confirm that idiomatic Kotlin constructs were applied throughout.
To close the lab, you'll run the CLI one final time to verify the refactored application produces the same output it did when you started.
Confirming end-to-end behavior
Running the CLI against the fully refactored codebase is the observable confirmation that every change you made across the steps composes correctly. The output you see now should match the output you observed in Task 1.1. Well done. You refactored a working JVM CLI inventory application from Java-style Kotlin into idiomatic Kotlin, keeping the behavioral test suite green at every step.
You applied four categories of Kotlin idiom:
- Data classes with primary constructors and auto-generated structural equality
- Kotlin property access in place of explicit getter and setter methods
- Safe-call and Elvis operators that let the compiler enforce null safety in place of manual guards
- Collection extension functions and
whenexpressions that replace imperative accumulation patterns with readable, top-to-bottom code
The final
./gradlew runconfirmed that all of those changes composed correctly and the application produces the same output it did at the start. These patterns appear in every Kotlin codebase migrated from Java, and the ability to recognize and refactor them systematically is what turns a working port into idiomatic Kotlin.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.