Segfault: Periplos de un Desarrollador

Re Aprendiendo Java Después De 10 Años Superando Los Sesgos Del Pasado

Carlos Ortiz

Abstract

Este artículo analiza cómo una década de trabajo continuo en Java puede llevar a la formación de sesgos que limitan la adopción de nuevas funcionalidades del lenguaje. Se examinan ejemplos específicos de prácticas anticuadas y se proponen estrategias para superar estos sesgos, promoviendo un enfoque de reaprendizaje que maximiza la eficiencia y modernidad en el desarrollo de software.

He trabajado con Java de manera continua durante al menos la última década, y en todo ese tiempo, el lenguaje ha sido mi herramienta de elección para construir aplicaciones robustas y escalables. Sin embargo, después de tantos años de uso, es natural que se formen ciertos sesgos, aquellos hábitos y patrones que seguimos por familiaridad y comodidad. Ahora, me encuentro en un punto donde quiero reaprender Java para desafiar estos sesgos y asegurarme de que estoy aprovechando todo lo que el lenguaje tiene para ofrecer en su estado actual.

¿Qué son los sesgos existentes?

Los sesgos existentes son aquellas preferencias y prácticas que desarrollamos con el tiempo, a menudo sin darnos cuenta. En el contexto de Java, estos sesgos pueden incluir:

Apego a prácticas antiguas

Uno de los sesgos más comunes es el apego a métodos y patrones que funcionaban bien en el pasado, pero que han sido superados por nuevas características del lenguaje. Algunos ejemplos incluyen:

Implementación manual de patrones como Singleton

El patrón Singleton es un diseño ampliamente utilizado para restringir la creación de objetos de una clase a una única instancia. Durante mucho tiempo, he implementado este patrón utilizando enfoques manuales que involucraban constructores privados y métodos sincronizados para garantizar la unicidad en entornos multi-hilo.

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

En esta implementación clásica, el uso de un método sincronizado garantiza que solo una instancia de la clase se cree, pero la sincronización introduce una penalización en el rendimiento debido al bloqueo en un entorno multi-hilo.

public enum Singleton {
    INSTANCE;

    public void someMethod() {
        // Métodos de la clase Singleton
    }
}

El enfoque moderno con enum elimina la necesidad de sincronización y maneja automáticamente la unicidad de la instancia y la serialización, proporcionando una solución más sencilla y eficiente.

Evolución de las Colecciones en Java

Java ha evolucionado significativamente en términos de colecciones y manejo de objetos. Las nuevas características han simplificado la manipulación de colecciones y reducido la necesidad de bibliotecas externas.

import java.util.Arrays;
import java.util.List;

public class Example {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", "B", "C");
        // Modificaciones no permitidas
    }
}

Antes, la creación de listas inmutables requería el uso de Arrays.asList(), que no proporciona una lista completamente inmutable.

import java.util.List;

public class Example {
    public static void main(String[] args) {
        List<String> list = List.of("A", "B", "C");
        // List.of() crea una lista inmutable
    }
}

Con Java 9 y versiones posteriores, List.of() ofrece una forma más concisa y segura de crear colecciones inmutables.

Uso de Objects

La clase Objects en Java proporciona métodos útiles para realizar operaciones comunes que antes requerían bibliotecas externas. Esto simplifica el código y mejora su legibilidad.

import org.apache.commons.lang3.ObjectUtils;

public class Example {
    public static void main(String[] args) {
        String result = ObjectUtils.defaultIfNull(null, "default");
        // Usaba Apache Commons Lang para manejar valores nulos
    }
}
import java.util.Objects;

public class Example {
    public static void main(String[] args) {
        String result = Objects.requireNonNullElse(null, "default");
        // Objects.requireNonNullElse() maneja valores nulos sin dependencias externas
    }
}

El uso de Objects.requireNonNullElse() desde Java 9 proporciona una forma sencilla de manejar valores nulos sin depender de bibliotecas adicionales.

Uso de Clases Sealed

Las clases sealed permiten definir un conjunto restringido de subtipos, lo que proporciona un control más preciso sobre la jerarquía de clases y puede hacer el código más seguro y mantenible.

public abstract class Shape {
    // Clases derivadas de Shape pueden ser cualquier cosa
}

public class Circle extends Shape {}
public class Square extends Shape {}

En este caso, la jerarquía de clases no está restringida, lo que puede llevar a problemas si se añaden tipos no deseados.

public sealed interface Shape permits Circle, Square {}

