IntelliJ IDEA Java

Concordancia de patrones en Java: cinco ejemplos para desarrolladores con poco tiempo libre

Read this post in other languages:

Dado que sabemos que tiene poco tiempo libre, no siempre es fácil estar al día de las nuevas funcionalidades y entender bien dónde y cómo se pueden utilizar.

En este artículo del blog, repasaremos cinco entornos en los que utilizar la concordancia de patrones en Java sin entrar en demasiados detalles. Cuando tenga un rato, eche un vistazo a los enlaces que encontrará a lo largo de este artículo.

¡Vamos allá!

 

1. Mejorar la legibilidad del código convirtiendo las construcciones largas «if-else» en expresiones «switch»

En primer lugar, una pregunta clave: ¿por qué es importante esta conversión?

Una de las principales ventajas es que el código será más conciso y fácil de leer y entender. Dado que, normalmente, las declaraciones largas «if-else» no caben en una sola pantalla y es posible que tenga que desplazarse de forma vertical, no siempre es fácil entender el código que se ejecuta para todas las comparaciones «if». Además, la sintaxis de las condiciones «if» puede ser poco clara, porque cada una de ellas podría estar formada por otro conjunto de condiciones.

A menudo, al navegar por una base de código, observamos código similar al que se muestra a continuación para una construcción «if-else» larga, que asigna un valor a una variable local de forma condicional. Eche un vistazo al código de la siguiente imagen. Le ayudaremos a navegar por este resaltando ciertas secciones un poco más adelante:

 

    
private static String getValueText(Object value) {
    final String newExpression;
    if (value instanceof String) {
        final String string = (String)value;
        newExpression = '"' + StringUtil.escapeStringCharacters(string) + '"';
    }
    else if (value instanceof Character) {
        newExpression = ''' + StringUtil.escapeStringCharacters(value.toString()) + ''';
    }
    else if (value instanceof Long) {
        newExpression = value.toString() + 'L';
    }
    else if (value instanceof Double) {
        final double v = (Double)value;
        if (Double.isNaN(v)) {
            newExpression = "java.lang.Double.NaN";
        }
        else if (Double.isInfinite(v)) {
            if (v > 0.0) {
                newExpression = "java.lang.Double.POSITIVE_INFINITY";
            }
            else {
                newExpression = "java.lang.Double.NEGATIVE_INFINITY";
            }
        }
        else {
            newExpression = Double.toString(v);
        }
    }
    else if (value instanceof Float) {
        final float v = (Float) value;
        if (Float.isNaN(v)) {
            newExpression = "java.lang.Float.NaN";
        }
        else if (Float.isInfinite(v)) {
            if (v > 0.0F) {
                newExpression = "java.lang.Float.POSITIVE_INFINITY";
            }
            else {
                newExpression = "java.lang.Float.NEGATIVE_INFINITY";
            }
        }
        else {
            newExpression = Float.toString(v) + 'f';
        }
    }
    else if (value == null) {
        newExpression = "null";
    }
    else {
        newExpression = String.valueOf(value);
    }
    return newExpression;
}

Resaltemos el código que nos interesa. En la siguiente imagen, verá que el método getValueText compara si el valor de la variable value es de un determinado tipo de datos, como String, Character, Long, Double u otro:

 

 

Para entender las otras partes de esta construcción «if-else», nos centraremos en la variable newExpression. Observe que a esta variable se le está asignando un valor para todos los valores posibles de la variable value:

 

 

Curiosamente, salvo dos, el resto de bloques de código correspondientes a las condiciones «if» son más largos que los demás, que suelen tener solo una línea de código:

 

 

Vamos a extraer estos dos bloques de código más largos en métodos separados y, después, convertiremos la construcción «if-else» en una expresión «switch».

Para extraer código a otro método, seleccione el código, invoque las acciones contextuales con Alt+Intro(Opción+Intro para macOS) y seleccione la opción «Extract method». Puede elegir uno de los nombres sugeridos para el nuevo método o ponerle uno nuevo. Para seleccionar fragmentos lógicos de código, le recomendamos el atajo Ctrl+W (o Ctrl+Mayús+W para reducir la selección). Tras extraer el método, siga las indicaciones de IntelliJ IDEA fijándose en las palabras clave resaltadas en amarillo e invoque las acciones contextuales (Alt+Intro). Para convertir este «if-else» en «switch», hemos invocado las acciones contextuales sobre «if» y seleccionado «Replace ‘if’ with ‘switch’»:

 

 

Esta es la construcción «switch» en el método getValueText, que es concisa y más fácil de entender:

private static String getValueText(Object value) {
    final String newExpression = switch (value) {
        case String string -> '"' + StringUtil.escapeStringCharacters(string) + '"';
        case Character character -> ''' + StringUtil.escapeStringCharacters(value.toString()) + ''';
        case Long aLong -> value.toString() + 'L';
        case Double aDouble -> getNewExpression(aDouble);
        case Float aFloat -> getNewExpression(aFloat);
        case null -> "null";
        default -> String.valueOf(value);
    };
    return newExpression;
}

