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.