41 - Sobrecarga de operadores

El lenguaje Kotlin permite que ciertos operadores puedan sobrecargarse y actúen de diferentes maneras según al objeto que se aplique.

La sobrecarga de operadores debe utilizarse siempre y cuando tenga sentido para la clase que se está implementando. Los conceptos matemáticos de vectores y matrices son casos donde la sobrecarga de operadores nos puede hacer nuestro código más legible y elegante.

Para sobrecargar los operadores +, -, * y / debemos implementar una serie de métodos especiales dentro de la clase:

Operación    Nombre del método a definir
a + b        plus
a - b        minus
a * b        times
a / b        div
a % b        rem
a..b         rangeTo

Problema 1

Declarar una clase llamada Vector que administre un array de 5 elementos de tipo entero y cargar valores aleatorios entre 1 y 10. Sobrecargar los operadores +, -, * y /
En la main definir una serie de objetos de la clase Vector y operar con ellos

Proyecto162 - Principal.kt

class Vector {
    val arreglo = IntArray(5)

    fun cargar() {
        for(i in arreglo.indices)
            arreglo[i] = (Math.random() * 11 + 1).toInt()
    }

    fun imprimir() {
        for(elemento in arreglo)
            print("$elemento ")
        println()
    }
    operator fun plus(vector2: Vector): Vector {
        var suma = Vector()
        for(i in arreglo.indices)
            suma.arreglo[i] = arreglo[i] + vector2.arreglo[i]
        return suma
    }

    operator fun minus(vector2: Vector): Vector {
        var resta = Vector()
        for(i in arreglo.indices)
            resta.arreglo[i] = arreglo[i] - vector2.arreglo[i]
        return resta
    }

    operator fun times(vector2: Vector): Vector {
        var producto = Vector()
        for(i in arreglo.indices)
            producto.arreglo[i] = arreglo[i] * vector2.arreglo[i]
        return producto
    }

    operator fun div(vector2: Vector): Vector {
        var division = Vector()
        for(i in arreglo.indices)
            division.arreglo[i] = arreglo[i] / vector2.arreglo[i]
        return division
    }
}

fun main(args: Array<String>) {
    val vec1 = Vector()
    vec1.cargar()
    val vec2 = Vector()
    vec2.cargar()
    vec1.imprimir()
    vec2.imprimir()
    val vecSuma = vec1 + vec2
    println("Suma componente a componente de los dos vectores")
    vecSuma.imprimir()
    val vecResta = vec1 - vec2
    println("La resta componente a componente de los dos vectores")
    vecResta.imprimir()
    val vecProducto = vec1 * vec2
    println("El producto componente a componente de los dos vectores")
    vecProducto.imprimir()
    val vecDivision = vec1 / vec2
    println("La división componente a componente de los dos vectores")
    vecDivision.imprimir()
}

Para sobrecargar el operador + debemos implementar el método plus que reciba un único parámetro, en este caso de tipo Vector y retorne otro objeto de la clase Vector.

Debemos anteceder a la palabra clave fun la palabra clave operator.
Dentro del método creamos un objeto de la clase Vector y procedemos a inicializar su propiedad arreglo con la suma componente a componente de los dos arreglos de cada objeto:

    operator fun plus(vector2: Vector): Vector {
        var suma = Vector()
        for(i in arreglo.indices)
            suma.arreglo[i] = arreglo[i] + vector2.arreglo[i]
        return suma
    }

Para llamar a este método en la main utilizamos el operador + :

    val vecSuma = vec1 + vec2

La otra sintaxis que podemos utilizar es la ya conocida de invocar el método:

    val vecSuma = vec1.plus(vec2)

Con esto podemos comprobar que el programa queda más legible utilizando el operador +.

Pensemos que si tenemos que sumar 4 vectores la sintaxis sería:

    val vecSuma = vec1 + vec2 + vec3 + vec4

En algoritmos complejos puede simplificar mucho nuestro código el uso correcto de la sobrecarga de operadores.

Podemos sobrecargar un operador con objetos de distinta clase.

Problema 2

