IntelliJ IDEA Java

Correspondência de padrões em Java — 5 exemplos para desenvolvedores ocupados

Read this post in other languages:

É difícil para um desenvolvedor ocupado acompanhar os novos recursos e compreender em profundidade onde e como se pode usá-los.

Nesta postagem do blog, abordarei 5 lugares onde se pode usar correspondência de padrões no Java, sem me aprofundar nos detalhes mais específicos. Quando você achar que já está pronto para explorar este assunto mais a fundo, confira os links incluídos nesta postagem.

Vamos começar!

 

1. Melhore a legibilidade do código convertendo longas construções “if-else” para “switch”

Primeiro, vamos abordar a pergunta mais importante: por que se importar com esta conversão?

Um dos principais benefícios é que o código fica mais conciso e mais fácil de ler e compreender. Como longas declarações “if-else” geralmente não cabem numa só tela e podem envolver rolagem vertical, é difícil compreender o código executado para todas as comparações do “if”. Além disso, a sintaxe das condições “if” pode ser obscura, porque cada condição “if” pode conter outro conjunto de condições.

Ao navegar por uma base de código, é comum observar código semelhante ao mostrado abaixo, de uma longa construção “if-else” que atribui valores condicionalmente a uma variável local. Dê uma olhada no código abaixo. Ajudarei você a navegar por ele realçando algumas partes daqui a pouco:

 

    
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;
}

Vamos realçar o código no qual devemos nos concentrar. Na imagem a seguir, observe que o método getValueText verifica se o valor da variável value é de um certo tipo de dados, como String, Character, Long, Double ou outro:

 

 

Para compreendermos as outras partes desta construção “if-else”, vamos nos concentrar na variável newExpression. Observe que está sendo atribuído um valor a esta variável correspondendo a cada tipo possível da variável value:

 

 

É interessante notar que dois blocos correspondentes às condições “if” são mais longos que os outros, que geralmente contêm apenas uma linha de código:

 

 

Vamos extrair estes dois blocos mais longos de código, para separar os métodos, e depois vamos proceder com a conversão da construção “if-else” em “switch”.

Para extrair código para outro método, selecione o código, invoque ações de contexto usando Alt+Enter (ou Option+Enter no macOS) e selecione a opção “Extract method”. Você pode escolher um dos nomes sugeridos para o novo método ou digitar um nome da sua escolha. Para selecionar trechos lógicos de código, meu atalho favorito é Ctrl+W (ou Ctrl+Shift+W para encolher a seleção). Depois da extração do método, siga as indicações do IntelliJ IDEA observando as palavras-chave com fundo amarelo e invocando ações de contexto (Alt+Enter). Para converter esta estrutura “if-else” para “switch”, invoque ações de contexto no “if” e selecione “Replace ‘if’ with ‘switch’ “:

 

 

Aqui está a construção “switch” no método getValueText, mais concisa e mais fácil de compreender:

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;
}

Isso faz você pensar por que você não usou expressões “switch” no seu código tanto quanto construções “if-else”? Há várias razões para isso. A construção “switch” foi aperfeiçoada nas versões recentes do Java. Elas podem retornar valores (expressões “switch”) e não se limitam mais a comparar valores de um tipo primitivo e limitado de dados, classes “wrapper” e outras como String ou “enum”. Além disso, os valores declarados nas suas cláusulas “case” também podem incluir padrões e condições.

Com correspondência de padrões e “switch”, você também pode manipular valores nulos, usando “null” como condição de um “case”. Além disso, cada valor declarado no “case” declara uma variável de padrão, não importando se é usada ou não no bloco correspondente do código. Se você estiver preocupado com a falta de cláusulas break, saiba que elas não são necessárias quando você usa os estilos de seta com “switch”.

Porém, este recurso ainda está em estágio de demonstração, o que significa que você não deve usá-lo no seu código de produção, pois poderá mudar numa versão futura do Java. Visite este link para verificar as configurações, se você não estiver familiarizado com elas.

Nem todas as declarações “if-else” podem ser convertidas em construções com “switch”. Você pode usar as construções “if-else” para definir condições complexas, que talvez usem uma combinação de variáveis, constantes e chamadas a métodos. Essas comparações complexas ainda não têm suporte em construções com “switch”. 

Para ver uma abordagem detalhada da correspondência de padrões, confira esta postagem de blog.

Execução da inspeção “if can be replaced with switch” na sua base de código

Pode ser demorado procurar construções “if-else” no seu código e verificar se elas podem ser substituídas por “switch”. Você pode executar a inspeção “if can be replaced with switch” em todas as classes da sua base de código ou em um subconjunto delas, como mostrado nesta postagem do blog.

Vamos trabalhar com nosso próximo exemplo, que usa correspondência de padrões no “instanceof”. Este já é um recurso de produção em Java.

