Arrays and lists in Kotlin: Getting started with these mutable and immutable structures

Video thumbnail

Arrays and lists are fundamental elements in any programming language that allow handling collections of values or data of the same type, whether they are Kotlin primitives or our own objects; in Kotlin.

As a fundamental principle in Kotlin, which is the handling of mutable and immutable types, this principle also extends to the arrays and lists that we will see below.

Previously we saw companion Objects to handle Static and Factories in Kotlin.

When to use lists and when?

In summary, lists represent the modern way of working in Kotlin. Both structures allow us to store a set of elements of the same type (integers, strings, or objects from a database).

For example, if you are developing an academy application (like the one you see in the background), you will have a Book class and you will want to return a list to display it on the screen. For that, we use these structures.

  • Lists: They are the preferred option 95% of the time. They allow varying their size (if they are mutable), manipulating data with functional programming (filters, sorting), and they feature the null safety characteristic of Kotlin.
  • Arrays: They are a lower-level structure. They are mainly used for interoperability with Java or when slightly superior efficiency is sought. They have a fixed size known from their creation.

1. When to use Lists (List, MutableList)

It is the preferred option in 95% of cases in Kotlin.

  • Dynamic size: When you don't know how many elements you are going to have (for example, data coming from a database or an API).
  • Data manipulation: If you need to easily add, remove, or filter elements.
  • Functional Programming: Lists have much more powerful methods to transform data such as map, filter, reduce, and groupBy.
  • Type safety: Lists in Kotlin are "covariant" (you can treat a List<String> as a List<Any>), which makes them more flexible in software design.

2. When to use Arrays (Array, IntArray, etc.)

Arrays are considered a "low-level" structure. You should use them in specific situations:

  • Critical Performance: Arrays are slightly faster and consume less memory because they map directly to native JVM structures. They are used in video games or massive data processing.
  • Primitive Types: If you need to optimize to the maximum, specialized arrays like IntArray or DoubleArray avoid "boxing" (converting numbers into objects), saving a lot of memory.
  • Interoperability with Java: Many old Java libraries request String[] or int[]. In Kotlin, to pass that data, you must use Array.
  • Known fixed size: When the collection will never change size (for example, the months of the year or the coordinates of a 3D point).

Summary:

MethodSizeInitial ContentMain Use
emptyArray()Always 0NothingAvoiding null values in functions.
arrayOfNulls(size)Defined by younullReserving fixed space before having the data.

Arrays in Kotlin: Low-level structure (The "Legacy" way)

If we want to define an array in Kotlin, we can do it using the arrayOf, arrayOfNulls, and emptyArray methods; each of these forms shares common properties and functions as we will see a bit later; finally, to create an array with the arrayOf method we have:

var array = arrayOf(1,2,3,4,5)

And with this we create an array of integers; if we want to print the array as a String or text chain, where we separate each of the elements that make up the array with commas, we can use the following method:

array.joinToString()

And we get:

1, 2, 3, 4, 5

Variations in the arrayOf, arrayOfNulls, and emptyArray methods

If we want to declare the previous array with integers, we use the intArrayOf method:

var array = intArrayOf(1,2,3,4,5)

If on the contrary we want them to be double:

var array = doubleArrayOf(1.1,2.2,3.3,4.4,5.5)

Of course, in all cases we can use the same primitives and methods like the one we mentioned earlier which is the joinToString() method:

array.joinToString() 
// prints 1.1, 2.2, 3.3, 4.4, 5.5

To get the size of the array, we use the size property:

array.size // prints 5 in any of the previous cases

We can also use some functions to get or set values:

var array = doubleArrayOf(1.1,2.2,3.3,4.4,5.5) 
println(array.get(2)) // prints 3.3 array.set(2, 2.5) 
// we set the value 2.5 at index 2 
println(array.get(2)) // prints 2.5 

We can also get values as if it were an array:

println(array[2]) // prints 2.5

emptyArray

This method creates an array with a size of 0.

What is it used for?
  • Default initialization: When you need to initialize a variable that must contain an array, but you don't have elements yet.
  • Function return: If a function must return an array and finds no results, it is better to return an emptyArray than a null value. This way you prevent the app from crashing due to a "null pointer" error.