public final class Circle implements Shape {}
public final class Square implements Shape {}

Con las clases sealed, se puede restringir la herencia a un conjunto específico de clases, proporcionando mayor control y previsibilidad sobre los tipos permitidos.

Pattern Matching

El pattern matching simplifica la comprobación de tipos y la extracción de datos en una sola operación, reduciendo el boilerplate y mejorando la claridad del código.

Ejemplo antes:

public class Example {
    public void process(Object obj) {
        if (obj instanceof String) {
            String s = (String) obj;
            System.out.println(s.toUpperCase());
        }
    }
}

Ejemplo ahora:

public class Example {
    public void process(Object obj) {
        if (obj instanceof String s) {
            System.out.println(s.toUpperCase());
        }
    }
}

El uso de pattern matching con instanceof permite declarar y usar la variable en una sola línea, simplificando el código y evitando castings explícitos.

Switch Mejorado

El switch mejorado proporciona una forma más expresiva y menos propensa a errores de manejar múltiples casos, incluyendo patrones de coincidencia. A partir de Java 14, el switch también permite que se manejen casos con null, lo cual agrega flexibilidad al diseño del código.

public class Example {
    public String getDayName(Integer day) {
        if (day == null) {
            return "Invalid day";
        }
        switch (day) {
            case 1: return "Monday";
            case 2: return "Tuesday";
            case 3: return "Wednesday";
            case 4: return "Thursday";
            case 5: return "Friday";
            case 6: return "Saturday";
            case 7: return "Sunday";
            default: throw new IllegalArgumentException("Invalid day: " + day);
        }
    }
}

En este ejemplo, se maneja el caso null fuera del switch, lo que puede hacer que el código sea menos limpio y más propenso a errores.

public class Example {
    public String getDayName(Integer day) {
        return switch (day) {
            case 1 -> "Monday";
            case 2 -> "Tuesday";
            case 3 -> "Wednesday";
            case 4 -> "Thursday";
            case 5 -> "Friday";
            case 6 -> "Saturday";
            case 7 -> "Sunday";
            case null -> "Invalid day";  // Manejo directo de null
            default -> throw new IllegalArgumentException("Invalid day: " + day);
        };
    }
}

Con el switch mejorado, ahora se puede manejar null directamente como una de las posibles opciones dentro del switch, eliminando la necesidad de un manejo especial fuera del bloque switch. Esto simplifica el código y lo hace más legible.

Preferencia por estructuras familiares

Después de tanto tiempo trabajando con Java, es común que tengamos una preferencia por estructuras y patrones familiares. Esto puede llevar a un uso excesivo de prácticas que ya no son las mejores opciones. Aquí hay algunos ejemplos de cómo esta preferencia puede manifestarse y cómo se puede actualizar a enfoques más modernos:

import java.util.ArrayList;
import java.util.List;

public class Example {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        // Usando ArrayList por familiaridad
    }
}
import java.util.List;

public class Example {
    public static void main(String[] args) {
        List<String> list = List.of("A", "B", "C");
        // List.of() ofrece una forma inmutable y más concisa de crear listas
    }
}

En el pasado, el uso de ArrayList era común, pero hoy en día, List.of() proporciona una alternativa más moderna y segura para crear listas inmutables.

Uso de Módulos en Java

Con la introducción de módulos en Java 9, la gestión de dependencias y la organización del código se han vuelto más efectivas. Los módulos permiten definir claramente qué paquetes están disponibles para otros módulos, facilitando la modularización y mejorando el rendimiento al reducir la sobrecarga de dependencias.

Desafiando y superando estos sesgos

Para re-aprender Java de manera efectiva, es fundamental identificar y desafiar estos sesgos. Aquí hay algunas estrategias:

La importancia del des-aprendizaje

Reaprender Java no se trata solo de aprender nuevas características, sino de desaprender los viejos hábitos que ya no son tan efectivos. Esto puede implicar reescribir partes del código, experimentar con nuevos paradigmas y estar abierto al cambio. Al hacerlo, no solo mejoramos como desarrolladores, sino que también nos aseguramos de que nuestro trabajo se mantenga relevante y eficiente en un entorno tecnológico en constante evolución.

Al final del día, desafiar los sesgos existentes no es un ejercicio trivial, pero es esencial para continuar creciendo y adaptándose. Reaprender Java es una oportunidad para reinventarse como desarrollador, aprovechando lo mejor del lenguaje mientras dejamos atrás lo que ya no nos sirve.