Llegados a este punto, quizás se esté preguntando por qué no utiliza las expresiones «switch» tan a menudo en su código como las construcciones «if-else». Los motivos son diversos. En las últimas versiones de Java, se ha mejorado la expresión «switch», que ahora puede devolver valores (expresiones «switch») y ya no se limita a comparar valores para un tipo de datos primitivo limitado, clases «wrapper» y otros elementos como String o enumeraciones. Además, las etiquetas «case» pueden incluir patrones y condiciones.

Con la concordancia de patrones y «switch», también es posible manejar valores nulos utilizando «null» como etiqueta «case». Además, cada etiqueta «case» declara una variable de patrón, tanto si se utilizan en el bloque de código correspondiente como si no. En cuanto a las etiquetas «break» que faltan, no son necesarias cuando se utilizan los estilos de flecha con «switch».

Sin embargo, la versión de esta funcionalidad todavía es preliminar, por lo que no debería utilizarla en el código de producción, ya que podría cambiar en las versiones posteriores de Java. Visite este enlace para comprobar la configuración si no está familiarizado con esta.

No todas las declaraciones «if-else» pueden convertirse en construcciones «switch». Puede utilizar las construcciones «if-else» para definir condiciones complejas que podrían utilizar una combinación de variables, constantes o llamadas a métodos. Este tipo de comparaciones complejas aún no son compatibles con las construcciones «switch». 

Para obtener más información acerca de la concordancia de patrones, consulte este artículo del blog.

Ejecutar la inspección «if can be replaced with switch» en el código base

Buscar construcciones «if-else» en el código y comprobar si pueden sustituirse por «switch» lleva tiempo. Así pues, puede ejecutar la inspección «if can be replaced with switch» en todas las clases del código base o en su subconjunto, tal como se explica en este artículo del blog.

Ahora nos centraremos en el siguiente ejemplo, que utiliza la concordancia de patrones para «instanceof», una funcionalidad de producción en Java.

2. Escribir código conciso utilizando la concordancia de patrones con «instanceof»

El uso de la concordancia de patrones para el operador instanceof ha estado disponible como una funcionalidad de producción desde la versión 16 de Java y se puede utilizar en código de producción.

Para utilizar esta función, seguiremos el ejemplo de IntelliJ IDEA e invocaremos las acciones contextuales sobre la palabra clave if, que aparece resaltada en amarillo.

Imagine que tiene una clase llamada Monitor. Este es uno de los muchos ejemplos que encontrará en bases de código para implementar su método equals:

public class Monitor {
    String model;
    double price;

    @Override
    public boolean equals(Object object) {
        if (object instanceof Monitor) {
            Monitor other = (Monitor) object;
            return model.equals(other.model) && price == other.price;
        }
        return false;
    }
}

El siguiente gif muestra cómo utilizar la concordancia de patrones invocando acciones contextuales sobre la variable llamada other, resaltada en amarillo, y seleccionando la opción «Replace ‘other’ with pattern variable». Refactorizar el código resultante invocando acciones contextuales sobre la sentencia «if» puede hacer que este código sea aún más conciso. El código final es más fácil de leer y entender, ya que devuelve «true» si las tres condiciones mencionadas son verdaderas.

¿Qué ocurre si, en lugar de una clase normal, trabaja con una instancia de Record? En el caso de Record, la concordancia de patrones para «instanceof» puede deconstruir una instancia de Record definiendo variables de patrón para los componentes de Record. En el siguiente ejemplo, el uso de Citizen(String name, int age) con el operador instanceof es un patrón Record:

 

Es fácil no reparar en el potencial de estas funcionalidades si nos limitamos a mostrar ejemplos de código sencillos como los dos anteriores. Veamos rápidamente otro ejemplo de uso de la concordancia de patrones con el operador «instanceof», en el que la eliminación de la declaración de una variable local abre otras posibilidades de refactorización o mejora. En resumen, combinar esta función con otras técnicas de refactorización o mejora del código le ayudará a escribir mejor código (¡solo tiene que seguir las indicaciones de IntelliJ IDEA!):

 

3. Ignorar los estados que no tienen sentido

Una construcción «if-else» puede no ser la mejor opción para iterar sobre los valores de un tipo que tiene un conjunto exhaustivo de valores, como las enumeraciones o los subtipos de una clase sellada. Por ejemplo, imagine que tiene una enumeración que define un conjunto fijo de valores, como el siguiente:

enum SingleUsePlastic {
    BOTTLE, SPOON, CARRY_BAG;
}

Aunque sepa que una instancia del tipo SingleUsePlastic puede tener cualquiera de los tres valores, es decir, BOTTLE, SPOON y CARRY_BAG, la variable local final replacement no compilaría el siguiente código:

