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
- Create variables of different numeric types and explore their ranges
- Write a function that processes different types using type checking
- Create type aliases for a domain model (e.g., e-commerce system)
- Experiment with nullable types and safe operations
- 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
- What's the difference between Int and Int? in Kotlin?
- Which type is the supertype of all non-nullable types?
- What does the Nothing type represent?
- When should you use explicit type declarations?
Show answers
- Int is non-nullable, Int? is nullable (can hold null values)
- Any is the supertype of all non-nullable types
- Nothing represents expressions that never complete normally (like functions that always throw exceptions)
- When the type isn't obvious from context, for API contracts, or when you need a specific type