Declarar una clase llamada Vector que administre un array de 5 elementos de tipo entero y cargar valores aleatorios entre 1 y 10. Sobrecargar el operador * que permita multiplicar un Vector con un número entero (se debe multiplicar cada componente del arreglo por el entero)

Proyecto163 - Principal.kt

class Vector {
    val arreglo = IntArray(5)

    fun cargar() {
        for(i in arreglo.indices)
            arreglo[i] = (Math.random() * 11 + 1).toInt()
    }

    fun imprimir() {
        for(elemento in arreglo)
            print("$elemento ")
        println()
    }

    operator fun times(valor: Int): Vector {
        var suma = Vector()
        for(i in arreglo.indices)
            suma.arreglo[i] = arreglo[i] * valor
        return suma
    }
}

fun main(args: Array<String>) {
    val vec1 = Vector()
    vec1.cargar()
    vec1.imprimir()
    println("El producto de un vector con el número 10 es")
    val vecProductoEnt = vec1 * 10
    vecProductoEnt.imprimir()
}

En este problema para sobrecargar el operador * de un Vector por un tipo entero al método debe llegar un tipo de dato Int:

    operator fun times(valor: Int): Vector {
        var suma = Vector()
        for(i in arreglo.indices)
            suma.arreglo[i] = arreglo[i] * valor
        return suma
    }

En la función main cuando procedemos a multiplicar un objeto de la clase Vector por un tipo de dato Int debemos hacerlo en este orden:

    println("El producto de un vector con el número 10 es")
    val vecProductoEnt = vec1 * 10
    vecProductoEnt.imprimir()

Si invertimos el producto, es decir un Int por un Vector debemos definir el método times en la clase Int. El programa completo luego quedaría:

class Vector {
    val arreglo = IntArray(5)

    fun cargar() {
        for(i in arreglo.indices)
            arreglo[i] = (Math.random() * 11 + 1).toInt()
    }

    fun imprimir() {
        for(elemento in arreglo)
            print("$elemento ")
        println()
    }

    operator fun times(valor: Int): Vector {
        var suma = Vector()
        for(i in arreglo.indices)
            suma.arreglo[i] = arreglo[i] * valor
        return suma
    }
}

operator fun Int.times(vec: Vector): Vector {
    var suma = Vector()
    for(i in vec.arreglo.indices)
        suma.arreglo[i] = vec.arreglo[i] * this
    return suma
}

fun main(args: Array<String>) {
    val vec1 = Vector()
    vec1.cargar()
    vec1.imprimir()
    println("El producto de un vector con el número 10 es")
    val vecProductoEnt = 10 * vec1
    vecProductoEnt.imprimir()
    println("El producto de un entero 5 por un vector es")
    val vecProductoEnt2 = vec1 * 5
    vecProductoEnt2.imprimir()

}

Utilizamos el concepto de funciones de extensión que vimos anteriormente:

operator fun Int.times(vec: Vector): Vector {
    var suma = Vector()
    for(i in vec.arreglo.indices)
        suma.arreglo[i] = vec.arreglo[i] * this
    return suma
}

Problema 3

Declarar una clase llamada Vector que administre un array de 5 elementos de tipo entero y cargar valores aleatorios entre 1 y 10. Sobrecargar los operadores ++ y -- (se debe incrementar o disminuir en uno cada elemento del arreglo)

Proyecto164 - Principal.kt

class Vector {
    val arreglo = IntArray(5)

    fun cargar() {
        for(i in arreglo.indices)
            arreglo[i] = (Math.random() * 11 + 1).toInt()
    }

    fun imprimir() {
        for(elemento in arreglo)
            print("$elemento ")
        println()
    }

    operator fun inc(): Vector {
        var suma1 = Vector()
        for(i in arreglo.indices)
            suma1.arreglo[i] = arreglo[i] + 1
        return suma1
    }

    operator fun dec(): Vector {
        var resta1 = Vector()
        for(i in arreglo.indices)
            resta1.arreglo[i] = arreglo[i] - 1
        return resta1
    }
}


fun main(args: Array<String>) {
    var vec1 = Vector()
    vec1.cargar()
    println("Vector original")
    vec1.imprimir()
    vec1++
    println("Luego de llamar al operador ++")
    vec1.imprimir()
    vec1--
    println("Luego de llamar al operador --")
    vec1.imprimir()
}

