Kotlin - Data Types

Kotlin Data Types

Kotlin has a rich type system that ensures type safety while maintaining simplicity. Learn about basic types, nullable types, type inference, and how Kotlin handles primitives vs objects.

Type System Overview

Everything is an Object

In Kotlin, everything is an object in the sense that you can call member functions and properties on any variable. However, some types can have a special internal representation (like primitives) for performance.

Key Insight: Kotlin's type system is unified - there's no distinction between primitive and wrapper types like in Java. The compiler optimizes to primitives when possible.

Basic Types (Numbers)

Integer Types

val byte: Byte = 127              // 8 bits: -128 to 127
val short: Short = 32767          // 16 bits: -32,768 to 32,767  
val int: Int = 2147483647         // 32 bits: -2^31 to 2^31-1
val long: Long = 9223372036854775807L  // 64 bits: -2^63 to 2^63-1

// Type inference (recommended)
val defaultInt = 42               // Int (default for integers)
val longNumber = 1000000000000L   // Long (L suffix)

// Underscores for readability
val million = 1_000_000
val binaryLit = 0b01010101        // Binary
val hexLit = 0xFF                 // Hexadecimal

Floating Point Types

val float: Float = 3.14f          // 32 bits, f or F suffix
val double: Double = 3.14159      // 64 bits (default for decimals)

// Type inference
val pi = 3.14159                  // Double (default)
val radius = 5.0f                 // Float (f suffix)

// Scientific notation
val avogadro = 6.022e23           // Double
val electron = 9.109e-31f         // Float

Type Ranges and Properties

fun exploreNumericTypes() {
    println("Byte: ${Byte.MIN_VALUE} to ${Byte.MAX_VALUE}")
    println("Short: ${Short.MIN_VALUE} to ${Short.MAX_VALUE}")
    println("Int: ${Int.MIN_VALUE} to ${Int.MAX_VALUE}")
    println("Long: ${Long.MIN_VALUE} to ${Long.MAX_VALUE}")
    
    println("Float: ${Float.MIN_VALUE} to ${Float.MAX_VALUE}")
    println("Double: ${Double.MIN_VALUE} to ${Double.MAX_VALUE}")
}

Character and Boolean Types

Character Type

val char: Char = 'A'
val digit: Char = '7'
val newline: Char = '\n'          // Escape sequences
val unicode: Char = '\u0041'      // Unicode (A)

// Characters are not numbers (unlike Java/C++)
// val wrong = char + 1           // ❌ Compilation error

// Correct way to work with characters
val nextChar = char + 1           // Actually works in Kotlin!
val charCode = char.code          // Get Unicode code point
val fromCode = 65.toChar()        // Convert number to Char

Boolean Type

val isReady: Boolean = true
val isComplete: Boolean = false

// Boolean operations
val result = isReady && !isComplete     // Logical AND and NOT
val either = isReady || isComplete      // Logical OR

// Nullable boolean
val maybeTrue: Boolean? = null
val defaultTrue = maybeTrue ?: true     // Default if null

String Type

String Basics

val greeting: String = "Hello, Kotlin!"
val empty: String = ""
val nullable: String? = null

// String properties and methods
println(greeting.length)           // 14
println(greeting.uppercase())      // HELLO, KOTLIN!
println(greeting.lowercase())      // hello, kotlin!
println(greeting.isEmpty())        // false
println(greeting.isNotEmpty())     // true

String Templates

val name = "Alice"
val age = 30

// Simple template
val message = "Hello, $name!"

// Expression template
val info = "$name is $age years old"
val nextYear = "Next year, $name will be ${age + 1}"

// Multiline strings
val multiline = """
    Line 1
    Line 2
    Line 3
""".trimIndent()

// Raw strings with $ literal
val price = """The price is ${'$'}99.99"""

Arrays

Array Creation and Access

// Array creation
val numbers = arrayOf(1, 2, 3, 4, 5)
val strings = arrayOf("apple", "banana", "cherry")
val mixed = arrayOf(1, "text", true)         // Array<Any>

// Typed arrays
val ints: Array<Int> = arrayOf(1, 2, 3)
val nullableStrings: Array<String?> = arrayOfNulls(5)

