trubidata
Lección 6 de 14·20 min

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.

scala
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étodo apply que viste en la lección 2.
  • Aunque miArray sea un val, sus elementos se pueden modificar. El val impide reasignar la variable a otro array, no cambiar el contenido. El Array es 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:

scala
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

scala
// 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:

  1. apply(x: T, xs: T*): Array[T] crea un array con los elementos que le pases.

    scala
    val array = Array(1, 2, 3) // equivale a Array.apply(1, 2, 3)
    
  2. concat[T](xss: Array[T]*): Array[T] concatena varios arrays en uno.

    scala
    val array1 = Array(1, 2, 3)
    val array2 = Array(4, 5, 6)
    val concatenado = Array.concat(array1, array2) // Array(1, 2, 3, 4, 5, 6)
    
  3. copy(src, srcPos, dest, destPos, length): Unit copia elementos de un array a otro, como System.arraycopy en Java.

    scala
    val origen = Array(1, 2, 3)
    val destino = new Array[Int](3)
    Array.copy(origen, 0, destino, 0, 3)
    println(destino.mkString(", ")) // 1, 2, 3
    
  4. empty[T]: Array[T] devuelve un array de longitud 0.

    scala
    val vacio = Array.empty[Int]
    
  5. iterate[T](start: T, len: Int)(f: T => T): Array[T] aplica una función repetidamente a partir de un valor inicial.

    scala
    val potencias = Array.iterate(1, 5)(_ * 2) // Array(1, 2, 4, 8, 16)
    
  6. fill[T](n: Int)(elem: => T): Array[T] crea un array repitiendo un valor.

    scala
    val saludos = Array.fill(4)("Hola") // Array(Hola, Hola, Hola, Hola)
    
  7. fill[T](n1: Int, n2: Int)(elem: => T): Array[Array[T]] crea un array bidimensional.

    scala
    val 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:

scala
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:

scala
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.

scala
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:

scala
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:

scala
/**
 * 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:

text
def fill[T](n: Int)(elem: => T)(implicit arg0: ClassTag[T]): Array[T]

Se lee así:

  • [T]: es genérico, funciona para cualquier tipo T.
  • (n: Int): primera lista de parámetros, la cantidad de elementos.
  • (elem: => T): segunda lista, el elemento. El => T significa que se evalúa cada vez que se necesita (un parámetro by-name), por eso Array.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 de T.

Matrices multidimensionales

Una matriz es un array de arrays. La forma más directa de crearla es Array.ofDim:

scala
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(" "))
}
text
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:

scala
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:

scala
// 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):

scala
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 bancarias. Guardá varias cuentas en un Array y agregá una más con :+=.
  2. Agregar transacciones. Agregá transacciones a una cuenta usando su ArrayBuffer y +=.
  3. Calcular el saldo total de todas las cuentas.
  4. Ordenar las cuentas por saldo, de forma ascendente y descendente.
  5. Encontrar las cuentas con saldo negativo usando filter.
  6. Eliminar las cuentas inactivas (con saldo igual a cero) usando filterNot.
  7. Agregar cuentas a un banco. Usá un ArrayBuffer de cuentas y agregá nuevas con +=.
  8. Transferir fondos entre cuentas del array, usando find (o indexWhere) para encontrarlas.
  9. Validar datos de entrada. Usá distinct para verificar que los números de cuenta sean únicos.

Soluciones

scala
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

  • Array tiene tamaño fijo, se accede con arr(i) y sus elementos se pueden modificar. Para verlo, usá mkString.
  • ArrayBuffer crece y se achica con +=, -=, ++=, insert y remove.
  • Array.ofDim, range, concat, fill, iterate y copy son las formas más comunes de crear y copiar arrays; :+ y ++ devuelven arrays nuevos.
  • for y foreach recorren; map, filter, reduce, sorted, sortBy, sum y contains transforman y consultan sin modificar el original.
  • Una matriz es un array de arrays: Array.ofDim[Int](3, 3) y se accede con m(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 asScala y asJava.

← Volver al temario