Expressões Lambda e interfaces funcionais: dicas e práticas recomendadas

Expressões Lambda e interfaces funcionais: dicas e práticas recomendadas

1. Visão geral

Agora que o Java 8 atingiu amplo uso, padrões e melhores práticas começaram a surgir para alguns de seus principais recursos. Neste tutorial, examinaremos mais de perto as interfaces funcionais e as expressões lambda.

Leitura adicional:

Por que as variáveis ​​locais usadas nas lambdas precisam ser finais ou efetivamente finais?

Saiba por que o Java exige que as variáveis ​​locais sejam efetivamente finais quando usadas em uma lambda.

Read more

Java 8 - Comparação Poderosa com Lambdas

Classificação elegante em Java 8 - As expressões Lambda ultrapassam o açúcar sintático e trazem semântica funcional poderosa para Java.

Read more

2. Prefira interfaces funcionais padrão

Interfaces funcionais, que são reunidas no pacotejava.util.function, atendem às necessidades da maioria dos desenvolvedores ao fornecer tipos de destino para expressões lambda e referências de método. Cada uma dessas interfaces é geral e abstrata, facilitando a adaptação a praticamente qualquer expressão lambda. Os desenvolvedores devem explorar este pacote antes de criar novas interfaces funcionais.

Considere uma interfaceFoo:

@FunctionalInterface
public interface Foo {
    String method(String string);
}

e um métodoadd() em alguma classeUseFoo, que leva esta interface como parâmetro:

public String add(String string, Foo foo) {
    return foo.method(string);
}

Para executá-lo, você escreveria:

Foo foo = parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", foo);

Olhe mais de perto e verá queFoo nada mais é do que uma função que aceita um argumento e produz um resultado. Java 8 já fornece tal interface emFunction<T,R> do pacotejava.util.function.

Agora podemos remover a interfaceFoo completamente e alterar nosso código para:

public String add(String string, Function fn) {
    return fn.apply(string);
}

Para executar isso, podemos escrever:

Function fn =
  parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", fn);

3. Use a anotação@FunctionalInterface

Anote suas interfaces funcionais com@FunctionalInterface. A princípio, essa anotação parece inútil. Mesmo sem ela, sua interface será tratada como funcional desde que tenha apenas um método abstrato.

Mas imagine um grande projeto com várias interfaces - é difícil controlar tudo manualmente. Uma interface, projetada para ser funcional, pode ser alterada acidentalmente adicionando outros métodos / métodos abstratos, tornando-a inutilizável como interface funcional.

Mas usando a anotação@FunctionalInterface, o compilador irá disparar um erro em resposta a qualquer tentativa de quebrar a estrutura predefinida de uma interface funcional. Também é uma ferramenta muito útil para facilitar a compreensão da arquitetura de aplicativos para outros desenvolvedores.

Então, use isto:

@FunctionalInterface
public interface Foo {
    String method();
}

em vez de apenas:

public interface Foo {
    String method();
}

4. Não abuse dos métodos padrão em interfaces funcionais

Você pode adicionar facilmente métodos padrão à interface funcional. Isso é aceitável para o contrato de interface funcional, desde que haja apenas uma declaração de método abstrato:

@FunctionalInterface
public interface Foo {
    String method();
    default void defaultMethod() {}
}

Interfaces funcionais podem ser estendidas por outras interfaces funcionais se seus métodos abstratos tiverem a mesma assinatura. Por exemplo:

@FunctionalInterface
public interface FooExtended extends Baz, Bar {}

@FunctionalInterface
public interface Baz {
    String method();
    default void defaultBaz() {}
}

@FunctionalInterface
public interface Bar {
    String method();
    default void defaultBar() {}
}

Assim como nas interfaces regulares, estender diferentes interfaces funcionais com o mesmo método padrão pode ser problemático. Por exemplo, suponha que as interfacesBareBaz tenham um método padrãodefaultCommon().. Nesse caso, você obterá um erro em tempo de compilação:

interface Foo inherits unrelated defaults for defaultCommon() from types Baz and Bar...

Para corrigir isso, o métododefaultCommon() deve ser substituído na interfaceFoo. Obviamente, você pode fornecer uma implementação personalizada desse método. Mas se você quiser usar uma das implementações das interfaces pai (por exemplo, da interfaceBaz), adicione a seguinte linha de código ao corpo do métododefaultCommon():

Baz.super.defaultCommon();