// Primitive arrays (more efficient)
val intArray = intArrayOf(1, 2, 3, 4, 5)
val doubleArray = doubleArrayOf(1.0, 2.0, 3.0)
val booleanArray = booleanArrayOf(true, false, true)

// Array with initialization function
val squares = Array(5) { i -> i * i }        // [0, 1, 4, 9, 16]

Array Operations

val fruits = arrayOf("apple", "banana", "cherry")

// Access elements
println(fruits[0])              // apple
println(fruits.get(1))          // banana

// Modify elements
fruits[0] = "orange"
fruits.set(1, "grape")

// Array properties
println(fruits.size)            // 3
println(fruits.indices)         // 0..2
println(fruits.lastIndex)       // 2

// Array methods
println(fruits.contains("cherry"))     // true
println(fruits.indexOf("grape"))       // 1
println(fruits.contentToString())      // [orange, grape, cherry]

Type Hierarchy

Any - The Root Type

// Any is the supertype of all non-nullable types
val anything: Any = 42
val alsoAnything: Any = "Hello"
val stillAnything: Any = true

// Any provides common methods
println(anything.toString())
println(anything.equals(42))
println(anything.hashCode())

// Any? is the supertype of all types (including nullable)
val maybeAnything: Any? = null

Unit - Represents No Value

// Functions that don't return a meaningful value return Unit
fun printMessage(): Unit {
    println("Hello!")
}

// Unit can be omitted (it's the default)
fun printMessageImplicit() {
    println("Hello!")
}

// Unit as a value
val unitValue: Unit = Unit
val fromFunction: Unit = printMessage()

Nothing - Represents Never

// Nothing is the type of expressions that never complete normally
fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}

fun infiniteLoop(): Nothing {
    while (true) {
        println("Running forever...")
    }
}

// Nothing is a subtype of every type
val result: String = if (condition) "value" else fail("Error")
// fail() returns Nothing, which can be assigned to String

Nullable Types

Nullable vs Non-Nullable

// Non-nullable types (default)
val name: String = "Alice"        // Cannot be null
val age: Int = 30                 // Cannot be null

// Nullable types (explicit with ?)
val optionalName: String? = null  // Can be null
val optionalAge: Int? = null      // Can be null

// Type checking
if (optionalName != null) {
    println(optionalName.length)  // Smart cast to String
}

Working with Nullable Types

val nullableString: String? = "Hello"

// Safe call operator
val length = nullableString?.length      // Int? (nullable Int)

// Elvis operator (default value)
val definiteLength = nullableString?.length ?: 0

// Safe cast
val obj: Any = "Hello"
val str: String? = obj as? String        // Safe cast to String?

// Not-null assertion (use carefully!)
val definiteString: String = nullableString!!  // Throws if null

Type Inference

Automatic Type Deduction

// Kotlin can infer types from context
val autoInt = 42                  // Int
val autoDouble = 3.14             // Double
val autoString = "Hello"          // String
val autoBoolean = true            // Boolean

// Collections
val autoList = listOf(1, 2, 3)    // List<Int>
val autoMap = mapOf("key" to "value")  // Map<String, String>

// Function return types can be inferred
fun add(a: Int, b: Int) = a + b    // Return type is Int

When to Use Explicit Types

// When type isn't obvious
val data: List<String> = emptyList()
val callback: () -> Unit = { println("Done") }

// For API contracts
fun processData(input: List<String>): Map<String, Int> {
    return input.associateWith { it.length }
}

// When you need a specific numeric type
val preciseValue: Float = 3.14f
val smallNumber: Byte = 100

Type Aliases

Creating Type Aliases

// Simple aliases for clarity
typealias UserName = String
typealias UserId = Int
typealias Coordinates = Pair<Double, Double>

// Function type aliases
typealias EventHandler = (String) -> Unit
typealias Predicate<T> = (T) -> Boolean

// Generic aliases
typealias StringMap<T> = Map<String, T>
typealias MutableStringMap<T> = MutableMap<String, T>

Using Type Aliases

fun createUser(id: UserId, name: UserName): User {
    return User(id, name)
}

fun handleEvent(handler: EventHandler) {
    handler("Button clicked")
}

