1. Operador []: lectura y escritura
Ahora pasemos a lo que motivaba todo esto: como obtener un elemento de un array y como escribirlo. En Kotlin, el operador [] es la forma estandar de acceder a los elementos de un array: a[i] lee el valor, y a[i] = ... escribe uno nuevo.
Empecemos con el ejemplo mas pequeno: leer, cambiar, volver a leer.
fun main() {
val steps = intArrayOf(3000, 5000, 4500)
println(steps[0]) // 3000
steps[0] = 7000
println(steps[0]) // 7000
}
Aqui es importante fijarse en dos cosas.
En primer lugar, los arrays son mutables: sus elementos se pueden cambiar incluso si la variable del array esta declarada como val. val prohibe reasignar la propia referencia al array, pero no prohibe cambiar el contenido de la “caja”. Es un comportamiento normal: “la caja es la misma, pero los objetos dentro han cambiado”.
En segundo lugar, la asignacion steps[0] = 7000 funciona solo si el indice es correcto. Si el indice es incorrecto, el programa fallara en tiempo de ejecucion.
2. Indices y numeracion desde cero
Cuando ves por primera vez a[0], apetece preguntar: “¿Por que no a[1]? Soy humano, no un robot”. La respuesta es simple: historicamente se quedo asi en casi todos los lenguajes, y Kotlin no es una excepcion. El indice es un desplazamiento desde el inicio del array: el primer elemento esta en el desplazamiento 0. Es incomodo exactamente los dos primeros dias, y luego empiezas a contar desde cero incluso las paginas del cuaderno.
La regla principal para arrays suena asi:
- el primer elemento del array tiene indice 0
- el ultimo elemento del array tiene indice size - 1
- los indices permitidos van de 0 a size - 1 inclusive
Kotlin sigue estrictamente este modelo: la indexacion de arrays empieza en cero, y el acceso/modificacion se hace mediante el operador [].
Veamos una ilustracion corta. Imaginemos un array de tres numeros:
valores: [ 50, 10, 99 ]
indices: 0 1 2
size = 3
ultimo indice = size - 1 = 2
Por eso la expresion a[3] para un array de longitud 3 es un intento de encontrar el cuarto elemento, que no existe.
3. size: cuantos elementos y donde esta el limite
Si en el array hay size elementos, surge la tentacion de pensar: “Si el tamano es 3, entonces los indices son 1..3”. Y aqui Kotlin te dice “no”: size es la cantidad de elementos, no el “indice maximo”.
Si size = 3, entonces los indices son: 0, 1, 2. Comparemos en una tabla:
del array |
Indices permitidos | Ultimo indice |
|---|---|---|
|
ninguno | — |
|
|
|
|
|
|
|
|
|
Por cierto, fijate en size = 0 (array vacio): no tiene ni un solo indice permitido. Y por eso cualquier intento de hacer a[0] llevara a un error. Pero ese array tiene derecho a existir.
Mini-ejemplo:
fun main() {
val a = intArrayOf(10, 20, 30)
println("size = ${a.size}") // size = 3
println("last index = ${a.size - 1}") // last index = 2
}
a.size - 1 es una expresion que escribiras muy a menudo. Primero en programas, y luego tambien en la vida real.
4. indices: el rango correcto de indices
Si escribes constantemente 0 y size - 1, es facil equivocarse en una unidad. Y los errores “de 1” son una maldicion antigua de la programacion, que se alimenta de tus deadlines :)
Por eso Kotlin da una cosa comoda: a.indices.
indices es un conjunto listo con todos los indices correctos del array. Si el array tiene longitud 3, entonces indices corresponde aproximadamente a 0..2. Si el array esta vacio, indices sera un “rango vacio”, y eso tambien es util: muchos bucles simplemente no haran nada.
Lo mejor: indices se puede usar tanto en bucles como en comprobaciones.
Comprobar un indice con in a.indices
Imagina que el usuario introduce el numero del dia de la semana y tu quieres escribir los pasos en un array. El usuario es una criatura creativa: hoy introduce 3, manana 999, pasado manana -5 (para comprobar “a ver que pasa”). Por eso hay que validar el indice.
fun main() {
val steps = intArrayOf(3000, 5000, 4500) // 3 dias para el ejemplo
val i = 2
if (i in steps.indices) {
println("steps[$i] = ${steps[i]}") // steps[2] = 4500
} else {
println("Indice incorrecto: $i")
}
}
Esta construccion se lee mas o menos asi: “si i esta dentro de los indices permitidos del array steps”.
Y si: in aqui es precisamente una comprobacion de pertenencia: “si el valor cae dentro del rango”. Esta combinacion (in + indices) es una de las tecnicas mas basicas para trabajar con arrays de forma segura.
Recorrer el array con for (i in a.indices)
Hoy no profundizamos en algoritmos de “suma/maximo/busqueda”, pero el hecho de recorrer por indices ya nos sirve, porque a veces necesitas el indice en si, y no solo el valor.
fun main() {
val steps = intArrayOf(3000, 5000, 4500)
for (i in steps.indices) {
println("Day#$i: ${steps[i]}")
// Day#0: 3000
// Day#1: 5000
// Day#2: 4500
}
}
¿Por que es mejor que for (i in 0..steps.size - 1)? Porque no escribes los limites a mano: menos posibilidades de equivocarte.
Que pasa al salirse de los limites
Hay cosas que Kotlin puede comprobar de antemano (por ejemplo, “no se puede sumar String y Int”). Y hay cosas que dependen de los datos en tiempo de ejecucion. Los indices del array son precisamente el segundo caso.
Si escribes:
val a = intArrayOf(1, 2, 3)
println(a[10])
El compilador puede no detectar tu error. Quizas luego cambies el array, quizas el indice venga de la entrada. Por eso el codigo compila, pero el programa fallara en tiempo de ejecucion con una excepcion de “indice fuera de los limites del array”.
5. Tracker de pasos y actualizacion segura
Para que los ejemplos sean mas realistas, inventemos un pequeno programa de practica que se puede ampliar mas adelante dentro del nivel sobre arrays. Que sea de nuevo un tracker de pasos semanal en consola. Ahora es lo mas simple posible: tenemos un array de 7 dias, pedimos al usuario el dia (indice) y el nuevo valor de pasos, y actualizamos el array de forma segura.
El punto clave: no intentamos hacer una “interfaz perfecta”, no usamos comandos ni parsing complejo: solo lo que ya conoces.
fun main() {
val steps = IntArray(7) // por defecto todos los elementos = 0
println("Introduzca el indice del dia (0..6):")
val day = readln().toIntOrNull()
println("Introduzca el numero de pasos:")
val value = readln().toIntOrNull()
if (day == null || value == null) {
println("Error: hay que introducir numeros enteros.")
return
}
if (day in steps.indices) {
steps[day] = value
println("Ok: steps[$day] = ${steps[day]}")
} else {
println("Error: el dia debe ser de 0 a 6.")
}
}
Fijate en el estilo de “doble proteccion”. Primero comprobamos que la entrada se haya parseado (toIntOrNull() no devolvio null). Luego comprobamos que el indice este dentro de los limites. Esto no es paranoia, es la vida real: el usuario puede introducir cualquier cosa, incluyendo “gatitos” y -100, y el programa al menos no debe caerse.
6. Operaciones masivas: fill() y copyOf()
A veces con un array hay que hacer una operacion “por lotes”: resetear rapidamente los valores o, al contrario, guardar una copia (snapshot) antes de cambios. Para eso Kotlin tiene metodos simples, pero muy utiles.
fill(value): rellenar rapidamente un array con un valor
A veces hace falta sobrescribir el array en masa: resetear valores, poner un valor por defecto, llenarlo con datos “vacios”. Hacerlo a mano con un bucle se puede, pero es aburrido: se supone que el programador debe ser perezoso de forma constructiva.
Para eso existe el metodo fill(value): recorre el array y escribe el mismo valor en todos los elementos.
Imagina este escenario: llevamos los pasos de la semana, pero queremos “empezar de cero” (por ejemplo, un nuevo challenge). Entonces lo mas facil es hacer un reset:
fun main() {
val steps = intArrayOf(1200, 5000, 4300, 0, 8000, 6500, 2000)
steps.fill(0)
for (x in steps) {
print("$x ")
}
// 0 0 0 0 0 0 0
}
Aqui es importante no confundir expectativas: fill(0) no cambia el tamano del array, solo cambia los valores de los elementos.
Si rellenas IntArray(7) con sietes, tendras siete elementos y todos seran 7, sin magia:
fun main() {
val steps = IntArray(7)
steps.fill(7000)
println(steps[0]) // 7000
println(steps[6]) // 7000
}
Esta es una de esas operaciones que hacen el codigo mas corto y mas claro: “rellena el array con el valor X” se lee mejor que un bucle de 7 lineas para la misma idea.
copyOf(): copia del array vs “segundo nombre”
Ahora viene uno de los temas mas importantes del nivel, y uno de los mas “traicioneros” para principiantes.
Cuando escribes:
val a = intArrayOf(1, 2, 3)
val b = a
no copias el array. Creas una segunda variable que apunta al mismo array. Es como dos accesos directos en el escritorio que llevan a la misma carpeta. Borras un archivo desde un acceso directo y desde el otro tambien desaparece.
En cambio, copyOf() crea un nuevo array que inicialmente contiene los mismos valores, pero existe por separado.
Veamos la diferencia en codigo. Primero, solo una referencia sin duplicar el array:
fun main() {
val original = intArrayOf(10, 20, 30)
val alias = original
alias[0] = 999
println("original[0] = ${original[0]}") // original[0] = 999
println("alias[0] = ${alias[0]}") // alias[0] = 999
}
Muchos esperan que original se quede en (10, 20, 30), pero no: alias no es una copia, es el mismo array.
Ahora, la copia independiente correcta:
fun main() {
val original = intArrayOf(10, 20, 30)
val copy = original.copyOf()
copy[0] = 999
println("original[0] = ${original[0]}") // original[0] = 10
println("copy[0] = ${copy[0]}") // copy[0] = 999
}
En el tracker de pasos, esto se puede usar como un “snapshot de la semana”: guardas los valores antiguos y luego empiezas a modificar los actuales.
Practica: snapshot del array y reset de la semana
Unamos dos operaciones (copyOf() y fill()) en un escenario pequeno pero practico. Que el programa haga esto: tenemos los pasos de la semana, creamos una copia como “archivo”, y luego reseteamos la semana actual a ceros. En la vida real puede ser “una nueva semana empieza con una hoja en blanco”.
fun main() {
val steps = intArrayOf(1200, 5000, 4300, 0, 8000, 6500, 2000)
val archive = steps.copyOf()
steps.fill(0)
print("archive: ")
for (x in archive) print("$x ")
println()
// archive: 1200 5000 4300 0 8000 6500 2000
print("steps: ")
for (x in steps) print("$x ")
println()
// steps: 0 0 0 0 0 0 0
}
7. Errores tipicos
Error n.º1: pensar que el ultimo indice es igual a size.
Es un clasico: un array de 3 elementos, escribes a[3] y te sorprendes de que el programa se caiga. La regla es simple: size es el numero de elementos, y el ultimo indice es size - 1. Si no quieres tenerlo en la cabeza todo el tiempo, usa a.indices en bucles y comprobaciones para no calcular los limites a mano.
Error n.º2: no validar el indice que viene de la entrada (o de un calculo).
Mientras el indice este “escrito a mano” y estes seguro de que es correcto, todo bien. Pero en cuanto el indice viene del usuario o se calcula de forma mas compleja, puede volverse negativo o demasiado grande. Entonces el acceso a[i] se convierte en una caida del programa. El habito de if (i in a.indices) no es una “linea extra”, sino un seguro contra un comportamiento incontrolado.
Error n.º3: creer que val b = a crea una copia del array.
No: es solo otro nombre para el mismo array. Cambias b[0] y tambien cambiara a[0]. Si necesitas una copia independiente, usa copyOf(). Esto es especialmente importante cuando guardas “snapshots” del estado: sin copyOf() estropearas el “archivo” al modificar el “actual”.
Error n.º4: intentar “imprimir el array” con println(a) y esperar una lista de elementos.
Para un principiante parece logico: “estoy imprimiendo la variable”. Pero por defecto veras no el contenido, sino una representacion tecnica del objeto. La forma mas clara para este nivel de ver los elementos es recorrerlos con un bucle e imprimir cada elemento. Si, es un poco mas largo, pero entiendes exactamente lo que esta pasando.
Error n.º5: esperar que fill() cambie el tamano del array o anada elementos.
fill(value) no anade ni elimina nada: solo sobrescribe los elementos existentes. El array sigue siendo de tamano fijo, exactamente el que creaste al principio. Si quieres una estructura “que crece”, sera un tema aparte mas adelante; hoy quedate con el principio: un array es una caja fija, y fill() solo coloca dentro objetos iguales.
GO TO FULL VERSION