Introducción
Una de las características más elogiadas de Kotlin es su sistema de seguridad de nulos que elimina el error de mil millones de dólares de las excepciones de puntero nulo en tiempo de compilación.
Tipos Nullable vs No-Nullable
// No-nullable (no puede ser null)
var nombre: String = "Alicia"
// nombre = null // ¡Error de compilación!
// Nullable (puede ser null)
var nombreNullable: String? = "Bob"
nombreNullable = null // OK
// Inferencia de tipo
val saludo = "Hola" // String (no-nullable)
val opcional: String? = null // String? (nullable)
Operador de Llamada Segura (?.)
val nombre: String? = null
// Llamada segura - retorna null si el receptor es null
val longitud = nombre?.length // null
val mayus = nombre?.uppercase() // null
// Encadenar llamadas seguras
val pais: String? = usuario?.direccion?.pais
Operador Elvis (?:)
Proporcionar valores por defecto para casos null:
val nombre: String? = null
// Forma tradicional
val nombreMostrado = if (nombre != null) nombre else "Desconocido"
// Operador Elvis
val nombreMostrado = nombre ?: "Desconocido"
// Con llamada segura
val longitud = nombre?.length ?: 0
// Retorno anticipado
fun procesarNombre(nombre: String?): String {
val nombreSeguro = nombre ?: return "Se requiere nombre"
return nombreSeguro.uppercase()
}
Aserción No-Nula (!!)
Convertir nullable a no-nullable (¡usar con cuidado!):
val nombre: String? = "Alicia"
// Esto lanzará NullPointerException si nombre es null
val longitud = nombre!!.length
// Solo usar cuando estés absolutamente seguro de que no es null
val config = cargarConfig() // Retorna Config?
val host = config!!.host // Mejor usar llamada segura o elvis
Conversiones Seguras (as?)
val cualquiera: Any = "Hola"
// Conversión insegura (lanza ClassCastException si falla)
// val str: String = cualquiera as String
// Conversión segura (retorna null si falla)
val str: String? = cualquiera as? String // "Hola"
val num: Int? = cualquiera as? Int // null
Función Let
Ejecutar código solo si no es null:
val nombre: String? = "Alicia"
// Forma antigua
if (nombre != null) {
println(nombre.length)
println(nombre.uppercase())
}
// Con let
nombre?.let {
println(it.length)
println(it.uppercase())
}
// Ejemplo del mundo real
usuario?.email?.let { email ->
enviarEmailA(email)
}
Función Also
Realizar efectos secundarios:
val numeros = mutableListOf(1, 2, 3)
.also { println("Original: $it") }
.also { it.add(4) }
.also { println("Modificado: $it") }
Función Run
Ejecutar bloque y retornar resultado:
val resultado = run {
val nombre: String? = obtenerNombre()
nombre?.uppercase() ?: "DESCONOCIDO"
}
Lateinit
Declarar propiedad no-nula que se inicializará después:
class MiActividad {
// Se inicializará antes del primer uso
lateinit var presentador: Presentador
fun onCreate() {
presentador = Presentador()
}
fun verificarInit() {
if (::presentador.isInitialized) {
presentador.iniciar()
}
}
}
Inicialización Perezosa
Inicializar propiedad en el primer acceso:
class GestorDatos {
// Se inicializa solo cuando se accede por primera vez
val baseDatos: BaseDatos by lazy {
println("Creando base de datos")
BaseDatos.conectar()
}
val config: Config by lazy {
cargarConfig()
}
}
Tipos de Plataforma
Al llamar a Java desde Kotlin:
// Método Java: String getName()
// Kotlin lo ve como String!
val nombre = objetoJava.name // String! (tipo de plataforma)
// Mejor: declarar explícitamente
val nombre: String = objetoJava.name // No-nulo
val nombreNullable: String? = objetoJava.name // Nullable
Buenas Prácticas
// ✅ Bueno: Usar llamadas seguras
fun obtenerLongitud(texto: String?): Int {
return texto?.length ?: 0
}
// ❌ Malo: Abusar de !!
fun obtenerLongitud(texto: String?): Int {
return texto!!.length // ¡Puede fallar!
}
// ✅ Bueno: Usar let para verificaciones de null
usuario?.email?.let { enviarEmail(it) }
// ❌ Malo: Verificación tradicional de null
if (usuario != null && usuario.email != null) {
enviarEmail(usuario.email)
}
// ✅ Bueno: Retorno anticipado con Elvis
fun procesar(datos: String?): Resultado {
val datosSeguro = datos ?: return Resultado.Error("Sin datos")
return Resultado.Exito(datosSeguro)
}
Ejemplos Prácticos
Validación de usuario:
data class Usuario(val nombre: String?, val email: String?, val edad: Int?)
fun validarUsuario(usuario: Usuario?): String {
val nombre = usuario?.nombre ?: return "Usuario es null"
val email = usuario.email ?: return "Email requerido"
val edad = usuario.edad ?: return "Edad requerida"
if (edad < 18) return "Debe ser mayor de 18"
if (!email.contains("@")) return "Email inválido"
return "Usuario válido: $nombre"
}
Carga segura de configuración:
class GestorConfig {
private var config: Config? = null
fun cargar() {
config = try {
cargarDeArchivo()
} catch (e: Exception) {
null
}
}
fun obtenerHost(): String = config?.host ?: "localhost"
fun obtenerPuerto(): Int = config?.puerto ?: 8080
fun estaCargado(): Boolean = config != null
}
Procesamiento de cadena opcional:
data class Direccion(val calle: String?, val ciudad: String?, val pais: String?)
data class Usuario(val nombre: String?, val direccion: Direccion?)
fun obtenerUbicacionUsuario(usuario: Usuario?): String {
return usuario?.direccion?.ciudad?.let { ciudad ->
usuario.direccion.pais?.let { pais ->
"$ciudad, $pais"
}
} ?: "Ubicación desconocida"
}
// O más elegantemente
fun obtenerUbicacionUsuario(usuario: Usuario?): String {
val ciudad = usuario?.direccion?.ciudad
val pais = usuario?.direccion?.pais
return if (ciudad != null && pais != null) {
"$ciudad, $pais"
} else {
"Ubicación desconocida"
}
}
Patrones Comunes
Operaciones seguras con colecciones:
val nombres: List<String?> = listOf("Alicia", null, "Bob", null, "Carlos")
// Filtrar valores no-nulos
val nombresNoNulos = nombres.filterNotNull() // [Alicia, Bob, Carlos]
// Mapear con llamadas seguras
val longitudes = nombres.mapNotNull { it?.length } // [6, 3, 6]
Múltiples verificaciones de null:
// En lugar de ifs anidados
if (a != null) {
if (b != null) {
if (c != null) {
hacerAlgo(a, b, c)
}
}
}
// Usar let con múltiples parámetros
a?.let { seguroA ->
b?.let { seguroB ->
c?.let { seguroC ->
hacerAlgo(seguroA, seguroB, seguroC)
}
}
}
// O verificar todos a la vez
if (a != null && b != null && c != null) {
hacerAlgo(a, b, c)
}
Conclusión
El sistema de seguridad de nulos de Kotlin previene la mayoría de excepciones de puntero nulo en tiempo de compilación. Al comprender los tipos nullable, las llamadas seguras y el operador Elvis, puedes escribir código más seguro y expresivo.
En el próximo capítulo, exploraremos clases y data classes en Kotlin.