public class Citizen {
    String getReplacements(SingleUsePlastic plastic) {
        final String replacement;
        if (plastic == SingleUsePlastic.BOTTLE) {
            replacement = "Booth 4: Pick up a glass bottle";
        } else if (plastic == SingleUsePlastic.SPOON) {
            replacement = "Pantry: Pick up a steel spoon";
        } else if (plastic == SingleUsePlastic.CARRY_BAG) {
            replacement = "Booth 5: Pick up a cloth bag";
        } 
        return replacement;
    }
}

Para que se compile, tendrá que añadir al final una cláusula «else» que no tenga sentido.

public class Citizen {
    String getReplacements(SingleUsePlastic plastic) {
        final String replacement;
        if (plastic == SingleUsePlastic.BOTTLE) {
            replacement = "Booth 4: Pick up a glass bottle";
        } else if (plastic == SingleUsePlastic.SPOON) {
            replacement = "Pantry: Pick up a steel spoon";
        } else if (plastic == SingleUsePlastic.CARRY_BAG) {
            replacement = "Booth 5: Pick up a cloth bag";
        } else {
            replacement = "";
        }
        return replacement;
    }
}

Con una construcción «switch», no es necesario codificar una parte «default» para valores que no existen:

public class Citizen {
    String getReplacements(SingleUsePlastic plastic) {
        final String replacement = switch (plastic) {
            case BOTTLE -> "Booth 4: Pick up a glass bottle";
            case SPOON -> "Pantry: Pick up a steel spoon";
            case CARRY_BAG -> "Booth 5: Pick up a cloth bag";
        };
        return replacement;
    }
}

Del mismo modo, si define una clase sellada, puede utilizar una construcción «switch» para iterar sobre su lista exhaustiva de subclases sin definir una cláusula «default»:

sealed interface Lego {}
final class SquareLego implements Lego {}
non-sealed class RectangleLogo implements Lego {}
sealed class CharacterLego implements Lego permits PandaLego {}
final class PandaLego extends CharacterLego {}

public class MyLegoGame {
    int processLego(Lego lego) {
        return switch (lego) {
            case SquareLego squareLego -> 100;
            case RectangleLego rectangleLego-> 200;
            case CharacterLego characterLego -> 300;
        };
    }
}

Si no está familiarizado con las clases selladas y quiere obtener más información al respecto, este artículo del blog le será de gran ayuda.

4. Procesamiento de datos potente y conciso

Puede crear código potente, pero conciso y expresivo al mismo tiempo, para procesar los datos utilizando una combinación de patrones Record, expresiones «switch» y clases selladas. Aquí tiene un ejemplo de una interfaz sellada TwoDimensional, que se implementa con los Records Point, Line, Triangle y Square:

sealed interface TwoDimensional {}
record Point    (int x, 
                 int y) implements TwoDimensional { }
record Line     (Point start, 
                 Point end) implements TwoDimensional { }
record Triangle (Point pointA, 
                 Point pointB, 
				 Point PointC) implements TwoDimensional { }
record Square   (Point pointA, 
                 Point pointB, 
			     Point PointC, 
			     Point pointD) implements TwoDimensional { }

El siguiente método define un proceso de método recursivo que utiliza una construcción «switch» para devolver la suma de las coordenadas x e y de todos los puntos de una figura bidimensional, como en el caso de Line, Triangle o Square:

static int process(TwoDimensional twoDim) {
    return switch (twoDim) {
        case Point(int x, int y) -> x + y;
        case Line(Point a, Point b) -> process(a) + process(b);
        case Triangle(Point a, Point b, Point c) -> process(a) + process(b) + process(c);
        case Square(Point a, Point b, Point c, Point d) -> process(a) + process(b) + process(c) + process(d);
    };
}

IntelliJ IDEA también muestra el icono de llamada recursiva en el medianil para este método:

 

 

5. Separación de cálculo y efecto secundario

No es extraño encontrarse con código que combine un cálculo y un efecto secundario (como imprimir en la consola) en el mismo bloque de código. Por ejemplo, el siguiente código utiliza un bloque «if-else» y el operador instanceof para determinar el tipo de una variable y devuelve un valor condicional en cada bloque de código «if».

void printObject(Object obj) {
    if (obj instanceof String s) {
        System.out.println("String: "" + s + """);
    } else if (obj instanceof Collection<?> c) {
        System.out.println("Collection (size = " + c.size() + ")");
    } else {
        System.out.println("Other object: " + obj);
    }
}

El siguiente gif muestra cómo se puede convertir este bloque «if-else» en una construcción «switch» y, después, utilizar una nueva inspección en «switch» («Push down ‘switch’ expression»), seguida de la extracción de una variable para separar el cálculo y sus efectos secundarios:

 

Resumen

En este artículo del blog, hemos presentado cinco entornos en los que los desarrolladores con poco tiempo libre pueden utilizar la concordancia de patrones en Java. Si quiere obtener más información acerca de estas funcionalidades o cómo ayuda IntelliJ IDEA a usarlas, consulte los enlaces que hemos incluido en los ejemplos.

Díganos sobre qué otros temas le gustaría leer en el próximo artículo del blog.

Hasta entonces, ¡feliz programación!

Artículo original en inglés de

Luiz Di Bella

Mala Gupta

image description