2. Escreva código conciso usando correspondência de padrões com “instanceof”

A correspondência de padrões no operador instanceof já está disponível como recurso de produção desde a versão 16 do Java e pode ser usada em código de produção.

Para usar este recurso, apenas seguirei as indicações do IntelliJ IDEA e invocarei ações de contexto na palavra-chave if, que estará realçada com um fundo amarelo.

Imagine que você tenha uma classe, digamos, Monitor. Aqui está um dos exemplos comuns que você encontrará em várias bases de código para implementar o 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;
    }
}

O GIF a seguir mostra como usar a correspondência de padrões invocando ações de contexto na variável other, realçada com fundo amarelo, e depois selecionando a opção “Replace ‘other’ with pattern variable”. Esse código pode ficar ainda mais conciso se o código resultante for refatorado invocando ações de contexto na declaração “if”. O código final é mais fácil de ler e compreender – ele retorna true se todas as três condições mencionadas forem verdadeiras.

O que acontece se, em vez de uma classe normal, você estiver trabalhando com uma instância de Record? No caso de Records, a correspondência de padrões em “instanceof” pode desconstruir uma instância de Record definindo variáveis de padrão para os componentes do Record. No exemplo a seguir, Citizen(String name, int age) usado com o operador instanceof é um padrão Record:

 

É fácil deixar de perceber o poder desses recursos quando se começa com exemplos simples de código, como os dois anteriores. Vamos dar uma olhada rápida em outro exemplo do uso da correspondência de padrões com o operador “instanceof”, no qual remover a declaração de uma variável local leva a outras possibilidades de refatoração ou aperfeiçoamento. Em resumo, a combinação deste recurso com outras técnicas de refatoração ou aperfeiçoamento de código pode ajudar você a escrever código melhor (basta seguir as indicações do IntelliJ IDEA!):

 

3. Ignore estados que não fazem sentido

Uma construção “if-else” pode não ser a melhor escolha para iterar sobre os valores de um tipo com um conjunto completo de valores, como um “enum” ou os subtipos de uma classe selada. Por exemplo, imagine que você tenha um “enum” que defina um conjunto fixo de valores, como abaixo:

enum SingleUsePlastic {
    BOTTLE, SPOON, CARRY_BAG;
}

Embora você saiba que uma instância do tipo SingleUsePlastic pode ter qualquer um dentre três valores — ou seja, BOTTLE, SPOON e CARRY_BAG —, o código a seguir falharia na compilação para a variável local final replacement:

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 fazer esse código compilar, você precisará adicionar uma declaração “else” ao final, que não fará 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;
    }
}

Com uma construção “switch”, você não precisa incluir uma cláusula default para valores que não existem:

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;
    }
}

De forma semelhante, se você definir uma classe selada, você poderá usar uma construção “switch” para iterar sobre sua lista completa de subclasses sem definir uma 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;
        };
    }
}

Se você não estiver familiarizado com classes seladas e desejar mergulhar a fundo nesse tópico, acesse esta postagem do blog.

4. Processamento poderoso e conciso de dados

Você pode criar um código poderoso, mas conciso e expressivo, para processar os seus dados, usando uma combinação de padrões Record, expressões “switch” e classes seladas. Aqui está um exemplo de uma interface selada TwoDimensional, que é implementada pelos Records Point, Line, Triangle e 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 { }

O método a seguir define um processo de método recursivo que usa uma construção “switch” para retornar a soma das coordenadas x e y de todos os pontos de uma figura bidimensional, como Line, Triangle ou 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);
    };
}

O IntelliJ IDEA também mostra o ícone de chamada recursiva para este método na medianiz:

 

 

5. Separação entre computação e efeitos colaterais

É comum encontrar código que combina uma computação e um efeito colateral (como imprimir em um console) no mesmo bloco de código. Por exemplo, o código a seguir usa um bloco “if-else” e um operador instanceof para determinar o tipo de uma variável e depois imprime um valor condicional em cada bloco de código.

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);
    }
}

O GIF a seguir mostra como converter aquele bloco “if-else” em uma construção “switch” e depois usar uma nova inspeção no “switch” — “Push down ‘switch’ expression” —, seguida da extração de uma variável para separar a computação dos seus efeitos colaterais:

 

Resumo

Nesta postagem de blog, abordamos 5 situações em que desenvolvedores ocupados podem usar a correspondência de padrões no Java. Se você tiver interesse em aprender mais sobre estes recursos ou sobre como o IntelliJ IDEA ajuda você a usá-los, consulte os links que incluí ao abordar estes exemplos.

Diga para mim quais outros tópicos você gostaria que eu abordasse na minha próxima postagem no blog.

Até lá, boa programação!

Artigo original em inglês por:

Luiz Di Bella

Mala Gupta

image description