Mas tenha cuidado. Adding too many default methods to the interface is not a very good architectural decision. Deve ser visto como um compromisso, para ser usado apenas quando necessário, para atualizar as interfaces existentes sem quebrar a compatibilidade com versões anteriores.

5. Instancie interfaces funcionais com expressões Lambda

O compilador permitirá que você use uma classe interna para instanciar uma interface funcional. No entanto, isso pode levar a um código muito detalhado. Você deve preferir expressões lambda:

Foo foo = parameter -> parameter + " from Foo";

sobre uma classe interna:

Foo fooByIC = new Foo() {
    @Override
    public String method(String string) {
        return string + " from Foo";
    }
};

The lambda expression approach can be used for any suitable interface from old libraries. É utilizável para interfaces comoRunnable,Comparator e assim por diante. However, thisdoesn’t mean that you should review your whole older codebase and change everything.

6. Evite sobrecarregar métodos com interfaces funcionais como parâmetros

Use métodos com nomes diferentes para evitar colisões; vamos ver um exemplo:

public interface Processor {
    String process(Callable c) throws Exception;
    String process(Supplier s);
}

public class ProcessorImpl implements Processor {
    @Override
    public String process(Callable c) throws Exception {
        // implementation details
    }

    @Override
    public String process(Supplier s) {
        // implementation details
    }
}

À primeira vista, isso parece razoável. Mas qualquer tentativa de executar qualquer um dos métodos deProcessorImpl:

String result = processor.process(() -> "abc");

termina com um erro com a seguinte mensagem:

reference to process is ambiguous
both method process(java.util.concurrent.Callable)
in com.example.java8.lambda.tips.ProcessorImpl
and method process(java.util.function.Supplier)
in com.example.java8.lambda.tips.ProcessorImpl match

Para resolver esse problema, temos duas opções. The first is to use methods with different names:

String processWithCallable(Callable c) throws Exception;

String processWithSupplier(Supplier s);

The second is to perform casting manually. Isso não é preferido.

String result = processor.process((Supplier) () -> "abc");

7. Não trate as expressões lambda como classes internas

Apesar do nosso exemplo anterior, onde essencialmente substituímos a classe interna por uma expressão lambda, os dois conceitos são diferentes de uma maneira importante: escopo.

Quando você usa uma classe interna, ela cria um novo escopo. Você pode ocultar variáveis ​​locais do escopo anexado, instanciando novas variáveis ​​locais com os mesmos nomes. Você também pode usar a palavra-chavethis dentro de sua classe interna como uma referência para sua instância.

No entanto, expressões lambda funcionam com o escopo delimitador. Você não pode ocultar variáveis ​​do escopo anexo dentro do corpo da lambda. Nesse caso, a palavra-chavethis é uma referência a uma instância envolvente.

Por exemplo, na classeUseFoo você tem uma variável de instânciavalue:

private String value = "Enclosing scope value";

Em algum método dessa classe, coloque o código a seguir e execute esse método.

public String scopeExperiment() {
    Foo fooIC = new Foo() {
        String value = "Inner class value";

        @Override
        public String method(String string) {
            return this.value;
        }
    };
    String resultIC = fooIC.method("");

    Foo fooLambda = parameter -> {
        String value = "Lambda value";
        return this.value;
    };
    String resultLambda = fooLambda.method("");

    return "Results: resultIC = " + resultIC +
      ", resultLambda = " + resultLambda;
}

Se você executar o métodoscopeExperiment(), obterá o seguinte resultado:Results: resultIC = Inner class value, resultLambda = Enclosing scope value

Como você pode ver, ao chamarthis.value em IC, você pode acessar uma variável local de sua instância. Mas no caso do lambda, a chamadathis.value dá acesso à variávelvalue que é definida na classeUseFoo, mas não à variávelvalue definida dentro do corpo de lambda.

8. Mantenha as expressões lambda curtas e autoexplicativas

Se possível, use construções de uma linha em vez de um grande bloco de código. Lembre-se delambdas should be anexpression, not a narrative. Apesar de sua sintaxe concisa,lambdas should precisely express the functionality they provide.

Este é principalmente um conselho estilístico, pois o desempenho não muda drasticamente. Em geral, no entanto, é muito mais fácil entender e trabalhar com esse código.

Isso pode ser alcançado de várias maneiras - vamos dar uma olhada mais de perto.

8.1. Evite blocos de código no corpo da Lambda