Cuando queremos sobrecargar los operadores ++ y -- debemos implementar los métodos inc y dec. Estos métodos no reciben ningún parámetro y retornan objetos de la clase Vector incrementando en uno o disminuyendo en uno cada componente del arreglo:

    operator fun inc(): Vector {
        var suma1 = Vector()
        for(i in arreglo.indices)
            suma1.arreglo[i] = arreglo[i] + 1
        return suma1
    }

    operator fun dec(): Vector {
        var resta1 = Vector()
        for(i in arreglo.indices)
            resta1.arreglo[i] = arreglo[i] - 1
        return resta1
    }

Cuando llamamos a los operadores ++ y -- el objeto que devuelve se asigna a la variable por la que llamamos, esto hace necesario definir el objeto de la clase Vector con la palabra clave var:

    var vec1 = Vector()
    vec1.cargar()
    println("Vector original")
    vec1.imprimir()

Una vez llamado al operador ++:

    vec1++

Ahora vec1 tiene la referencia al objeto que se creó en el método inc:

    println("Luego de llamar al operador ++")
    vec1.imprimir()

No hay que hacer cambios si necesitamos utilizar notación prefija con los operadores ++ y --:

    ++vec1
    --vec1

Sobrecarga de operadores > >= y < <=

Para sobrecargar estos operadores debemos implementar el método compareTo.

Problema 4

Implementar una clase llamada Persona que tendrá como propiedades su nombre y edad. Sobrecargar los operadores > >= y < <= .

Proyecto165 - Principal.kt

class Persona (val nombre: String, val edad: Int) {

    fun imprimir() {
        println("Nombre: $nombre y tiene una edad de $edad")
    }

    operator fun compareTo(per2: Persona): Int {
        return when {
            edad < per2.edad -> -1
            edad > per2.edad -> 1
            else -> 0
        }
    }
}

fun main(parametro: Array<String>) {
    val persona1 = Persona("Juan", 30)
    persona1.imprimir()
    val persona2 = Persona("Ana", 30)
    persona2.imprimir()
    println("Persona mayor")
    when {
        persona1 > persona2 -> persona1.imprimir()
        persona1 < persona2 -> persona2.imprimir()
        else -> println("Tienen la misma edad")
    }
}

El método compareTo debe retornar un Int, indicando que si devuelve un 0 las dos personas tienen la misma edad. Si retornar un 1 la persona que está a la izquierda del operador es mayor de edad y viceversa.:

    operator fun compareTo(per2: Persona): Int {
        return when {
            edad < per2.edad -> -1
            edad > per2.edad -> 1
            else -> 0
        }
    }

Luego podemos preguntar con estos operadores para objetos de la clase Persona cual tiene una edad mayor:

    when {
        persona1 > persona2 -> persona1.imprimir()
        persona1 < persona2 -> persona2.imprimir()
        else -> println("Tienen la misma edad")
    }

Sobrecarga de operadores de subíndices

En Kotlin podemos sobrecargar el manejo de subíndices implementando los métodos get y set.

la expresión              se traduce
a[i] = b	          a.set(i, b)
a[i, j] = b	          a.set(i, j, b)
a[i_1, ..., i_n] = b	  a.set(i_1, ..., i_n, b)

a[i]	                  a.get(i)
a[i, j]	                  a.get(i, j)
a[i_1, ..., i_n]	  a.get(i_1, ..., i_n)

Problema 5

Implementar una clase TaTeTi que defina una propiedad para el tablero que sea un IntArray de 9 elementos con valor cero.
Hay dos jugadores que disponen fichas, el primer jugador carga el 1 y el segundo carga un 2.
Mediante sobrecarga de operadores de subíndices permitir asignar la fichas a cada posición del tablero mediante dos subíndices que indiquen la fila y columna del tablero.

Proyecto166 - Principal.kt

class TaTeTi {
    val tablero = IntArray(9)

    fun imprimir() {
        for(fila in 0..2) {
            for (columna in 0..2)
                print("${this[fila, columna]} ")
            println()
        }
        println()
    }

