Arrays y matrices
En esta guía (13)
En esta lección vas a trabajar con arrays: colecciones de elementos del mismo tipo, guardados uno al lado del otro en memoria. Vas a ver los de longitud fija (Array), los de longitud variable (ArrayBuffer), cómo recorrerlos y transformarlos, cómo armar matrices de varias dimensiones y cómo leer la documentación de un método en Scaladoc. Al final vas a aplicar todo en una serie de ejercicios con cuentas bancarias.
La documentación oficial de las colecciones de Scala está en docs.scala-lang.org, en la sección Collections Types. Es una buena referencia para tener a mano.
Arrays de longitud fija
Un Array tiene un tamaño fijo: se define al crearlo y no cambia. A cambio, es la estructura más eficiente y la que menos memoria ocupa.
val miArray = Array(1, 2, 3, 4, 5)
println(miArray(0)) // 1: se accede por índice, empezando en 0
println(miArray.length) // 5
miArray(0) = 10 // los elementos se pueden modificar
println(miArray.mkString(", ")) // 10, 2, 3, 4, 5
val vacio = new Array[Int](3) // tres posiciones, inicializadas en 0
println(vacio.mkString(", ")) // 0, 0, 0
Tres detalles:
- Para acceder a un elemento se usan paréntesis, no corchetes:
miArray(0). Es una llamada al métodoapplyque viste en la lección 2. - Aunque
miArraysea unval, sus elementos se pueden modificar. Elvalimpide reasignar la variable a otro array, no cambiar el contenido. ElArrayes una colección mutable. - Si hacés
println(miArray)vas a ver algo como[I@1b6d3586, que es cómo Java representa un array. Para ver el contenido, usámkString.
Arrays de longitud variable: ArrayBuffer
Cuando necesitás agregar o quitar elementos, usá un ArrayBuffer, que crece y se achica según haga falta:
import scala.collection.mutable.ArrayBuffer
val miBuffer = ArrayBuffer(1, 2, 3)
miBuffer += 4 // agrega un elemento al final
miBuffer -= 2 // quita la primera aparición del 2
miBuffer ++= List(5, 6) // agrega varios
miBuffer.insert(0, 100) // inserta en una posición
miBuffer.remove(1) // quita el elemento de la posición 1
println(miBuffer) // ArrayBuffer(100, 3, 4, 5, 6)
val fijo: Array[Int] = miBuffer.toArray // convierte a Array cuando terminás de construirlo
Un patrón común: armar los datos en un ArrayBuffer y, cuando están completos, convertirlos a Array con toArray.
Otras formas de crear y combinar arrays
// Una matriz de 3x3, inicializada en 0
val miMatriz = Array.ofDim[Int](3, 3)
// Concatenar arrays
val lista1 = Array(1.9, 2.9, 3.4, 3.5)
val lista2 = Array(8.9, 7.9, 0.4, 1.5)
val lista3 = lista1 ++ lista2
lista3.foreach(println)
// Crear un array a partir de un rango: de 10 a 20 (sin incluir), de a 2
val rango = Array.range(10, 20, 2)
println(rango.mkString(", ")) // 10, 12, 14, 16, 18
// Agregar un elemento: :+ devuelve un array nuevo
var cuentas = Array("A", "B")
cuentas = cuentas :+ "C" // o, más corto: cuentas :+= "C"
println(cuentas.mkString(", ")) // A, B, C
Como un Array no cambia de tamaño, :+ no lo modifica: crea uno nuevo con el elemento agregado. Por eso hace falta reasignarlo, y por eso cuentas es un var.
Métodos de Array
Algunos de los métodos más usados del objeto Array:
-
apply(x: T, xs: T*): Array[T]crea un array con los elementos que le pases.scalaval array = Array(1, 2, 3) // equivale a Array.apply(1, 2, 3) -
concat[T](xss: Array[T]*): Array[T]concatena varios arrays en uno.scalaval array1 = Array(1, 2, 3) val array2 = Array(4, 5, 6) val concatenado = Array.concat(array1, array2) // Array(1, 2, 3, 4, 5, 6) -
copy(src, srcPos, dest, destPos, length): Unitcopia elementos de un array a otro, comoSystem.arraycopyen Java.scalaval origen = Array(1, 2, 3) val destino = new Array[Int](3) Array.copy(origen, 0, destino, 0, 3) println(destino.mkString(", ")) // 1, 2, 3 -
empty[T]: Array[T]devuelve un array de longitud 0.scalaval vacio = Array.empty[Int] -
iterate[T](start: T, len: Int)(f: T => T): Array[T]aplica una función repetidamente a partir de un valor inicial.scalaval potencias = Array.iterate(1, 5)(_ * 2) // Array(1, 2, 4, 8, 16) -
fill[T](n: Int)(elem: => T): Array[T]crea un array repitiendo un valor.scalaval saludos = Array.fill(4)("Hola") // Array(Hola, Hola, Hola, Hola) -
fill[T](n1: Int, n2: Int)(elem: => T): Array[Array[T]]crea un array bidimensional.scalaval ceros = Array.fill(3, 2)(0) // una matriz de 3x2 llena de ceros
Recorrer arrays y buffers
Podés recorrerlos con un for o con el método foreach, que recibe una función:
val miArray = Array(1, 2, 3, 4, 5)
for (elemento <- miArray) {
println(elemento)
}
miArray.foreach(elemento => println(elemento))
miArray.foreach(println) // forma corta: pasa la función println directamente
Si también necesitás la posición:
val montos = Array(1500.0, 300.5, 2200.0)
for (i <- montos.indices) {
println(s"Posición $i: ${montos(i)}")
}
for ((monto, i) <- montos.zipWithIndex) {
println(s"Posición $i: $monto")
}
Transformar arrays
Las funciones de orden superior que viste en la lección 5 (map, filter, reduce) funcionan con arrays. No modifican el original: devuelven un array nuevo.
val numeros = Array(1, 2, 3, 4, 5)
val cuadrados = numeros.map(x => x * x) // Array(1, 4, 9, 16, 25)
val pares = numeros.filter(_ % 2 == 0) // Array(2, 4)
val total = numeros.reduce(_ + _) // 15
val dobles = for (n <- numeros) yield n * 2 // Array(2, 4, 6, 8, 10)
println(cuadrados.mkString(", "))
Algoritmos comunes
Los arrays traen incorporadas muchas operaciones habituales de búsqueda, ordenamiento y agregación:
val numeros = Array(5, 2, 8, 1, 3)
println(numeros.sorted.mkString(", ")) // 1, 2, 3, 5, 8
println(numeros.sortWith(_ > _).mkString(", ")) // 8, 5, 3, 2, 1
println(numeros.sum) // 19
println(numeros.max) // 8
println(numeros.min) // 1
println(numeros.contains(2)) // true
println(numeros.count(_ > 2)) // 3
println(numeros.reverse.mkString(", ")) // 3, 1, 8, 2, 5
Ninguna de estas operaciones modifica numeros: sorted y reverse devuelven arrays nuevos.
Descifrar Scaladoc
Scaladoc es la herramienta de documentación de Scala, equivalente a Javadoc. Se escribe con comentarios especiales arriba de cada clase o función:
/**
* Suma dos números enteros.
*
* @param a El primer número.
* @param b El segundo número.
* @return La suma de a y b.
*/
def suma(a: Int, b: Int): Int = a + b
A partir de esos comentarios se genera la documentación en HTML, como la de la librería estándar que vas a consultar seguido. Al principio las firmas de los métodos asustan un poco. Tomá esta, de Array.fill:
def fill[T](n: Int)(elem: => T)(implicit arg0: ClassTag[T]): Array[T]
Se lee así:
[T]: es genérico, funciona para cualquier tipoT.(n: Int): primera lista de parámetros, la cantidad de elementos.(elem: => T): segunda lista, el elemento. El=> Tsignifica que se evalúa cada vez que se necesita (un parámetro by-name), por esoArray.fill(3)(scala.util.Random.nextInt(10))genera tres números distintos.(implicit arg0: ClassTag[T]): un parámetro que completa el compilador solo. Lo podés ignorar.: Array[T]: devuelve un array deT.
Matrices multidimensionales
Una matriz es un array de arrays. La forma más directa de crearla es Array.ofDim:
val matriz = Array.ofDim[Int](3, 3) // 3 filas por 3 columnas, todo en 0
println("Matriz original:")
for (fila <- matriz) {
for (elemento <- fila) {
print(s"$elemento ")
}
println() // nueva línea después de cada fila
}
// Asignar valores: matriz(fila)(columna)
for (i <- 0 until 3) {
for (j <- 0 until 3) {
matriz(i)(j) = i * 3 + j + 1
}
}
println("\nMatriz con nuevos valores:")
for (fila <- matriz) {
println(fila.mkString(" "))
}
Matriz original:
0 0 0
0 0 0
0 0 0
Matriz con nuevos valores:
1 2 3
4 5 6
7 8 9
Se accede con dos pares de paréntesis: matriz(1)(2) es la fila 1, columna 2 (vale 6).
Interoperar con Java
Scala corre sobre la JVM, así que podés usar cualquier clase de Java directamente. Por ejemplo, el ArrayList de Java:
import java.util.ArrayList
val lista = new ArrayList[String]()
lista.add("Manzana")
lista.add("Plátano")
println(lista) // [Manzana, Plátano]
Las colecciones de Java no tienen los métodos de Scala (map, filter, etc.). Para convertirlas se usan los conversores de scala.jdk.CollectionConverters:
// continúa del bloque anterior
import scala.jdk.CollectionConverters._
val enScala = lista.asScala.toList // de Java a Scala
println(enScala.map(_.toUpperCase)) // List(MANZANA, PLÁTANO)
val deVuelta = List("a", "b").asJava // de Scala a Java
Practicá
Para estos ejercicios, representá cada cuenta con esta case class (las vas a ver en detalle en la lección 8):
import java.time.LocalDateTime
import scala.collection.mutable.ArrayBuffer
case class Transaccion(monto: Double, fecha: LocalDateTime = LocalDateTime.now())
case class CuentaBancaria(
numero: Int,
saldo: Double,
activa: Boolean = true,
transacciones: ArrayBuffer[Transaccion] = ArrayBuffer()
)
- Crear una lista de cuentas bancarias. Guardá varias cuentas en un
Arrayy agregá una más con:+=. - Agregar transacciones. Agregá transacciones a una cuenta usando su
ArrayBuffery+=. - Calcular el saldo total de todas las cuentas.
- Ordenar las cuentas por saldo, de forma ascendente y descendente.
- Encontrar las cuentas con saldo negativo usando
filter. - Eliminar las cuentas inactivas (con saldo igual a cero) usando
filterNot. - Agregar cuentas a un banco. Usá un
ArrayBufferde cuentas y agregá nuevas con+=. - Transferir fondos entre cuentas del array, usando
find(oindexWhere) para encontrarlas. - Validar datos de entrada. Usá
distinctpara verificar que los números de cuenta sean únicos.
Soluciones
import java.time.LocalDateTime
import scala.collection.mutable.ArrayBuffer
case class Transaccion(monto: Double, fecha: LocalDateTime = LocalDateTime.now())
case class CuentaBancaria(
numero: Int,
saldo: Double,
activa: Boolean = true,
transacciones: ArrayBuffer[Transaccion] = ArrayBuffer()
)
// 1. Crear una lista de cuentas
var cuentas: Array[CuentaBancaria] = Array(
CuentaBancaria(1, 1000.0),
CuentaBancaria(2, 500.0),
CuentaBancaria(3, 1500.0)
)
cuentas :+= CuentaBancaria(4, -200.0)
println(cuentas.map(c => s"${c.numero}: ${c.saldo}").mkString(" | "))
// 1: 1000.0 | 2: 500.0 | 3: 1500.0 | 4: -200.0
// 2. Agregar transacciones
val cuenta1 = cuentas(0)
cuenta1.transacciones += Transaccion(105.0)
cuenta1.transacciones += Transaccion(-40.0)
println(cuenta1.transacciones.map(_.monto)) // ArrayBuffer(105.0, -40.0)
// 3. Saldo total
val saldoTotal = cuentas.map(_.saldo).sum
println(saldoTotal) // 2800.0
// Con reduce: cuentas.map(_.saldo).reduce(_ + _)
// 4. Ordenar por saldo
val ascendente = cuentas.sortBy(_.saldo)
val descendente = cuentas.sortBy(_.saldo)(Ordering[Double].reverse)
println(ascendente.map(_.numero).mkString(", ")) // 4, 2, 1, 3
println(descendente.map(_.numero).mkString(", ")) // 3, 1, 2, 4
// 5. Saldos negativos
val negativas = cuentas.filter(_.saldo < 0)
println(negativas.map(_.numero).mkString(", ")) // 4
// 6. Eliminar inactivas
cuentas :+= CuentaBancaria(5, 0.0)
val activas = cuentas.filterNot(_.saldo == 0)
println(activas.map(_.numero).mkString(", ")) // 1, 2, 3, 4
// 7. Un banco con ArrayBuffer
val banco = ArrayBuffer(CuentaBancaria(10, 2000.0), CuentaBancaria(11, 0.0))
banco += CuentaBancaria(12, 3000.0)
println(banco.map(_.numero)) // ArrayBuffer(10, 11, 12)
// 8. Transferir fondos
def transferirFondos(origen: Int, destino: Int, monto: Double): Unit = {
val iOrigen = cuentas.indexWhere(_.numero == origen)
val iDestino = cuentas.indexWhere(_.numero == destino)
if (iOrigen == -1 || iDestino == -1) {
println("Una o ambas cuentas no existen.")
} else if (cuentas(iOrigen).saldo < monto) {
println("Saldo insuficiente en la cuenta de origen.")
} else {
cuentas(iOrigen) = cuentas(iOrigen).copy(saldo = cuentas(iOrigen).saldo - monto)
cuentas(iDestino) = cuentas(iDestino).copy(saldo = cuentas(iDestino).saldo + monto)
println(s"Transferencia exitosa de $monto desde la cuenta $origen a la cuenta $destino.")
}
}
transferirFondos(1, 2, 200.0)
transferirFondos(2, 9, 50.0)
println(cuentas.map(c => s"${c.numero}: ${c.saldo}").mkString(" | "))
// 1: 800.0 | 2: 700.0 | 3: 1500.0 | 4: -200.0 | 5: 0.0
// 9. Números de cuenta únicos
val numeros = cuentas.map(_.numero)
val sonUnicos = numeros.distinct.length == numeros.length
println(s"¿Números únicos? $sonUnicos") // true
Un error común en el ejercicio 8: la case class es inmutable, así que cuenta.copy(saldo = ...) no cambia la cuenta; devuelve una cuenta nueva con el saldo modificado. Si no guardás el resultado en algún lado, la transferencia "se hace" y se pierde. Por eso la solución busca la posición de cada cuenta con indexWhere y reemplaza el elemento del array: cuentas(i) = cuentas(i).copy(...).
En el ejercicio 4, sorted solo sirve para tipos que Scala sabe ordenar (números, strings). Para ordenar objetos por uno de sus campos se usa sortBy; y para invertir el orden, se le pasa Ordering[Double].reverse.
Resumen
Arraytiene tamaño fijo, se accede conarr(i)y sus elementos se pueden modificar. Para verlo, usámkString.ArrayBuffercrece y se achica con+=,-=,++=,insertyremove.Array.ofDim,range,concat,fill,iterateycopyson las formas más comunes de crear y copiar arrays;:+y++devuelven arrays nuevos.foryforeachrecorren;map,filter,reduce,sorted,sortBy,sumycontainstransforman y consultan sin modificar el original.- Una matriz es un array de arrays:
Array.ofDim[Int](3, 3)y se accede conm(i)(j). - Scaladoc documenta el código; leer firmas como
fill[T](n: Int)(elem: => T)es una habilidad que se gana con práctica. - Podés usar clases de Java directamente y convertir colecciones con
asScalayasJava.