Kotlin’s type system is one of its defining features. It combines the safety of static typing with the convenience of type inference, so you rarely need to write explicit type annotations while still getting full compile-time checking. Most notably, Kotlin distinguishes between nullable and non-nullable types at the language level, eliminating the null pointer exceptions that plague Java code.
As a multi-paradigm language blending object-oriented and functional styles, Kotlin encourages immutability by default. The val keyword declares read-only bindings while var declares mutable ones — a design choice that nudges developers toward safer, more predictable code. In this tutorial, you’ll explore Kotlin’s type system, variable declarations, type conversions, and the null safety features that set it apart.
Variable Declarations: val vs var
Kotlin provides two keywords for declaring variables. The val keyword creates a read-only reference (similar to final in Java), while var creates a mutable reference that can be reassigned. Kotlin’s type inference means the compiler can determine the type from the assigned value, though you can always declare types explicitly.
Create a file named Variables.kt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| fun main() {
// Immutable (read-only) variables with val
val language: String = "Kotlin" // Explicit type
val version = 2.0 // Inferred as Double
val year = 2011 // Inferred as Int
val isModern = true // Inferred as Boolean
println("=== val (read-only) variables ===")
println("Language: $language")
println("Version: $version")
println("Year: $year")
println("Is modern: $isModern")
// Mutable variables with var
var counter = 0
println("\n=== var (mutable) variables ===")
println("Counter before: $counter")
counter = 10
println("Counter after: $counter")
// Basic types
println("\n=== Basic Types ===")
val byte: Byte = 127 // 8-bit signed integer
val short: Short = 32767 // 16-bit signed integer
val int: Int = 2_147_483_647 // 32-bit signed integer (underscores for readability)
val long: Long = 9_000_000_000L // 64-bit signed integer
val float: Float = 3.14F // 32-bit floating point
val double: Double = 3.141592653589793 // 64-bit floating point
val char: Char = 'K' // Single character
val bool: Boolean = true // Boolean
println("Byte: $byte")
println("Short: $short")
println("Int: $int")
println("Long: $long")
println("Float: $float")
println("Double: $double")
println("Char: $char")
println("Boolean: $bool")
// Strings
println("\n=== Strings ===")
val greeting = "Hello, Kotlin"
val multiline = """
|This is a
|multiline string
|with trimmed margins
""".trimMargin()
println("Greeting: $greeting")
println("Length: ${greeting.length}")
println("Multiline:\n$multiline")
// Type conversions (explicit — no implicit widening in Kotlin)
println("\n=== Type Conversions ===")
val intVal = 42
val longVal = intVal.toLong()
val doubleVal = intVal.toDouble()
val stringVal = intVal.toString()
val backToInt = "123".toInt()
println("Int: $intVal")
println("To Long: $longVal")
println("To Double: $doubleVal")
println("To String: \"$stringVal\" (type: String)")
println("String to Int: $backToInt")
}
|
Null Safety
Kotlin’s null safety system is built into the type system itself. By default, variables cannot hold null values. To allow nulls, you must explicitly declare the type as nullable using the ? suffix. This forces you to handle the null case, preventing null pointer exceptions at compile time rather than runtime.
Create a file named NullSafety.kt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| fun main() {
// Non-nullable types (default)
val name: String = "Kotlin"
// val invalid: String = null // Won't compile!
// Nullable types use the ? suffix
var nickname: String? = "K"
println("=== Null Safety ===")
println("Name: $name")
println("Nickname: $nickname")
nickname = null
println("Nickname after null: $nickname")
// Safe call operator (?.) — returns null if the receiver is null
println("\n=== Safe Calls ===")
println("Nickname length: ${nickname?.length}") // prints: null
println("Name length: ${name.length}") // prints: 6
// Elvis operator (?:) — provides a default for null
val displayName = nickname ?: "No nickname"
println("Display: $displayName")
// Safe calls with chaining
val input: String? = "hello, world"
val result = input?.uppercase()?.take(5)
println("Chained safe call: $result")
// Not-null assertion (!!) — throws if null, use sparingly
val definitelyNotNull: String? = "I exist"
println("Asserted length: ${definitelyNotNull!!.length}")
// Smart casts with null checks
println("\n=== Smart Casts ===")
val maybeNull: String? = "Kotlin"
if (maybeNull != null) {
// Compiler knows maybeNull is non-null here
println("Smart cast length: ${maybeNull.length}")
}
// let — execute block only when non-null
nickname?.let {
println("This won't print because nickname is null")
}
val restored: String? = "Back again"
restored?.let {
println("Let block: $it has ${it.length} chars")
}
}
|
Constants and Compile-Time Values
Kotlin distinguishes between runtime read-only values (val) and true compile-time constants (const val). The const modifier can only be applied to top-level or object properties with primitive or String types.
Create a file named Constants.kt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| // Compile-time constants (must be top-level or in object/companion)
const val MAX_USERS = 1000
const val APP_NAME = "CodeArchaeology"
const val PI = 3.14159265358979
// Object for grouping constants (similar to Java's static final)
object Config {
const val VERSION = "1.0"
const val DEBUG = false
}
fun main() {
println("=== Constants ===")
println("Max users: $MAX_USERS")
println("App name: $APP_NAME")
println("Pi: $PI")
println("Version: ${Config.VERSION}")
println("Debug: ${Config.DEBUG}")
// val is read-only but computed at runtime
val currentTime = System.currentTimeMillis()
println("\n=== Runtime val vs const val ===")
println("Current time (runtime val): $currentTime")
println("App name (const val): $APP_NAME")
// Type checking with 'is' and smart casts
println("\n=== Type Checking ===")
val items: List<Any> = listOf(42, "hello", 3.14, true, 'K')
for (item in items) {
val description = when (item) {
is Int -> "Int: $item (doubled: ${item * 2})"
is String -> "String: \"$item\" (length: ${item.length})"
is Double -> "Double: $item"
is Boolean -> "Boolean: $item"
is Char -> "Char: '$item'"
else -> "Unknown: $item"
}
println(description)
}
}
|
Running with Docker
1
2
3
4
5
6
7
8
9
10
11
| # Pull the Kotlin image
docker pull zenika/kotlin:1.4
# Run the variables example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 sh -c "kotlinc Variables.kt -include-runtime -d variables.jar && java -jar variables.jar"
# Run the null safety example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 sh -c "kotlinc NullSafety.kt -include-runtime -d nullsafety.jar && java -jar nullsafety.jar"
# Run the constants example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 sh -c "kotlinc Constants.kt -include-runtime -d constants.jar && java -jar constants.jar"
|
Expected Output
Variables.kt:
=== val (read-only) variables ===
Language: Kotlin
Version: 2.0
Year: 2011
Is modern: true
=== var (mutable) variables ===
Counter before: 0
Counter after: 10
=== Basic Types ===
Byte: 127
Short: 32767
Int: 2147483647
Long: 9000000000
Float: 3.14
Double: 3.141592653589793
Char: K
Boolean: true
=== Strings ===
Greeting: Hello, Kotlin
Length: 13
Multiline:
This is a
multiline string
with trimmed margins
=== Type Conversions ===
Int: 42
To Long: 42
To Double: 42.0
To String: "42" (type: String)
String to Int: 123
NullSafety.kt:
=== Null Safety ===
Name: Kotlin
Nickname: K
Nickname after null: null
=== Safe Calls ===
Nickname length: null
Name length: 6
Display: No nickname
Chained safe call: HELLO
Asserted length: 7
=== Smart Casts ===
Smart cast length: 6
Let block: Back again has 10 chars
Constants.kt (currentTime will vary):
=== Constants ===
Max users: 1000
App name: CodeArchaeology
Pi: 3.14159265358979
Version: 1.0
Debug: false
=== Runtime val vs const val ===
Current time (runtime val): 1712150400000
App name (const val): CodeArchaeology
=== Type Checking ===
Int: 42 (doubled: 84)
String: "hello" (length: 5)
Double: 3.14
Boolean: true
Char: 'K'
Key Concepts
val vs var: Prefer val (read-only) by default; use var only when mutation is necessary. This encourages immutable, predictable code.- Type inference: Kotlin infers types from assigned values, reducing verbosity while maintaining full static type safety.
- No implicit conversions: Unlike Java, Kotlin requires explicit conversion between numeric types (e.g.,
intVal.toLong()), preventing subtle precision bugs. - Null safety: The
? suffix marks nullable types. The compiler enforces null checks, eliminating null pointer exceptions at compile time. - Safe call operator (
?.): Chains operations on nullable values without manual null checks, returning null if any link in the chain is null. - Elvis operator (
?:): Provides concise default values for nullable expressions, replacing verbose if-null-then patterns. - Smart casts: After a null check or type check, the compiler automatically casts the variable to the appropriate type within that scope.
const val: True compile-time constants for primitives and strings, enforced at the language level — distinct from runtime val bindings.
Comments
Loading comments...
Leave a Comment