Java-aware beginners can usually read braces and types, but Kotlin may introduce String?, primary constructors, expression bodies and collection pipelines too quickly. An Android-free console project stores four study sessions and produces exact, checkable output. Kotlin fundamentals come before framework choices; broader routes are available under Coding & Skills.
Read the smallest Kotlin program before adding abstractions
Start with one Kotlin console file:
fun main() {
val language = "Kotlin"
val message = if (language.length == 6) "ready" else "check"
println("$language: $message")
}The output is Kotlin: ready. The keyword fun declares a function. Kotlin infers that language is a String, while val makes its reference read-only. $language and $message are string templates. Here, if is an expression because it produces the value assigned to message. Semicolons are not mandatory, and object construction later will not need new.
Run the file in a current local Kotlin/JVM project or the official Kotlin Playground. The project models a StudySession, formats it with a function, filters longer sessions through a list pipeline, and totals minutes with an extension.
Model one session with a data class and a nullable property
data class StudySession(val topic: String, val minutes: Int, val note: String?)Kotlin's official null-safety documentation distinguishes String, which cannot hold null, from String?, which can. The primary constructor defines all three properties, and each is a val because a study session is an immutable value.
The official data-class documentation explains that primary-constructor properties participate in generated members including toString(), equals() and hashCode(), component functions and copy():
val original = StudySession("Functions", 25, "named arguments")
val revised = original.copy(minutes = 30, note = null)
println(original)
println(revised)The exact values are:
StudySession(topic=Functions, minutes=25, note=named arguments)
StudySession(topic=Functions, minutes=30, note=null)copy created a revised value without mutating original. Kotlin's compact form still belongs in the wider class-and-object model explained in Object Oriented Technology Explained. It does not remove the need to understand encapsulation or object identity.
Make the nullable path explicit with safe calls and Elvis
fun StudySession.label(prefix: String = "Study"): String =
"$prefix: $topic - $minutes min - ${note?.uppercase() ?: "NO NOTE"}"For StudySession("Null safety", 35, "redo Elvis operator"), note?.uppercase() produces REDO ELVIS OPERATOR. The fallback is skipped, so label(prefix = "Today") returns Today: Null safety - 35 min - REDO ELVIS OPERATOR.
For StudySession("Data classes", 50, null), the safe call produces null. The Elvis operator ?: selects NO NOTE, so label() returns Study: Data classes - 50 min - NO NOTE. These safe-call and fallback rules follow the official null-safety documentation.
The expression deliberately avoids note!!.uppercase(). Applying !! to null can produce a NullPointerException. Java-aware readers can compare related string concepts in String Handling in Java: Pool, Immutability, Interview Qs, but Java string behaviour is not evidence for Kotlin's nullable type rules.

Use functions to make intent visible
In StudySession.label, StudySession is the receiver type, label is the function name, and prefix = "Study" is a default parameter. String declares the return type. The expression after = is the entire function body. Kotlin's official functions documentation defines default parameters, named arguments and single-expression functions in these terms.
val first = StudySession("Null safety", 35, "redo Elvis operator")
val second = StudySession("Data classes", 50, null)
first.label(prefix = "Today")
second.label()
fun validMinutes(minutes: Int): Boolean = minutes > 0The first call uses a named argument to replace the default, while the second keeps Study. Function arguments are read-only inside the function. validMinutes(35) is true, and validMinutes(0) is false. Keeping validation separate from formatting gives each function one visible job.
Complete the console project with collections and an extension
Use this program:
data class StudySession(val topic: String, val minutes: Int, val note: String?)
fun StudySession.label(prefix: String = "Study"): String =
"$prefix: $topic - $minutes min - ${note?.uppercase() ?: "NO NOTE"}"
fun validMinutes(minutes: Int): Boolean = minutes > 0
fun List<StudySession>.totalMinutes(): Int = sumOf { it.minutes }
fun main() {
val sessions = listOf(
StudySession("Null safety", 35, "redo Elvis operator"),
StudySession("Data classes", 50, null),
StudySession("Functions", 25, "named arguments"),
StudySession("Collections", 40, null)
)
val total = sessions.totalMinutes()
val longTopics = sessions.filter { it.minutes >= 40 }.map { it.topic }
println(sessions[0].label(prefix = "Today"))
println(sessions[1].label())
println("Total: $total min")
println("Long sessions: $longTopics")
val pace = if (total >= 120) "on track" else "add a session"
println("Plan: $pace")
}Its five output lines are:
Today: Null safety - 35 min - REDO ELVIS OPERATOR
Study: Data classes - 50 min - NO NOTE
Total: 150 min
Long sessions: [Data classes, Collections]
Plan: on trackThe read-only list keeps its construction order. The extension wraps sumOf, so 35 + 50 + 25 + 40 = 150. The filter keeps only 50 and 40, then map extracts their topics in order. Since 150 >= 120, the plan is on track. These operations follow Kotlin's official collection and extension documentation.
![Data flow: the minutes >= 40 filter yields [Data classes, Collections] and totalMinutes sums to 150, so the plan reads on track.](https://cdn.knowledgegate.ai/blog-assets/blog_asset_1784652142610_0jb137.jpg)
Change one value and predict the result before running
Replace only the Functions entry with StudySession("Functions", 45, null). Write your prediction before running it. The total rises from 150 to 170, because 20 minutes were added. Long sessions becomes [Data classes, Functions, Collections], and the plan remains on track. The first line stays Today: Null safety - 35 min - REDO ELVIS OPERATOR. The unchanged second session still prints Study: Data classes - 50 min - NO NOTE.
Now isolate the nullable-number idea:
val maybeMinutes: Int? = null
val safeMinutes = maybeMinutes ?: 0safeMinutes is exactly 0. Change maybeMinutes to 45, and predict 45. This confirms that Elvis works with nullable numbers as well as strings. For more trace-and-predict practice, reproduce small programs across languages, but preserve each language's own type and null rules.
Common traps and how assessments probe them
Keep these distinctions precise:
StringandString?are different types.?.propagatesnull, while?:supplies a fallback.!!can move a null failure to runtime.A data-class
copy()creates a new instance, but the copy is shallow when properties refer to mutable objects.A
valprevents reference reassignment. It does not make the contents of a mutable collection immutable.mapreturns a transformed list rather than editingsessions.
Extensions allow member-like call syntax, but they do not modify the class, and their dispatch uses the declared receiver type. Honest assessment exercises can ask you to predict both null-safety labels, identify a declaration that fails to compile, calculate the filtered list and total, or choose the generated data-class members. Use the first-party Kotlin documentation as the authority for language behaviour.
Short version and the next step
Use nullable types deliberately.
Prefer
?.and?:to unchecked!!.Model transparent value records with data classes.
Keep functions small, using defaults and named calls where useful.
Use collection pipelines and extensions when they clarify transformations.
Remember 150 min, [Data classes, Collections], and on track. For adjacent JVM fundamentals, the Java course teaches the Java language in depth, and DSA using Java adds algorithm practice on the same JVM; both are Java courses, not Kotlin ones. Make the 25 -> 45 change and predict all five lines before running.




