Desde o lançamento de sua primeira versão oficial em 1996, a linguagem Java evangelizou uma legião de seguidores: estudantes, engenheiros e programadores que fazem do Java uma comunidade única e vibrante. É uma linguagem que mesmo depois de tanto tempo, continua sendo usada em pequenos, grandes e gigantescos projetos. Nessa linha evolutiva do Java, assim como ocorre em qualquer tecnologia, sempre quando temos uma nova versão sendo lançada, fazemos a mesma pergunta: Por que devo migrar meu código para essa nova versão?
No caso do Java 8 a resposta para a pergunta acima é simples e fácil de ser dada. As mudanças ocorridas no Java 8 são em sua grande maioria as maiores mudanças ocorridas na linguagem desde seu lançamento, desde o ponto de vista de arquitetura quanto de sintaxe do código-fonte.
A boa notícia é que com o Java 8 podemos escrever programas de forma mais fácil, rápida e com um código muito mais limpo, de fácil leitura e menos verboso.
Um ponto é importante para destacar, é da perspectiva de hardware. A grande maioria dos computadores de hoje em dia possuem mais de um núcleo de CPU – o processador de seu laptop ou desktop provavelmente tem quatro ou mais núcleos de CPU dentro dele. Entretanto, a grande maioria de programas Java existente utiliza apenas um desses núcleos e deixam os outros ociosos.
Antes do Java 8, utilizávamos threads para usar todos os núcleos, o problema é que trabalhar com threads dependendo do contexto acarreta em uma dificuldade maior e problemas de concorrência e condições de corrida devem ser levados em conta. Bem da verdade que a evolução da linguagem Java se deu também na forma de como trabalhamos com a concorrência. Na versão Java 1.0 tinham mecanismos de lock/semáforos, na versão Java 5 foi introduzido coleção concorrente, no Java 7 foi adicionado o framework fork/join fazendo com que o paralelismo fosse tratado de uma forma mais robusta, mas ainda sim, existia uma complexidade grande.
No Java 8 temos uma forma nova e simples de se pensar em paralelismo. Java 8 introduz uma nova API (Streams) que suporta diversas operações em paralelo para processamento de dados, quando começamos a utilizar a API de Streams achamos muito parecido da forma que fazemos nossas consultas em banco de dados. Através da API de Streams podemos escrever nossos códigos concorrentes sem a necessidade de utilizarmos o synchronized, e isso é uma ótima notícia, pois, além de tornar nosso código menos passível de erros de concorrência o synchronized apresenta um custo de processamento maior do que trabalhar com Streams.
Outro ponto extremamente relevante do Java 8 é a forma como parametrizamos comportamentos. Suponha que você tenha dois métodos que são muito parecidos em sua implementação, com algumas pequenas linhas de diferença entre eles, com Java 8 você pode passar essas diferenças como argumento. Você deve estar se perguntando se isso não poderia ser feito antes do Java 8 com classes anônimas, por exemplo. A resposta é: Sim! Entretanto, passando código como argumentos traz uma forma muito mais consistente e clara de escrever seus programas.
A funcionalidade de se passar código para métodos é chamada de programação funcional e em Java chamamos de expressões lambdas.
Evolução ou Morte
Com certeza, você deve ter acompanhado a evolução do Java ao longo dos anos. Por exemplo, a introdução de tipos Genéricos e a utilização de List<String> ao invés de List pode tê-lo deixado você incomodado com a nova sintaxe da linguagem, mas hoje em dia estamos completamente adaptados a essa forma de escrita e os benefícios que ela traz (verificação de erros no momento da compilação, código mais claro e menos verboso porque sabemos de que Tipo a nossa coleção é).
Outras mudanças não são tão expressivas, mas ajudam muito em tornar nosso código mais legível, tal como o for-each ao invés de utilizarmos o objeto Iterator.
Talvez você ache que as principais mudanças ocorridas no Java 8 estejam se afastando da programação orientada a objeto clássica ou que temos um conflito entre orientação objeto e programação funcional, de forma que não possam conviver juntas.
A ideia é trazer o melhor dos dois paradigmas, dessa forma você tem uma maior chance de ter uma melhor linguagem com mais possibilidades para trabalhar.
Qualquer linguagem precisa evoluir, seja para suportar novos requisitos de hardware ou atender novas expectativas do mercado/programadores, Java evolui criando novas funcionalidades, entretanto, essa evolução será inútil ao menos que utilizemos os novos recursos oferecidos. Utilizando (ou migrando) as novas funcionalidades do Java, você está garantindo sua sobrevivência como programador Java, tenho certeza que você irá adorar escrever código no estilo funcional.
Funções em Java
A palavra função em linguagens de programação é normalmente usada como sinônimo para referenciarmos um método, particularmente um método estático.
Java 8 adiciona funções como uma nova forma de expressar valores. Pense um instante sobre os possíveis tipos de valores manipulados em programas Java. Em primeiro lugar, existem os valores primitivos, como 28(do tipo int), e 14.3(do tipo double). Em segundo lugar, os valores podem ser objetos (mais especificamente, as referências de objetos).
O ponto mais importante de uma linguagem de programação é a manipulação de valores, outras estrutura como classes e métodos são criadas para estruturarmos nossos dados e representarmos a estrutura desses dados, entretanto não podemos usá-los como argumentos no momento da execução. Com o crescimento da adoção do estilo de programação funcional, percebeu-se que ser capaz de passar métodos em tempo de execução, e de certa forma tratarmos os métodos como valores é extremamente útil para a programação. Algumas linguagens já fazem isso e mostraram uma grande robustez e diversas possibilidades interessantes na escrita do código, a mais famosa de todas, JavaScript.
Streams
Tente lembrar a última vez que escreveu um programa em Java e não teve a necessidade de criar ou manipular Collections….provavelmente não se lembrará com tanta facilidade. Collections são estruturas extremamente comuns de utilizarmos no nosso dia-a-dia, porém, nem sempre trabalhar com Collections é algo fácil e claro. Dependendo da operação que precisamos fazer (ordenação, filtro, etc), somos obrigados a recorrer a estruturas de if/else e for encadeados, o que acarreta em um baixo reuso além do código ficar pouco legível.
A boa notícia é que a a API de Streams introduzida no Java 8 traz uma forma diferente de processar dados em comparação com as tradicionais Collections. Utilizando Collections, você é o responsável pela iteração, precisamos percorrer cada elemento, um a um utilizando uma estrutura de repetição e então processando cada elemento. Chamamos esse tipo de iteração de iteração externa. Por outro lado, utilizando Streams, não precisamos pensar em estruturas de repetição, os dados são processados internamente. Chamamos essa ideia de iteração interna.
Outro problema que temos manipulando dados com Collections é se o número de elementos for muito grande. Pense no caso de uma Collections com centenas de milhares de elementos, como ficaria o tempo de processamento? Um único core de CPU provavelmente não será suficiente para processar todo esse volume de informações, porém você provavelmente deve ter outros cores disponíveis em sua máquina para ser utilizado. O ideal seria utilizarmos todos os cores de CPUs disponíveis e dividirmos o trabalho entre eles a fim de que o tempo de processamento seja diminuído.
Métodos Default
Um dos conceitos mais difundidos na orientação objeto é a relação entre uma interface e suas implementações concretas. Vejamos o seguinte trecho de código:
Listagem 1. Métodos Default
[java]
List<Apple> heavyApple = inventory.stream().filter((Apple a) -> a.getWeight() > 150).collect(toList());
List<Apple> heavyApple2 = inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150).collect(toList());
[/java]
Existe um grande problema no código acima, a interface List<T> não possui os métodos stream() e parallelStream(), sem essas métodos declarados na interface esse trecho de código apresentará erros de compilação. A solução mais simples seria introduzir na interface List<T> esses métodos e então implementa-los, na classe ArrayList, por exemplo. Mas fazendo isso, traria um grande problema para todos os programas que implementam a interface List<T>, adicionando um novo método na interface estaríamos obrigando todos a implementarem os novos métodos. Isso trouxe um grande dilema: Como evoluir interfaces que já estão em uso, e no caso da API de Collection, interfaces largamente utilizadas sem gerar erros de compilação nos programas existentes?
A solução do Java 8 foi que, a partir de agora, uma interface pode conter assinaturas de métodos sem que as classes que implementam essa interface precise implementá-los. A implementação fica na própria interface, dessa forma nossas interfaces podem ser enriquecidas com novos métodos além daqueles que planejamos incialmente, sem quebrar o código existente. Abaixo temos um exemplo do métodos sort da interface List.
Listagem 2. Métodos Default na Interface List
[java]
default void sort(Comparator<? super E> c){
Collections.sort(this, c);
}
[/java]
Dessa forma, qualquer classe concreta que implementa a interface List não precisa explicitamente implementar o método sort.
Refatorando para o Java 8
Como vimos anteriormente, é claro e evidente as grandes melhorias que foram introduzidas no Java 8, imagino que se tiver a oportunidade de iniciar um projeto do zero sua escolha natural será pelo Java 8 para usufruir todos os seus benefícios. Entretanto, infelizmente esse não é o cenário de todos nós. Grande parte do tempo nos deparamos com a situação de migrar um código existente, quando essas migrações são de versões recentes, menos mal, mas às vezes nos deparamos com migrações com versões bem antigas e nem sempre é uma tarefa fácil de efetuarmos a migração.
Pensando nisso, elaborei algumas receitas para serem usadas na refatoração de código para o Java 8 utilizando expressões lambdas e streams. São tarefas simples, que você poderá aplicar com facilidade em seus projetos e já começar a utilizar os benefícios da nova versão.
Além de tornar seus códigos mais legíveis e flexíveis. Mais adiante também mostrarei algumas receitas de refatoração aplicadas a padrões de projetos bem conhecidos no mundo da orientação objeto.
Porque refatorar um código?
Um dos grandes argumentos para refatorar seu código para o Java 8 é a utilização de expressões lambdas sem dúvida nenhuma. Através delas, podemos escrever códigos mais concisos, legíveis e flexíveis. A expressão lambda tende a ser concisa porque conseguimos escrever um comportamento de uma forma muito mais compacta do que se utilizarmos classes anônimas, por exemplo.
Métodos referência também são um grande trunfo na refatoração visto que podemos através deles passar um método como argumento para outro método de uma forma elegante e robusta.
Seu código ficará mais flexível visto que as expressões lambdas forçam o pensamento em direção a parametrização comportamental. Seu código poderá usar e executar múltiplos comportamentos passados como argumentos de acordo com mudanças que podem acontecer nos requisitos de negócio.
Outra questão que sempre é lembrada quando vamos refatorar código é em relação a tornar nosso código mais legível. É difícil definir o quão legível é um código, mesmo porque é uma questão bastante subjetiva. Acredito que a lei básica da regra da legibilidade é: “A facilidade com que seu código pode ser compreendido por outra pessoa”. Muitas vezes quando terminamos uma programação de algo complexo, nos preocupamos simplesmente com dois pontos: Compila? E Funciona? Uma vez o código compilando e funcionando damos como encerrado o assunto e vamos para o próximo problema.
É muito raro, gastamos uma ou duas horas depois que nosso código está funcionalmente correto, refatorando a solução para ajudar o programador que virá depois de nós para dar manutenção naquele código.
Existem algumas regras que devem ser seguidas para tornar seu código mais legível, as duas regras básicas são: boa documentação e seguir os padrões de codificação da linguagem.
O Java 8 pode lhe ajudar nessa tarefa melhorando a clareza e tornando seu código mais legível que versões anteriores do Java.
- Tornar o código menos verboso, fazendo com que ele fique mais fácil de ser compreendido;
- Através de métodos referência e Streams seu código ganha em robustez ficando a escrita do código muito próximo ao problema funcional que você resolvendo.
Vejamos agora três refatorações simples que utilizam expressões lambdas, streams e métodos referência que você pode aplicar em seus códigos para torná-los mais legíveis:
- Refatorar classes anônimas para expressões lambdas;
- Refatorar expressões lambdas para métodos referência;
- Refatorar processamento de Collections(estilo imperativo) para Streams.
De classes anônimas para expressões lambdas
A primeira refatoração que vamos analisar é converter classes anônimas que implementam apenas um método abstrato em expressões lambdas. Por quê? Porque acreditamos que classes anônimas são estruturas extremamente verbosas e propensas a erros. Utilizando expressões lambdas, o código produzido se torna mais sucinto e legível. Por exemplo, abaixo temos uma implementação da interface Runnable e sua corresponde como uma expressão lambda.
Listagem 3. Refatoração classes anônimas para expressões lambdas
[java]
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(“Refatorando…”);
}
};
Runnable r2 = () -> System.out.println(“Refatorado!”);
[/java]
Converter uma classe anônima em uma expressão lambda pode ser uma tarefa difícil em certas situações. Primeiro, as palavras-chaves this e super têm significados diferentes para classes anônimas e expressões lambdas. Dentro de uma classe anônima a palavra this se refere aos membros declarados no escopo da classe anônima, mas na expressão lambda não podemos usar a palavra this. Segundo, nas classes anônimas podemos declarar variáveis a mesma variável como membro da classe e como variável local ao método que estamos implementando, nas expressões lambdas, não!
Listagem 4. Palavra-chave this no contexto de classes anônimas
[java]
Runnable r1 = new Runnable() {
int a = 1;
@Override
public void run() {
int a = 2;
System.out.println(“Variavel Local: ” + a);
System.out.println(“Atributo da classe anonima: ” + this.a);
}
};
[/java]
Listagem 5. Palavra-chave this no contexto de expressões lambdas
[java]
int a = 1;
Runnable r2 = () -> {
int a = 2; //erro de compilacao, variavel ‘a’ ja declarada
System.out.println(a);
System.out.println(this.a); //erro de compilacao
};
[/java]
Finalizando, a refatoração de uma classe anônima para uma expressão lambda pode produzir um código ambíguo no contexto da sobrecarga de métodos. O que ocorre é que o Tipo de uma classe anônima é definido explicitamente no momento de sua criação, mas o Tipo de uma expressão lambda depende do seu contexto. Abaixo temos um exemplo de como isso pode ser problemático. Vamos criar uma interface funcional(¹) chamada Tarefa com a mesma assinatura da interface Runnable.
Listagem 6. Sobrecarga de métodos com expressões lambdas
[java]
interface Tarefa {
public void execute();
}
class SobrecargaComLambda {
public static void fazAlgumaCoisa(Runnable r) {
r.run();
}
public static void fazAlgumaCoisa(Tarefa t) {
t.execute();
}
}
[/java]
Podemos agora passar para o método fazAlgumaCoisa uma classe anônima que implementa a interface Tarefa:
Listagem 7. Sobrecarga de métodos com expressões lambdas
[java]
fazAlgumaCoisa(new Tarefa() {
@Override
public void execute() {
System.out.println(“Perigo!”);
}
});
[/java]
Mas se tentarmos converter essa classe anônima em uma expressão lambda teremos um problema de compilação, porque nossa expressão lambda seria válida para os dois métodos sobrecarregados:
Listagem 8. Sobrecarga de métodos com expressões lambdas
[java]
fazAlgumaCoisa(() -> System.out.println(“Erro!!”)); //erro de compilacao
[/java]
Devemos resolver essa ambiguidade inserindo um casting explícito para o tipo Tarefa
Listagem 9. Sobrecarga de métodos com expressões lambdas
[java]
fazAlgumaCoisa((Tarefa)() -> System.out.println(“Agora, tudo OK!!”));
[/java]
De expressões lambdas para métodos de referência
Expressões lambdas são muito interessantes para implementarmos trechos de códigos pequenos para serem passadas como argumento. Mas considere a utilização de métodos referência sempre que possível para melhorar a legibilidade do código. Uma chamada de método sempre nos traz maior legibilidade, porque se escolhermos bem o nome do método, a leitura do trecho de código fica bem intuitiva. Veja o trecho de código abaixo, a ideia é agrupar refeições pelo nível de calorias.
Listagem 10. Refatorando expressões lambdas para métodos de referência
[java]
public class Refeicao {
private final String nome;
private final boolean vegetariano;
private final int calorias;
private final Tipo tipo;
public Refeicao(String nome, boolean vegetariano, int calorias, Tipo tipo) {
this.nome = nome;
this.vegetariano = vegetariano;
this.calorias = calorias;
this.tipo = tipo;
}
public String getNome() {
return nome;
}
public boolean isVegetariano() {
return vegetariano;
}
public int getCalorias() {
return calorias;
}
public Tipo getTipo() {
return tipo;
}
public enum Tipo {
CARNE, PEIXE, OUTROS
}
public enum NivelCaloria {
DIET, NORMAL, ALTO
}
@Override
public String toString() {
return getNome() + ” ” + getCalorias();
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.*;
import artigoJM.Refeicao.NivelCaloria;
import artigoJM.Refeicao.Tipo;
public class TesteRefeicao {
public static void main(String[] args) {
List<Refeicao> menu = init();
Map<NivelCaloria, List<Refeicao>> refeicaoPorNivelCaloria = menu.stream().collect(groupingBy(refeicao -> {
if (refeicao.getCalorias() <= 400)
return NivelCaloria.DIET;
else if (refeicao.getCalorias() <= 700)
return NivelCaloria.NORMAL;
else
return NivelCaloria.ALTO;
}));
System.out.println(refeicaoPorNivelCaloria);
}
public static List<Refeicao> init() {
List<Refeicao> listaRefeicao = new ArrayList<Refeicao>();
listaRefeicao.add(new Refeicao(“Parmegiana”, false, 1000, Tipo.CARNE));
listaRefeicao.add(new Refeicao(“Macarrão”, false, 750, Tipo.OUTROS));
listaRefeicao.add(new Refeicao(“Sardinha”, false, 250, Tipo.PEIXE));
return listaRefeicao;
}
}
[/java]
Utilizando a API de Streams para realizar a operação de agrupamento
Listagem 11. Refatorando expressões lambdas para métodos de referência
[java]
Map<NivelCaloria, List<Refeicao>> refeicaoPorNivelCaloria = menu.stream().collect(groupingBy(Refeicao::getNivelCaloria));
System.out.println(refeicaoPorNivelCaloria);
[/java]
Com isso adicionamos o seguinte método na classe Refeicao:
Listagem 12. Refatorando expressões lambdas para métodos de referência
[java]
public NivelCaloria getNivelCaloria() {
if (getCalorias() <= 400)
return NivelCaloria.DIET;
else if (getCalorias() <= 700)
return NivelCaloria.NORMAL;
else
return NivelCaloria.ALTO;
}
[/java]
Além disso, considere fazer uso de métodos estáticos helper tais como comparing e maxBy sempre que possíveis. Esses métodos foram projetados para serem usados com métodos de referência. Perceba na listagem 11 que esse código se tornou bem mais legível e defini melhor o seu propósito do que a versão com expressões lambdas.
De processamento de dados imperativos para Streams
De forma geral, você deve tentar converter todos os códigos que processam Collections, sejam eles utilizando for-each, Iterator para Streams. Por que? A API de Streams expressa de forma mais clara e objetiva a sua intenção no pipeline de processamento dos dados.
Além disso, Streams podem ser otimizadas pela JVM fazendo uso de mecanismos short-circuit, lazy e o mais importante de todos, utilizar a arquitetura multicore do seu processador.
O exemplo abaixo, processa uma Collections utilizando dois padrões largamente conhecidos: filtro e extração, normalmente esses padrões estão sempre muito acoplados no processamento da Collections, o que torna a leitura e o entendimento do código mais difícil de ser compreendida. Se levarmos em conta a implementação desses trecho utilizando paralelismo acrescentaríamos uma complexidade que com certeza não ficaria nada trivial de ser entendida.
Listagem 13. Refatorando processamento em Collections
[java]
List<String> nomeRefeicao = new ArrayList<String>();
for (Refeicao refeicao : menu) {
if (refeicao.getCalorias() > 300) {
nomeRefeicao.add(refeicao.getNome());
}
}
[/java]
A alternativa utilizando a API de Strems fica bem mais intuitiva e próximo a descrição do problema, e o processamento pode ser facilmente paralelizado
Listagem 14. Refatorando processamento em Collections
[java]
List<Refeicao> menu = init();
List<String> collect = menu.parallelStream().
filter(d -> d.getCalorias() > 300).map(Refeicao::getNome).collect(toList());
System.out.println(collect);
[/java]
Infelizmente, converter processamento imperative para Streams pode ser uma tarefa extremamente complexa, visto que precisamos analisar instruções de controle de fluxo tais como: break, continue e return e verificar qual operação da API de Streams devemos utilizar. A boa notícia é que existe boas ferramentas para lhe ajudar nessa tarefa[²].
Refatorando padrões de projeto com lambdas
Sempre quando uma nova versão de uma linguagem é lançada algumas formas ou hábitos que tínhamos correm o risco de serem substituídos. Por exemplo, com a introdução do laço for-each na versão Java 5 praticamente se eliminou de nosso código a iteração em uma Collection através do objeto Iterator, ele ainda existe, em alguns casos o utilizamos, mas o for-each acabou o substituindo no nosso dia-a-dia.
Não vou nesse artigo conceituar e me aprofundar no assunto padrões de projeto. Imaginando que você já conheça ou tenha implementado alguns dos padrões de projeto mais comuns que existem no mercado, vamos apenas lembrar que padrões de projeto são soluções reutilizáveis para problemas comuns/recorrentes no desenvolvimento de software.
Através das expressões lambdas do Java 8 ganhamos uma nova e poderosa ferramenta para implementarmos padrões de projeto de uma forma mais clara, concisa e robusta. Com as expressões lambdas conseguimos atingir os objetivos dos padrões de projeto com menos trabalho e de uma forma mais simples. A maior parte do padrões de projeto que usamos na orientação a objetos podem ser escrito de uma forma mais clara utilizando expressões lambdas. Vamos analisar em especial quatro padrões de projeto:
- Strategy
- Template Method
- Observer
- Factory
Vamos ilustrar como através das expressões lambdas podemos implementar de uma forma alternativa os padrões de projeto acima.
Strategy
O padrão de projeto Strategy é um dos mais famosos e utilizados padrões de projeto do mercado. Seu propósito é implementar uma família de algoritmos com baixo acoplamento, de forma que podemos no momento da execução escolher qual dessas implementações queremos utilizar. O padrão Strategy pode ser aplicado em múltiplos cenários, praticamente em qualquer aplicação de médio porte já conseguimos visualizar aplicações para ele, alguns dos cenários mais comuns são: validação de entrada de dados através de diferentes critérios, diferentes formas de fazer parsing, ou formatação de uma entrada de dados.
Figura 1. O padrão de projeto Strategy
O padrão de projeto Strategy consiste em três partes, conforme ilustrado na figura 1.
- Uma interface que representa algum algoritmo
- Uma ou mais classes concretas que implementam essa interface que representam os diversos algoritmos
- Um ou mais clientes que usam essas classes concretas
Vamos imaginar que precisamos validar uma entrada de texto através de diversas formas(por exemplo, texto em caixa baixa, ou se o texto é numérico, etc). Começamos definindo uma interface para validar o texto(representando como um String):
Listagem 15. Refatorando o padrão de projeto Strategy
[java]
public interface ValidacaoStrategy {
public boolean execute(String s);
}
[/java]
Agora, vamos criar uma ou mais implementações para essa interface:
Listagem 16. Refatorando o padrão de projeto Strategy
[java]
class IsCaixaBaixa implements ValidacaoStrategy {
@Override
public boolean execute(String s) {
return s.matches(“[a-z]+”);
}
}
class IsNumero implements ValidacaoStrategy {
@Override
public boolean execute(String s) {
return s.matches(“\\d+”);
}
}
[/java]
Listagem 17. Refatorando o padrão de projeto Strategy
[java]
public class Validador {
private final ValidacaoStrategy strategy;
public Validador(ValidacaoStrategy strategy) {
this.strategy = strategy;
}
public boolean validar(String s) {
return strategy.execute(s);
}
public static void main(String[] args) {
Validador validadorNumero = new Validador(new IsNumero());
System.out.println(validadorNumero.validar(“99999”));
Validador validadorCaixaBaixa = new Validador(new IsCaixaBaixa());
System.out.println(validadorCaixaBaixa.validar(“java”));
}
}
[/java]
Utilizando expressões lambdas
Nesse refactoring, o mais importante é identificarmos que a ValidacaoStrategy é uma interface funcional. Isso quer dizer que ao invés de criarmos novas classes para cada implementação diferente, podemos passar expressões lambdas diretamente, uma forma mais clara e enxuta de implementarmos o padrão strategy.
Listagem 18. Refatorando o padrão de projeto Strategy
[java]
public class ValidadorLambda {
private final ValidacaoStrategy strategy;
public ValidadorLambda(ValidacaoStrategy strategy) {
this.strategy = strategy;
}
public boolean validar(String s) {
return strategy.execute(s);
}
public static void main(String[] args) {
ValidadorLambda validadorNumero =
new ValidadorLambda((String s) -> s.matches(“\\d+”));
System.out.println(validadorNumero.validar(“99999”));
ValidadorLambda validadorCaixaBaixa =
new ValidadorLambda((String s) -> s.matches(“[a-z]+”));
System.out.println(validadorCaixaBaixa.validar(“java”));
}
}
[/java]
Como você pode perceber, através da implementação com expressões lambdas não precisamos mais criar uma classe para cada implementação da interface Strategy, a própria expressão lambda já encapsula um trecho de código, uma nova implementação.
Nota: Em resumo, uma interface funcional é uma unterface que contém um e apenas um método abstrato. Na API do Java encontramos diversas interfaces funcionais tais como: Runnable e Comparator.
Template Method
O padrão de projeto Template Method é útil quando queremos representar o esboço de um algoritmo sem abrir mão da flexibilidade, ou seja, conseguir de alguma forma, deixar uma porta aberta para mudarmos alguns pontos do algoritmo de acordo com a necessidade.
Apesar do seu entendimento ser um pouco abstrato, vamos imaginar o template method como um padrão de projeto que especifica um algoritmo mas possibilita que alteremos o seu comportamento para diversas situações.
Vamos analisar como o esse padrão de projeto funciona. Vamos imaginar que você precisa escrever um método que recebe o ID de um Cliente, esse método faz a consulta na base de dados por esse ID e então, com o Cliente recuperado ele passa esse objeto para um método que irá manipular o objeto Cliente.
Queremos escrever nosso método de forma que a implementação da manipulação do objeto Cliente fique flexível para diversas aplicações. Podemos escrever a seguinte classe abstrata para representar essa situação:
Listagem 19. Refatorando o padrão de projeto Template Method
[java]
public abstract class TemplateMethod {
public void processaCliente(int id) {
Cliente c = Database.buscarClientePorID(id);
manipularCliente(c);
}
public abstract void manipularCliente(Cliente c);
}
[/java]
O método manipularCliente(Cliente c) defini o esqueleto desse algoritmo, fica a cargo das subclasses da classe TemplateMethod definir como cada uma irá manipular o objeto Cliente. Diferente subclasses podem ter diferentes implementações para o método abstrato.
Utilizando expressões lambdas
Você pode enfrentar o mesmo problema, utilizando expressões lambdas, entretanto, sem a necessidade de criar uma subclasse para cada nova implementação que precisar construir.
Ao invés de uma nova subclasse podemos utilizar uma expressão lambda diferente. No caso do nosso exemplo, substituiremos o método abstrato por mais um parâmetro no método processaCliente, esse parâmetro é a interface: Consumer<T>, ela é uma interface funcional que defini um método accept(T t) no qual podemos utilizá-lo no nosso exemplo.
Listagem 20. Refatorando o padrão de projeto Template Method
[java]
public void processaCliente(int id, Consumer<Cliente> consumer) {
Cliente c = Database.buscarClientePorID(id);
consumer.accept(c);
}
[/java]
Podemos agora criar expressões lambdas para representar diferentes algoritmos sem a necessidade de uma subclasse, apenas passando uma expressão lambda para o método processa cliente:
Listagem 21. Refatorando o padrão de projeto Template Method
[java]
public static void main(String[] args) {
new TemplateMethod().processaCliente(10,
(Cliente c) -> System.out.println(c));
}
[/java]
Mais um exemplo de como podemos tornar a implementação dos nossos padrões de projeto mais limpas e enxutas!
Observer
O padrão de projeto Observer é útil quando temos a necessidade de que um objeto(Subject) queira notificar de forma automática uma lista de outros objetos(observers) quando um evento ocorre(por exemplo, uma mudança de estado em seu modelo). É um padrão largamente utilizado quando implementamos GUI no mundo desktop. Registramos um conjunto de Observers para um componente, por exemplo um botão. Quando o botão é clicado a lista de Observers é notificada e podem executar uma ação específica em resposta a esse evento. Entretanto, o padrão de projeto Observer não é limitado apenas a interfaces gráficas, existem diversas aplicações em que o padrão pode ser útil. A figura a seguir ilustra o seu diagrama UML:
Figura 2. O padrão de projeto Observer
Vamos analisar esse exemplo para entender melhor o funcionamento do padrão Observer e como podemos utilizar as expressões lambdas no seu contexto. Vamos imaginar um sistema de notificações personalizadas para um aplicativo como o Twitter. O conceito é simples: várias agências de jornais estão inscritas em um feed de notícias e querem receber uma notificação quando um tweet com alguma palavra-chave específica for enviado.
Primeiro vamos construir uma interface Observer será implementada por todos aqueles que queiram ser notificados quando um novo tweet estiver disponível. Essa interface terá um método (notify) no qual implementaremos a lógica específica de cada Observer.
Listagem 22. Refatorando o padrão de projeto Observer
[java]
interface Observer {
public void notify(String tweet);
}
[/java]
Podemos agora implementar diferentes Observers para cada palavra-chave que gostaríamos de capturar
Listagem 23. Refatorando o padrão de projeto Observer
[java]
class JavaObserver implements Observer {
@Override
public void notify(String tweet) {
if (tweet.contains(“Java”)) {
System.out.println(“Nova notícia sobre Java: ” + tweet);
}
}
}
class SQLObserver implements Observer {
@Override
public void notify(String tweet) {
if (tweet.contains(“SQL”)) {
System.out.println(“Nova notícia sobre SQL: ” + tweet);
}
}
}
class UMLObserver implements Observer {
@Override
public void notify(String tweet) {
if (tweet.contains(“UML”)) {
System.out.println(“Nova notícia sobre UML: ” + tweet);
}
}
}
[/java]
Vamos agora implementar a outra interface, o Subject. Vamos criar apenas dois métodos, um para registrarmos novos Observer e outro método para notifica-los:
Listagem 24. Refatorando o padrão de projeto Observer
[java]
interface Subject {
public void registerObserver(Observer observer);
public void notifyObservers(String tweet);
}
class Feed implements Subject {
private List<Observer> observerList = new ArrayList<Observer>();
@Override
public void registerObserver(Observer observer) {
this.observerList.add(observer);
}
@Override
public void notifyObservers(String tweet) {
observerList.forEach(o -> o.notify(tweet));
}
}
[/java]
É um exemplo bastante simples, a classe Feed mantém uma lista interna de Observer e os notifica quando o método notifyObservers for chamado. Abaixo o exemplo com um método main() fazendo essas chamadas:
Listagem 25. Refatorando o padrão de projeto Observer
[java]
public class FeedTeste {
public static void main(String[] args) {
Feed feed = new Feed();
feed.registerObserver(new JavaObserver());
feed.registerObserver(new UMLObserver());
feed.registerObserver(new SQLObserver());
feed.notifyObservers(“Novo treinamento de Java e SQL disponível!”);
}
}
[/java]
Utilizando expressões lambdas
Perceba que as diferentes classes que implementam a interface Observer estão todas fornecendo implementações diferentes de um único método, notify(String s). Nesse caso, podemos substituir essas implementações por expressões lambdas, visto que a interface Observer é uma interface funcional, podemos no momento do registro de um novo Observer já criar a sua implementação:
Listagem 26. Refatorando o padrão de projeto Observer
[java]
public class FeedTesteLambda {
public static void main(String[] args) {
Feed feed = new Feed();
feed.registerObserver((String tweet) -> {
if (tweet.contains(“Java”)) {
System.out.println(“Nova notícia sobre Java: ” + tweet);
}
});
feed.registerObserver((String tweet) -> {
if (tweet.contains(“SQL”)) {
System.out.println(“Nova notícia sobre SQL: ” + tweet);
}
});
feed.registerObserver((String tweet) -> {
if (tweet.contains(“UML”)) {
System.out.println(“Nova notícia sobre UML: ” + tweet);
}
});
feed.notifyObservers(“Novo treinamento de Java e SQL disponível!”);
}
}
[/java]
Em todas as implementações de Observer você conseguirá substituir por expressões lambdas? Sinceramente, acredito que não. No exemplo acima, conseguimos fazer a mudança pois a implementação do Observer era muito simples, em casos mais complexos que envolvem várias chamadas de métodos, validações, etc. Você continuará implementando-os como uma classe.
Factory
O padrão de projeto Factory é utilizado quando desejamos criar objetos sem expor a lógica da instância para o cliente. Por exemplo, imagine que está trabalhando para um Banco e precisa criar diversos produtos financeiros, tais como: empréstimo, ações, investimentos, etc.
Normalmente, criamos uma classe Factory com um método responsável pela criação desses diferentes objetos:
Listagem 27. Refatorando o padrão de projeto Factory
[java]
public class ProdutoFinanceiroFactory {
public static ProdutoFinanceiro criarProduto(String nome) {
switch (nome) {
case “emprestimo”:
return new Emprestimo();
case “acoes”:
return new Acoes();
case “investimento”:
return new Investimento();
default:
throw new RuntimeException(“Produto indisponível ” + nome);
}
}
}
[/java]
No exemplo acima, as classes Emprestimo, Acoes e Investimento são implementações da interface Produto. O método criarProduto pode ter uma lógica adicional e complexa para criar cada um desses objetos. Entretanto, a vantagem agora e que podemos criar um objeto sem expor os detalhes de sua configuração para o cliente. A criação do objeto ficará dessa forma:
[java]Produto p = ProdutoFinanceiroFactory.criarProduto(“acoes”);[/java]
Utilizando expressões lambdas
Podemos referenciar construtores da mesma forma como referenciamos métodos usando métodos de referência. Abaixo temos um exemplo de como seria a referência para o construtor da classe Acoes:
[java]
Supplier<Acoes> acoes = Acoes::new;
Acoes acao = acoes.get();
[/java]
Usando essa técnica, podemos reescrever o exemplo anterior, criando uma estrutura Map contendo os nomes de cada ProdutoFinanceiro e seu construtor.
Listagem 28. Refatorando o padrão de projeto Factory
[java]
public class ProdutoFinanceiroFactoryLambda {
private final static Map<String, Supplier<ProdutoFinanceiro>> map = new HashMap<>();
static {
map.put(“emprestimo”, Emprestimo::new);
map.put(“acoes”, Acoes::new);
map.put(“investimento”, Investimento::new);
}
public static ProdutoFinanceiro criarProduto(String nome) {
Supplier<ProdutoFinanceiro> supplier = map.get(nome);
if (supplier != null) {
return supplier.get();
} else {
throw new IllegalArgumentException(“Produto indisponível ” + nome);
}
}
}
[/java]
Conclusão
Sem dúvida nenhuma, as expressões lambdas e a API de Streams abrem um precedente nunca visto na linguagem Java. É dever de todo programador Java estar atento a essas mudanças e evoluir junto com a linguagem.
Os motivos e as motivações para mergulharmos no Java 8, são muitos, nesse breve artigo vimos uma pequena introdução de como as expressões lambdas podem mudar a forma como estamos acostumados a programar e pensar, tornando nosso código mais legível, coeso e enxuto.
Links
http://dig.cs.illinois.edu/papers/lambdaRefactoring.pdf
http://refactoring.info/tools/LambdaFicator/
http://c2.com/cgi/wiki?GangOfFour
Tadeu Barbosa ([email protected]) é formado em Ciência da Computação pela PUC-SP. Trabalha com Java há 13 anos grande parte desse tempo como Instrutor oficial da Sun Microsystems e Oracle, atualmente é Arquiteto JEE na CBDS. Possui as certificações: SCJP, SCWCD, OCMJEA, Oracle WLS 12c e Scrum Master.