// We create an empty array of Strings 
var names = emptyArray<String>() 
println(names.size) // Prints 0

arrayOfNulls

This method creates an array of a specific size but fills all its positions with the null value.

What is it used for?

Memory reservation: If you know exactly how many elements your collection will have (for example, 100), but you are going to calculate or receive them one by one later. It is more efficient than creating a list and making it grow.

Data structures: It is widely used when implementing algorithms where you need empty "holes" that will be filled according to certain logic.

// We reserve space for 3 integers, initially they are null 
val myGrades = arrayOfNulls<Int>(3) 
myGrades[0] = 10 myGrades[1] = 8 
// myGrades[2] is still null 
println(myGrades.joinToString()) 

In Summary:

  • emptyArray<T>(): Creates an array of size zero. It is useful when you need to declare the variable but don't have the elements or the final size yet.
  • arrayOfNulls<T>(size): Creates a fixed-size array filled with null values. It serves to reserve memory space that you will fill later.
  • Primitive Arrays: For greater efficiency, Kotlin offers versions like intArrayOf() or doubleArrayOf(). These avoid object "boxing," working directly with primitive types.

Comparison of Array types

If you use arrayOf<Int>(1, 2), Kotlin creates an object for each number ("boxed" Integer), which is inefficient. When using intArrayOf(), Kotlin uses direct primitive types from the processor, which is much faster and saves memory.

The concept of "Boxing"

Imagine a number is a small piece of metal.

  • Primitive Type (int): It is the loose piece of metal. It is light, small, and the processor can handle it directly super fast.
  • Object (Integer): It is that same piece of metal, but tucked inside a velvet-lined wooden box. The box has labels, methods, and takes up much more space in the warehouse (memory).

Why is arrayOf<Int> inefficient?

When you create an Array<Int>, what you have in memory is:

A structure for the Array.

  • References (arrows) pointing to different "Integer" objects scattered throughout memory.
  • Each "Integer" object spends extra memory (object metadata) in addition to the numeric value.

Lists in Kotlin

As we mentioned at the beginning and as a fundamental principle in Kotlin, we have the handling of mutable and immutable types which also leads to the use of lists, so by default, lists in Kotlin are immutable, which in other words means, they cannot mutate or what is the same, they cannot change.

Therefore Kotlin provides methods for queries; on the other hand, for mutable lists Kotlin provides methods to mutate lists as we want, i.e. methods like editing or changing values or removing them; first we will see the default lists or those of the immutable type.

In summary, in Kotlin, by default, lists are immutable (they cannot be modified once created).

  • listOf(): Creates a read-only list. It does not have methods to add or change elements. This protects data integrity.
  • mutableListOf(): If you need to add, delete, or modify elements, you must use this variant.

Immutable Lists

To create a list we have the List class predefined in Kotlin that allows creating lists of generic objects; that is, of any type; to create a list of immutable elements we have:

val list = listOf(1,2,3,4,5)

Here the same organization presented with the Arrays we saw earlier is NOT extended, in that we cannot specify the data type of the list by indicating the data type in front of the list name as we did with Arrays; that is, the following would be invalid code:

val list = intListOf(1,2,3,4,5) // error: unresolved reference: intListOf

Mutable Lists

If on the contrary we need to customize the values of the list, that is, use methods to add, remove, or change values, we can use mutable lists and in this way we have an extra range of options to be able to customize or change the list whenever we want; for this we must add "mutable" in front of "ListOf", for example, to create a mutable list we have:

val mutableList = mutableListOf(1,2,3,4,5)

To change a value, we can do it in two ways, just as happens with Arrays:

mutableList[1] = 85 
println(mutableList[1]) // prints 85

Or through:

mutableList.set(1,90) 
println(mutableList.get(1)) // prints 90

In the two previous code blocks, in addition to changing or setting another value to an index of the list, we saw how to get the value from it; for this we use the conventional method using brackets [] and between them the index of the position we want to query, or the get method that receives said index as a parameter; we can use whichever method we prefer both for setting and getting values.