Em uma situação ideal, as lambdas devem ser escritas em uma linha de código. Com essa abordagem, o lambda é uma construção auto-explicativa, que declara qual ação deve ser executada com quais dados (no caso de lambdas com parâmetros).

Se você tem um grande bloco de código, a funcionalidade do lambda não é imediatamente clara.

Com isso em mente, faça o seguinte:

Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
    String result = "Something " + parameter;
    //many lines of code
    return result;
}

ao invés de:

Foo foo = parameter -> { String result = "Something " + parameter;
    //many lines of code
    return result;
};

However, please don’t use this “one-line lambda” rule as dogma. Se você tiver duas ou três linhas na definição de lambda, pode não ser valioso extrair esse código para outro método.

8.2. Evite especificar tipos de parâmetros

Na maioria dos casos, um compilador é capaz de resolver o tipo de parâmetros lambda com a ajuda detype inference. Portanto, adicionar um tipo aos parâmetros é opcional e pode ser omitido.

Faça isso:

(a, b) -> a.toLowerCase() + b.toLowerCase();

em vez disso:

(String a, String b) -> a.toLowerCase() + b.toLowerCase();

8.3. Evite parênteses em torno de um único parâmetro

A sintaxe do Lambda requer parênteses apenas em torno de mais de um parâmetro ou quando não há nenhum parâmetro. É por isso que é seguro tornar seu código um pouco mais curto e excluir parênteses quando houver apenas um parâmetro.

Então, faça o seguinte:

a -> a.toLowerCase();

em vez disso:

(a) -> a.toLowerCase();

8.4. Evite declaração de retorno e chaves

As instruçõesBracesereturn são opcionais em corpos lambda de uma linha. Isso significa que eles podem ser omitidos por clareza e concisão.

Faça isso:

a -> a.toLowerCase();

em vez disso:

a -> {return a.toLowerCase()};

8.5. Use referências de método

Muitas vezes, mesmo em nossos exemplos anteriores, as expressões lambda chamam métodos que já foram implementados em outros lugares. Nessa situação, é muito útil usar outro recurso do Java 8:method references.

Então, a expressão lambda:

a -> a.toLowerCase();

pode ser substituído por:

String::toLowerCase;

Isso nem sempre é mais curto, mas torna o código mais legível.

9. Use Variáveis ​​“Efetivamente Finais”

O acesso a uma variável não final dentro das expressões lambda causará o erro em tempo de compilação. But it doesn’t mean that you should mark every target variable as final.

De acordo com o conceito “effectively final”, um compilador trata todas as variáveis ​​comofinal,, desde que sejam atribuídas apenas uma vez.

É seguro usar essas variáveis ​​dentro de lambdas porque o compilador controlará seu estado e disparará um erro em tempo de compilação imediatamente após qualquer tentativa de alterá-las.

Por exemplo, o código a seguir não será compilado:

public void method() {
    String localVariable = "Local";
    Foo foo = parameter -> {
        String localVariable = parameter;
        return localVariable;
    };
}

O compilador informará que:

Variable 'localVariable' is already defined in the scope.

Essa abordagem deve simplificar o processo de tornar a execução lambda segura para threads.

10. Proteja as variáveis ​​do objeto da mutação

Um dos principais objetivos dos lambdas é o uso em computação paralela - o que significa que eles são realmente úteis quando se trata de thread-safety.

O paradigma "efetivamente final" ajuda muito aqui, mas não em todos os casos. Lambdas não podem alterar o valor de um objeto do escopo fechado. Porém, no caso de variáveis ​​de objetos mutáveis, um estado pode ser alterado dentro das expressões lambda.

Considere o seguinte código:

int[] total = new int[1];
Runnable r = () -> total[0]++;
r.run();

Este código é válido, pois a variáveltotal permanece “efetivamente final”. Mas o objeto que ele referencia terá o mesmo estado após a execução do lambda? No!

Mantenha este exemplo como um lembrete para evitar código que pode causar mutações inesperadas.

11. Conclusão

Neste tutorial, vimos algumas práticas recomendadas e armadilhas nas expressões lambda e interfaces funcionais do Java 8. Apesar da utilidade e poder desses novos recursos, eles são apenas ferramentas. Todo desenvolvedor deve prestar atenção ao usá-los.

Osource code completo para o exemplo está disponível emthis GitHub project - este é um projeto Maven e Eclipse, portanto, pode ser importado e usado como está.