    operator fun set(fila: Int, columna: Int, valor: Int){
        tablero[fila*3 + columna] = valor
        imprimir()
    }

    operator fun get(fila: Int, columna: Int): Int{
        return tablero[fila*3 + columna]
    }
}

fun main(args: Array<String>) {
    val juego = TaTeTi()
    juego[0, 0] = 1
    juego[0, 2] = 2
    juego[2, 0] = 1
    juego[1, 2] = 2
    juego[1, 0] = 1
}

La propiedad tablero almacena las nueve casillas del juego de Taterí en un arreglo. Para que sea más intuitivo la carga de fichas en el juego de tateti procedemos a sobrecargar el operador de subíndices con fila y columna.

La idea es que podamos indicar una fila, columna y la ficha que se carga con la sintaxis:

    juego[0, 0] = 1
    juego[0, 2] = 2
    juego[2, 0] = 1
    juego[1, 2] = 2
    juego[1, 0] = 1

Cada asignación en realidad llama al método set, que además de cargar la ficha pasa a imprimir el tablero:

    operator fun set(fila: Int, columna: Int, valor: Int){
        tablero[fila*3 + columna] = valor
        imprimir()
    }

La impresión del tablero procede a acceder mediante this a la sobrecarga del operador de subíndices accediendo al método get:

    fun imprimir() {
        for(fila in 0..2) {
            for (columna in 0..2)
                print("${this[fila, columna]} ")
            println()
        }
        println()
    }

La sobrecarga para recuperar el valor almacenado se hace implementando el método get:

    operator fun get(fila: Int, columna: Int): Int{
        return tablero[fila*3 + columna]
    }

Recordar que la sobrecarga de operadores tiene por objetivo hacer más legible nuestro programa, acá lo logramos porque es muy fácil entender como cargamos las fichas en el tablero con la asignación:

    val juego = TaTeTi()
    juego[0, 0] = 1

Tengamos siempre en cuenta que cuando se sobrecarga un operador en realidad se está llamando un método de la clase.

La ejecución de este programa nos muestra en pantalla los estados sucesivos del tablero de Tatetí a medida que le asignamos piezas:

sobrecarga operador subindices Kotlin

Sobrecarga de paréntesis

En Kotlin también podemos sobrecargar los paréntesis implementando el método invoke.

la expresión            se traduce
a()                     a.invoke()
a(i)                    a.invoke(i)
a(i, j)	                a.invoke(i, j)
a(i_1, ..., i_n)        a.invoke(i_1, ..., i_n)

Problema 6

Plantear una clase Dados que administre 10 valores de dados en un arreglo de tipo IntArray.
Sobrecargar el operador de paréntesis para la clase y acceder mediante una posición al valor de un dado específico.

Proyecto167 - Principal.kt

class Dados (){

    val arreglo = IntArray(10)

    fun tirar() {
        for(i in arreglo.indices)
            arreglo[i] = ((Math.random() * 6) + 1).toInt()
    }

    operator fun invoke(nro: Int) = arreglo[nro]
}

fun main(args: Array<String>) {
    var dados = Dados()
    dados.tirar()
    println(dados(0))
    println(dados(1))
    for(i in 2..9)
        println(dados(i))
}

Declaramos un arreglo de 10 enteros y guardamos valores aleatorios entre 1 y 6:

class Dados (){

    val arreglo = IntArray(10)

    fun tirar() {
        for(i in arreglo.indices)
            arreglo[i] = ((Math.random() * 6) + 1).toInt()
    }

En la función main creamos el objeto de la clase Dados y llamamos al método tirar:

    var dados = Dados()
    dados.tirar()

Luego si disponemos el nombre del objeto y entre paréntesis pasamos un entero se llamará al método invoke y retornará en este caso un entero que representa el valor del dado para dicha posición:

    println(dados(0))
    println(dados(1))
    for(i in 2..9)
        println(dados(i))

En la clase Dados especificamos la sobrecarga del operador de paréntesis con la implementación del método invoke:

    operator fun invoke(nro: Int) = arreglo[nro]

Para este problema podíamos también haber sobrecargado el operador de corchetes como vimos anteriormente.

La elección de que operador sobrecargar dependerá del problema a desarrollar y ver con cual queda más claro su empleo.

Sobrecarga de operadores += -= *= /= %=

Los métodos que se invocan para estos operadores son:

la expresión       se traduce
a += b	           a.plusAssign(b)
a -= b	           a.minusAssign(b)
a *= b	           a.timesAssign(b)
a /= b	           a.divAssign(b)
a %= b	           a.modAssign(b)

Problema 7

Declarar una clase llamada Vector que administre un array de 5 elementos de tipo entero.
Sobrecargar el operador +=
En la main definir una serie de objetos de la clase y emplear el operador +=

Proyecto168 - Principal.kt

class Vector {
    val arreglo = IntArray(5, {it})

    fun imprimir() {
        for (elemento in arreglo)
            print("$elemento ")
        println()
    }

    operator fun plusAssign(vec2: Vector) {

        for(i in arreglo.indices)
            arreglo[i] += vec2.arreglo[i]
    }
}

fun main(args: Array<String>) {
    val vec1 = Vector()
    vec1.imprimir()
    val vec2 = Vector()
    vec2.imprimir()
    vec1 += vec2
    vec1.imprimir()
}

Para sobrecargar el operador += en la clase Vector definimos el método plusAssign:

    operator fun plusAssign(vec2: Vector) {

        for(i in arreglo.indices)
            arreglo[i] += vec2.arreglo[i]
    }

Al método llega un objeto de la clase Vector que nos sirve para acceder al arreglo contenido en el mismo.

En la función main definimos dos objetos de la clase Vector y procedemos a acumular en vec1 el contenido de vec2 mediante el operador +=:

fun main(args: Array<String>) {
    val vec1 = Vector()
    vec1.imprimir()
    val vec2 = Vector()
    vec2.imprimir()
    vec1 += vec2
    vec1.imprimir()
}

Sobrecarga de operadores in y !in

Los métodos que se invocan para estos operadores son:

la expresión       se traduce
a in b             b.contains(a)
a !in b           !b.contains(a)

Problema 8

Plantear un data class Alumno que almacene su número de documento y su nombre.
Luego en una clase Curso definir un Array con 3 objetos de la clase Alumno. Sobrecargar el operador in para verificar si un número de documento se encuentra inscripto en el curso.

Proyecto169 - Principal.kt

data class Alumno(val documento: Int, val nombre: String)

class Curso {
    val alumnos = arrayOf(Alumno(123456, "Marcos"), 
                          Alumno(666666, "Ana"), 
                          Alumno(777777, "Luis"))

    operator fun contains(documento: Int): Boolean {
        return alumnos.any {documento == it.documento}
    }
}

fun main(args: Array<String>) {
    val curso1 = Curso()
    if (123456 in curso1)
        println("El alumno Marcos está inscripto en el cuso")
    else
        println("El alumno Marcos no está inscripto en el cuso")
}

Declaramos un data class que representa un Alumno:

data class Alumno(val documento: Int, val nombre: String)

Declaramos la clase Cursos definiendo un Array con cuatro alumnos:

class Curso {
    val alumnos = arrayOf(Alumno(123456, "Marcos"), Alumno(666666, "Ana"), Alumno(777777, "Luis"))

Sobrecargamos el operador in:

    operator fun contains(documento: Int): Boolean {
        return alumnos.any {documento == it.documento}
    }

Llega al método un entero que representa el número de documento a buscar dentro del arreglo de alumnos. En el caso que se encuentre retornamos un true, en caso negativo retornamos un falso.

Podemos escribir en forma más conciso el método contains:

    operator fun contains(documento: Int) = alumnos.any {documento == it.documento}

En la main creamos un objeto de la clase curso y luego mediante el operador in podemos verificar si un determinado número de documento se encuentra inscripto en el curso de alumnos:

fun main(args: Array<String>) {
    val curso1 = Curso()
    if (123456 in curso1)
        println("El alumno Marcos está inscripto en el cuso")
    else
        println("El alumno Marcos no está inscripto en el cuso")
}