Deleting values from the list by value and index.

We also have methods to delete values; for this there is the removeAt method which allows deleting a value from the list by indicating the index of the position we want to delete and remove which allows deleting a value by the value of the position:

val mutableList = mutableListOf(10,20,30,400,50) 
mutableList.removeAt(1) 
// the list becomes mutableList [10, 30, 400, 50] 
mutableList.remove(400) 
// the list becomes mutableList [10, 30, 50]

Mutable and immutable lists

As we can see, by default Kotlin offers listOf and by adding the word "mutable" we can already handle mutable types; it's a kind of "rule" or tip we can keep in mind when working with lists; with mutable types, which if we take it to its base meaning are those that can mutate or change, we can use methods to get values via get, delete via, and set values via set which we cannot do with immutable lists.

Lists also have common methods and properties like the size property:

val mutableList = mutableListOf(1,2,3,4,5 ) 
mutableList.size // prints 5 

setOf Lists

In addition to the lists we saw earlier, Kotlin also has another structure similar to lists called setOf, which are collections of data that do not allow repeated values, that is, they only allow unique values, meaning they do not repeat:

val set = setOf(1,1,2,3,4,5,5,1)

It will print:

[1, 2, 3, 4, 5]

This type of data collection does not allow modifying, deleting, or adding new values, that is, they are of the immutable type; to modify it we have to use the mutable types.

val setMutable = mutableSetOf(1,2,3,4,5)

Examples of List operations

1. Plus (+) and Minus (-) Operators

Unlike .add() or .remove(), these operators do not modify the original list, but return a new list with the change applied. This is ideal for maintaining immutability:

val numbers = listOf(10, 20, 30, 40) 
// Plus: Creates a list with an extra element 
val listPlusOne = numbers + 50 // Result: [10, 20, 30, 40, 50] 
// Minus: Creates a list removing the first occurrence of the element 
val listMinusOne = numbers - 30 
// Result: [10, 20, 40] 
println("Original: $numbers") // The original remains intact

2. Data Classes in Lists

In the real world, you rarely handle lists of simple numbers. You handle objects. Data classes are perfect because Kotlin already knows how to compare their values automatically.

data class Product(val name: String, val price: Double) 
val cart = listOf( Product("Laptop", 1200.0), 
Product("Mouse", 25.0), Product("Keyboard", 80.0) ) 
// Now the list contains complex objects println(cart[0].name) 
// Prints: Laptop

3. Predicates (Filter, Map, Any, Find)

Predicates are conditions (lambda functions) that you pass to the list to process the data elegantly.

val prices = listOf(10, 50, 100, 200, 500) 
// FILTER: Keeps only what meets the condition 
val expensive = prices.filter { it > 150 } 
// Result: [200, 500] 
// MAP: Transforms each element (e.g., applying 10% discount) 
val discounts = prices.map { it * 0.9 } 
// Result: [9.0, 45.0, 90.0, 180.0, 450.0] 
// ANY: Returns true if AT LEAST ONE element meets the condition 
val anySuperExpensive = prices.any { it > 1000 } 
// Result: false 
// FIND: Searches for the first element that meets the condition or returns null val firstLarge = prices.find { it > 150 } 
// Result: 200

Putting it all together

Imagine you want the names of products that cost more than 50:

val expensiveProdNames = cart 
  .filter { it.price > 50.0 } // We filter 
  .map { it.name }            
// We extract only the name 
println(expensiveProdNames) 
// [Laptop, Keyboard]

Filtering and Transformation Functions

These functions are extremely powerful and common in modern development:

  • filter: Returns a list with the elements that meet a condition.
  • map: Transforms each element of the list into another data type.
  • any: Returns true if at least one element meets the condition.
  • find: Searches for the first match that meets the rule.

Conclusion

As you can see, lists are usually handled using Data Classes to represent entities (products, books, users). Handling lists and arrays is truly learned by practicing, something we will do thoroughly when we start creating our application in Android.

The Array and lists are a fundamental element that allow handling collections of data that in Kotlin are of mutable or immutable type and as a result of this they allow editing the list and/or consulting it only.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español