Comportamiento de los Operadores == y === en Kotlin

Diferenciación Semántica entre Igualdad e Identidad

En Kotlin, comprender la distinción técnica entre comparar valores reales y comparar referencias de memoria es fundamental. El símbolo == ejecuta una comprobación de contenido mediante el método equals(), mientras que === verifica si dos operandos son físicamente la misma instancia en el heap.

A continuación, se presenten ejemplos ilustrativos que demuestran cómo el tipo de datos y el autoboxing afectan este comportamiento.

Ejemplo 1: Tipos Primitivos No Null

fun testEqualities() {
    val num : Int = 1000
    // Compara valores lógicos
    println(num == num)       // Resultado: true
    // Compara referencia exacta
    println(num === num)      // Resultado: true
    
    val copia1 : Int = num
    val copia2 : Int = num
    println(copia1 == copia2) // Resultado: true
    println(copia1 === copia2)// Resultado: true
}

Aunque copia1 y copia2 son distintas variables locales, al tratarse de tipos primitivos que Kotlin maneja como valores directos en ciertos contextos compilados, o simplemente por optimización de la VM en esta asignación inmediata, el resultado suele ser coincidencia total tanto en valor como en referencia lógica.

Ejemplo 2: Variables Nulables (Int?)

fun testNullableEquality() {
    val numBase : Int = 1000
    // Al convertir explícitamente a un objeto Nullable
    val refA : Int? = numBase 
    val refB : Int? = numBase 
    
    println(refA == refB)     // Resultado: true (Valores iguales)
    println(refA === refB)    // Resultado: false (Objetos distintos en memoria)
}

Cuando se utiliza Int?, se fuerza la creación de un objeto de la clase Wrapper. Aunque ambos contienen el número 1000, refA y refB generan intsancias separadas en la pila de llamadas, lo que provoca que la comparación de identidad (===) falle.

Ejemplo 3: Propagación de Referencia Única

fun testSharedReference() {
    val obj : Int? = 1000
    val destino1 : Int? = obj
    val destino2 : Int? = obj
    
    println(destino1 == destino2)  // Resultado: true
    println(destino1 === destino2) // Resultado: true
}

La lógica cambia si la fuente ya es un objeto nullable. Al asignar obj a destino1 y destino2, ambas variables apuntan exactamente a la misma instancia del objeto Int. Por tanto, su identidad coincide plenamente.

Ejemplo 4: Caché de Enteros Pequeños

fun testCachedIntegers() {
    val corto : Int = 100
    val nuloCorto1 : Int? = corto
    val nuloCorto2 : Int? = corto
    
    println(nuloCorto1 === nuloCorto2) // Resultado: true
}

Kotlin, heredando comportamientos similraes a Java, optimiza la creación de objetos para enteros dentro del rango de \[-128, 127\]. En este intervalo, las cajas (boxed instances) se reutilizan desde una caché interna. Si se intenta con un valor fuera de este rango, como 1000, la comparación de identidad fallará nuevamente, generando nuevos objetos en memoria.

Etiquetas: Kotlin identidad-de-referencia Autoboxing tipos-primitivos seguridad-de-null

Publicado el 10-13 23:17