val userPreferences: StringMap<Boolean> = mapOf(
    "darkMode" to true,
    "notifications" to false
)

Type Checking and Casting

is and !is Operators

fun processValue(value: Any) {
    when {
        value is String -> {
            println("String with length ${value.length}")  // Smart cast
        }
        value is Int -> {
            println("Integer: ${value * 2}")              // Smart cast
        }
        value !is Boolean -> {
            println("Not a boolean: $value")
        }
    }
}

Casting Operations

val obj: Any = "Hello, World!"

// Unsafe cast (throws ClassCastException if fails)
val str1: String = obj as String

// Safe cast (returns null if fails)
val str2: String? = obj as? String

// Safe cast with default
val str3: String = (obj as? String) ?: "default"

// Casting to nullable type
val nullableStr: String? = obj as? String?

Real-World Examples

User Data Processing

data class User(
    val id: UserId,
    val name: UserName,
    val email: String?,
    val age: Int?,
    val preferences: StringMap<Any>
)

fun validateUser(user: User): List<String> {
    val errors = mutableListOf<String>()
    
    if (user.name.isBlank()) {
        errors.add("Name cannot be blank")
    }
    
    user.email?.let { email ->
        if (!email.contains("@")) {
            errors.add("Invalid email format")
        }
    }
    
    user.age?.let { age ->
        if (age < 0 || age > 150) {
            errors.add("Invalid age range")
        }
    }
    
    return errors
}

Number Processing Utility

fun processNumbers(values: Array<Any>): Map<String, List<Number>> {
    val integers = mutableListOf<Int>()
    val decimals = mutableListOf<Double>()
    val others = mutableListOf<Number>()
    
    for (value in values) {
        when (value) {
            is Int -> integers.add(value)
            is Double -> decimals.add(value)
            is Number -> others.add(value)
        }
    }
    
    return mapOf(
        "integers" to integers,
        "decimals" to decimals,
        "others" to others
    )
}

Best Practices

✅ Do

  • Use type inference when the type is obvious
  • Prefer non-nullable types when possible
  • Use type aliases for complex or repeated types
  • Use safe casts (as?) instead of unsafe casts
  • Choose appropriate numeric types for your use case

❌ Don't

  • Use explicit types when they're obvious from context
  • Use unsafe casts unless you're certain
  • Mix nullable and non-nullable unnecessarily
  • Use Any when a more specific type is appropriate
Architecture Note: Kotlin's type system provides safety without sacrificing performance. The compiler optimizes to JVM primitives when possible while maintaining object-oriented consistency in the language.

Practice Exercises

  1. Create variables of different numeric types and explore their ranges
  2. Write a function that processes different types using type checking
  3. Create type aliases for a domain model (e.g., e-commerce system)
  4. Experiment with nullable types and safe operations
  5. Build a simple calculator that handles different numeric types

Real-World Example

// E-commerce type system
typealias ProductId = String
typealias Price = Double
typealias Quantity = Int

data class Product(
    val id: ProductId,
    val name: String,
    val price: Price,
    val description: String?,
    val inStock: Boolean
)

data class CartItem(
    val product: Product,
    val quantity: Quantity
) {
    val totalPrice: Price get() = product.price * quantity
}

class ShoppingCart {
    private val items = mutableMapOf<ProductId, CartItem>()
    
    fun addItem(product: Product, quantity: Quantity) {
        val existingItem = items[product.id]
        if (existingItem != null) {
            items[product.id] = existingItem.copy(
                quantity = existingItem.quantity + quantity
            )
        } else {
            items[product.id] = CartItem(product, quantity)
        }
    }
    
    val totalPrice: Price
        get() = items.values.sumOf { it.totalPrice }
}

Quick Quiz

  1. What's the difference between Int and Int? in Kotlin?
  2. Which type is the supertype of all non-nullable types?
  3. What does the Nothing type represent?
  4. When should you use explicit type declarations?
Show answers
  1. Int is non-nullable, Int? is nullable (can hold null values)
  2. Any is the supertype of all non-nullable types
  3. Nothing represents expressions that never complete normally (like functions that always throw exceptions)
  4. When the type isn't obvious from context, for API contracts, or when you need a specific type