Kotlin / Language foundations and explicit state / Lesson 1
Run a Kotlin program and reason about values
Understand a JVM-oriented entry point, val and var, inferred types, arithmetic, and explicit conversions.
On this page
Lesson goals
- Explain what the compiler infers and what a declaration promises.
- Predict arithmetic and output without confusing a read-only reference with an immutable object.
Separate source code, compilation, and execution
Kotlin targets several platforms. This course uses JVM-oriented plain source examples unless a lesson explicitly states otherwise; Android, JavaScript, and Native have different platform libraries and build requirements. A top-level main function is a program entry point, and println writes a textual representation followed by a line break. Source code must first be compiled or evaluated by suitable Kotlin tooling. Java being installed does not prove that a Kotlin compiler is available. No compiler was present in this authoring environment, so expected outputs are reasoned examples and runnable flags remain false.
Choose val unless reassignment is needed
A val local is assigned once; a var local can receive another value of its declared type. Type inference chooses a type from the initializer, not from every future use. A count initialized with an integer literal is normally inferred as Int, so later assigning a fractional value is a type mismatch rather than automatic widening. Prefer meaningful names and explicit units, such as minutes or cents. A concise declaration should still let a reader understand the valueโs role and the range the surrounding algorithm expects.
fun main() {
val capacity = 8
var reserved = 2
reserved += 1
val remaining: Int = capacity - reserved
println("remaining=$remaining")
println("capacity type remains Int")
}
remaining=5
capacity type remains Int
The expected lines are remaining=5 and the literal type note. capacity is never reassigned, while reserved is. This complete teaching program was not compiled in the authoring environment.
Division follows operand types
When both operands are integers, division produces an integer result and discards the fractional part toward zero. Making one operand Double requests floating-point division. An explicit conversion such as toDouble changes the represented numeric valueโs type for the operation; a cast is not a general numeric conversion. Floating-point values cannot exactly represent every decimal fraction. Use an appropriate money representation or domain library for financial rules rather than assuming a displayed decimal proves exact accounting. Integer arithmetic also has bounded ranges, so large inputs require deliberate range reasoning.
Keep conversion visible at the calculation boundary
The example contrasts integer and floating-point results using the same positive inputs. String templates insert a value using a dollar-prefixed name or a braced expression. A template changes presentation, not arithmetic semantics: division inside braces still follows operand types. Avoid relying on formatting to fix a calculation performed in the wrong type. When combining durations or sizes, convert to a common unit before the operation and record whether rounding is intended. The expected output is a language-level prediction, not a measured performance result.
fun main() {
val tasks = 5
val workers = 2
println(tasks / workers)
println(tasks.toDouble() / workers)
println("workers=$workers")
}
2
2.5
workers=2
Expected output is 2, then 2.5, then workers=2. Converting tasks before division preserves the fractional result; converting the already truncated result would not. No compiler execution is claimed.
Read-only binding does not freeze an object
A val can refer to a mutable collection whose contents still change. It prevents assigning a different collection to that variable, not mutation through the referenced object. Likewise, a read-only interface may view an object that another reference can mutate. These distinctions become important when sharing state between functions or concurrent tasks. Begin with the simplest value that expresses the domain and restrict mutation to an explicit owner. Use small deterministic examples while learning, then introduce input and platform dependencies only when their error and lifetime behavior are understood.
Common errors and fixes
Val cannot be reassigned.
- Why it happens
- A read-only binding was used for a value that must be replaced.
- How to fix it
- Use var for deliberate reassignment or compute a new val.
- Prevent it next time
- Decide whether the algorithm needs state or a derived value.
The fractional part disappears.
- Why it happens
- Both division operands were integers.
- How to fix it
- Convert an operand before division when fractional semantics are intended.
- Prevent it next time
- Include a non-divisible case in examples and tests.
A val list changes unexpectedly.
- Why it happens
- Binding immutability was confused with object immutability.
- How to fix it
- Inspect mutable aliases and choose ownership or a defensive copy.
- Prevent it next time
- Document whether values are snapshots or shared views.
Check your understanding
1. Predict 7 / 2 and 7.toDouble() / 2. Explain the difference.
Show worked solution
The first is the integer value 3; the second is the Double value 3.5. The conversion occurs before division in the second expression. Writing (7 / 2).toDouble() would produce 3.0 because the fractional part was already discarded. Select the operation type before attempting to format the result.
2. A seat count is never reassigned but a mutable list of reservations grows. Which can use val?
Show worked solution
Both bindings can use val. The number remains the same value, and the list reference remains the same object even while its contents change. This does not make the list safe for concurrent mutation or protect a capacity invariant. Those guarantees need a separate design.
Key takeaways
- Types determine operations, and val controls reassignment.
- Expected output is distinct from a compiler-tested result.