Capítulo 1 Olá, expressão lambda!
Seção 1
O estilo de codificação Java está enfrentando mudanças tremendas.
Nosso trabalho diário se tornará mais simples, conveniente e expressivo. Java, um novo método de programação, apareceu em outras linguagens de programação há décadas. Depois que esses novos recursos forem introduzidos em Java, poderemos escrever um código mais conciso, elegante, mais expressivo e com menos erros. Podemos implementar várias estratégias e padrões de design com menos código.
Neste livro exploraremos a programação de estilo funcional por meio de exemplos de programação cotidiana. Antes de usar essa maneira nova e elegante de projetar e codificar, vamos primeiro dar uma olhada no que há de tão bom nela.
mudou a maneira como você pensa
Estilo imperativo - Esta é a abordagem que a linguagem Java fornece desde o seu início. Usando esse estilo, temos que dizer ao Java o que fazer em cada etapa e então observá-lo realmente executá-lo passo a passo. É claro que isso é bom, mas parece um pouco rudimentar. O código parece um pouco detalhado e gostaríamos que a linguagem se tornasse um pouco mais inteligente, deveríamos apenas dizer o que queremos em vez de dizer como fazer; Felizmente, Java pode finalmente nos ajudar a realizar esse desejo. Vejamos alguns exemplos para entender as vantagens e diferenças desse estilo.
maneira normal
Vamos começar com dois exemplos familiares. Este é um método de comando para verificar se Chicago está na coleção de cidades especificada - lembre-se, o código listado neste livro é apenas um fragmento parcial.
Copie o código do código da seguinte forma:
booleano encontrado = falso;
for(String cidade: cidades) {
if(cidade.equals("Chicago")) {
encontrado = verdadeiro;
quebrar;
}
}
System.out.println("Encontrado Chicago?:" + encontrado);
Esta versão imperativa parece um pouco detalhada e rudimentar e é dividida em várias partes de execução; Primeiro, inicialize uma tag booleana chamada encontrada e, em seguida, percorra cada elemento da coleção, se a cidade que procuramos for encontrada, defina essa tag, saia do loop e finalmente imprima os resultados da pesquisa;
uma maneira melhor
Depois de ler este código, programadores Java cuidadosos pensarão rapidamente em uma maneira mais concisa e clara, como esta:
Copie o código do código da seguinte forma:
System.out.println("Chicago encontrado?:" + cidades.contains("Chicago"));
Este também é um estilo imperativo de escrita - o método contains faz isso diretamente para nós.
melhorias reais
Escrever código como este tem várias vantagens:
1. Chega de mexer com essa variável mutável
2. Encapsular a iteração na camada inferior
3. O código é mais simples
4. O código está mais claro e focado
5. Faça menos desvios e integre mais de perto o código e as necessidades de negócios
6. Menos sujeito a erros
7. Fácil de entender e manter
Vejamos um exemplo mais complicado.
Este exemplo é muito simples. A consulta imperativa se um elemento existe em uma coleção pode ser vista em qualquer lugar em Java. Agora suponha que queremos usar programação imperativa para realizar algumas operações mais avançadas, como análise de arquivos, interação com bancos de dados, chamada de serviços WEB, programação concorrente, etc. Agora podemos usar Java para escrever código mais conciso, elegante e livre de erros, não apenas neste cenário simples.
o jeito antigo
Vejamos outro exemplo. Definimos uma faixa de preços e calculamos o preço total com desconto de diferentes maneiras.
Copie o código do código da seguinte forma:
final List<BigDecimal> preços = Arrays.asList(
novo BigDecimal("10"), novo BigDecimal("30"), novo BigDecimal("17"),
novo BigDecimal("20"), novo BigDecimal("15"), novo BigDecimal("18"),
novo BigDecimal("45"), novo BigDecimal("12"));
Supondo que haja um desconto de 10% se exceder 20 yuans, vamos implementá-lo primeiro da maneira normal.
Copie o código do código da seguinte forma:
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;
for(preço BigDecimal: preços) {
if(preço.compareTo(BigDecimal.valueOf(20)) > 0)
totalDePreçosDescontados =
totalOfDiscountedPrices.add(preço.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("Total de preços com desconto: " + totalOfDiscountedPrices);
Este código deve ser muito familiar; primeiro use uma variável para armazenar o preço total; preço após desconto.
Aqui está a saída do programa:
Copie o código do código da seguinte forma:
Total de preços com desconto: 67,5
O resultado está totalmente correto, mas o código está um pouco confuso. Não temos culpa de só podermos escrever da maneira que escrevemos. No entanto, esse código é um pouco rudimentar. Ele não apenas sofre de paranóia básica, mas também viola o princípio da responsabilidade única. Se você trabalha em casa e tem filhos que querem ser programadores, você tem que esconder seu código, caso eles vejam e suspirem de decepção e digam: “Você ganha a vida fazendo isso”?
Existe uma maneira melhor
Podemos fazer melhor – e muito melhor. Nosso código é um pouco como uma especificação de requisitos. Isto pode diminuir a lacuna entre os requisitos de negócios e o código implementado, reduzindo a possibilidade de má interpretação dos requisitos.
Não permitimos mais que Java crie uma variável e atribua-a indefinidamente. Precisamos nos comunicar com ela a partir de um nível superior de abstração, como o código a seguir.
Copie o código do código da seguinte forma:
final BigDecimal totalOfDiscountedPrices =
preços.stream()
.filter(preço -> preço.compareTo(BigDecimal.valueOf(20)) > 0)
.map(preço -> preço.multiply(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("Total de preços com desconto: " + totalOfDiscountedPrices);
Leia em voz alta - filtre os preços superiores a 20 yuans, converta-os em preços com desconto e depois some-os. Este código é exatamente igual ao processo que usamos para descrever nossos requisitos. Em Java, também é muito conveniente dobrar uma longa linha de código e alinhá-la por linha de acordo com o ponto antes do nome do método, assim como acima.
O código é muito simples, mas usamos muitas coisas novas em Java8. Primeiro, chamamos um método stream da lista de preços. Isso abre a porta para inúmeros iteradores convenientes, que discutiremos mais tarde.
Usamos alguns métodos especiais, como filter e map, em vez de percorrer diretamente a lista inteira. Esses métodos não são como os do JDK que usamos antes, eles aceitam uma função anônima - expressão lambda - como parâmetro. (Discutiremos isso em profundidade mais tarde). Chamamos o método reduzir() para calcular a soma dos preços retornados pelo método map().
Assim como o método contains, o corpo do loop fica oculto. No entanto, o método do mapa (e o método do filtro) é muito mais complicado. Ele chama a expressão lambda passada para calcular cada preço na lista de preços e coloca o resultado em uma nova coleção. Finalmente chamamos o método reduzir nesta nova coleção para obter o resultado final.
Esta é a saída do código acima:
Copie o código do código da seguinte forma:
Total de preços com desconto: 67,5
áreas para melhoria
Esta é uma melhoria significativa em relação à implementação anterior:
1. Bem estruturado, mas não confuso
2. Sem operações de baixo nível
3. Fácil de aprimorar ou modificar a lógica
4. Iteração por biblioteca de métodos
5. Avaliação preguiçosa eficiente do corpo do loop
6. Facilmente paralelizado
Abaixo falaremos sobre como Java implementa isso.
As expressões lambda estão aqui para salvar o mundo
As expressões lambda são um atalho que nos salva dos problemas da programação imperativa. Este novo recurso fornecido pelo Java mudou nosso método de programação original, tornando o código que escrevemos não apenas conciso e elegante, menos sujeito a erros, mas também mais eficiente, fácil de otimizar, melhorar e paralelizar.
Seção 2: O maior ganho da programação funcional
O código de estilo funcional tem uma relação sinal-ruído mais alta; menos código é escrito, mas mais é feito por linha ou expressão. Comparada com a programação imperativa, a programação funcional nos beneficiou muito:
Evita-se a modificação ou atribuição explícita de variáveis, que muitas vezes são fonte de bugs e dificultam a paralelização do código. Na programação de linha de comando, atribuímos continuamente valores à variável totalOfDiscountedPrices no corpo do loop. No estilo funcional, o código não sofre mais operações de modificação explícitas. Quanto menos variáveis forem modificadas, menos bugs o código terá.
O código de estilo funcional pode ser facilmente paralelizado. Se o cálculo for demorado, podemos facilmente executar os elementos da lista simultaneamente. Se quisermos paralelizar o código imperativo, também teremos que nos preocupar com os problemas causados pela modificação simultânea da variável totalOfDiscountedPrices. Na programação funcional só acessamos essa variável após ela ter sido completamente processada, eliminando assim preocupações com a segurança do thread.
O código é mais expressivo. A programação imperativa é dividida em várias etapas para explicar o que fazer - criar um valor de inicialização, iterar pelos preços, adicionar preços com desconto às variáveis, etc. - enquanto a programação funcional só precisa que o método map da lista retorne um valor incluindo o desconto . Basta criar uma nova lista de preços e depois acumulá-los.
A programação funcional é mais simples; é necessário menos código para obter o mesmo resultado do que a programação imperativa. Código mais limpo significa menos código para escrever, menos para ler e menos para manter - consulte "É menos sucinto o suficiente para ser sucinto?"
O código funcional é mais intuitivo – ler o código é como descrever o problema – e é fácil de entender quando estamos familiarizados com a sintaxe. O método map executa a função fornecida (calcula o preço com desconto) para cada elemento da coleção e a seguir retorna o conjunto de resultados, conforme mostrado na figura abaixo.
Figura 1 - map executa a função dada em cada elemento da coleção
Com expressões lambda, podemos aproveitar ao máximo o poder da programação funcional em Java. Usando um estilo funcional, você pode escrever um código mais expressivo, conciso, com menos atribuições e menos erros.
O suporte para programação orientada a objetos é uma grande vantagem do Java. A programação funcional e a programação orientada a objetos não são mutuamente exclusivas. A verdadeira mudança de estilo é da programação de linha de comando para a programação declarativa. No Java 8, funcional e orientado a objetos podem ser integrados de forma eficaz. Podemos continuar a usar o estilo OOP para modelar entidades de domínio e seus estados e relacionamentos. Além disso, também podemos usar funções para modelar comportamento ou transições de estado, fluxo de trabalho e processamento de dados, e criar funções compostas.
Seção 3: Por que usar o estilo funcional?
Vimos as vantagens da programação funcional, mas vale a pena usar esse novo estilo? Isto é apenas uma pequena melhoria ou uma mudança completa? Ainda há muitas questões práticas que precisam ser respondidas antes de realmente dedicarmos tempo a isso.
Copie o código do código da seguinte forma:
Xiao Ming perguntou:
Menos código significa simplicidade?
Simplicidade significa menos, mas não desordem. Em última análise, significa ser capaz de expressar a intenção de forma eficaz. Os benefícios são de longo alcance.
Escrever código é como empilhar ingredientes. Simplicidade significa ser capaz de misturar ingredientes em temperos. Escrever código conciso requer muito trabalho. Há menos código para ler e o código verdadeiramente útil é transparente para você. Um código de acesso difícil de entender ou que oculta detalhes é curto e não conciso.
Código simples, na verdade, significa design ágil. Código simples sem burocracia. Isso significa que podemos testar ideias rapidamente, seguir em frente se funcionarem bem e pular rapidamente se não funcionarem bem.
Escrever código em Java não é difícil e a sintaxe é simples. E já conhecemos muito bem as bibliotecas e APIs existentes. O que é realmente difícil é usá-lo para desenvolver e manter aplicações de nível empresarial.
Precisamos garantir que os colegas fechem a conexão com o banco de dados no momento correto, que não continuem ocupando transações, que as exceções sejam tratadas corretamente na camada apropriada, que os bloqueios sejam adquiridos e liberados corretamente, etc.
Tomadas individualmente, qualquer uma dessas questões não é grande coisa. Contudo, quando combinado com a complexidade do campo, o problema torna-se muito difícil, os recursos de desenvolvimento são escassos e a manutenção é difícil.
O que aconteceria se encapsulassemos essas estratégias em muitos pequenos pedaços de código e deixássemos que elas realizassem o gerenciamento de restrições de forma independente? Assim não teremos que gastar energia constantemente para implementar estratégias. Esta é uma grande melhoria, vamos dar uma olhada em como a programação funcional faz isso.
Iterações malucas
Temos escrito várias iterações para processar listas, conjuntos e mapas. Usar iteradores em Java é muito comum, mas é muito complicado. Eles não apenas ocupam várias linhas de código, mas também são difíceis de encapsular.
Como percorremos a coleção e as imprimimos? Você pode usar um loop for. Como filtramos alguns elementos da coleção? Ainda use um loop for, mas você precisa adicionar algumas variáveis modificáveis adicionais. Após selecionar esses valores, como utilizá-los para encontrar o valor final, como valor mínimo, valor máximo, valor médio, etc.? Então você tem que reciclar e modificar as variáveis.
Esse tipo de iteração é como uma panacéia, pode fazer tudo, mas tudo é escasso. Java agora fornece iteradores integrados para muitas operações: por exemplo, aqueles que apenas fazem loops, aqueles que fazem operações de mapeamento, aqueles que filtram valores, aqueles que reduzem operações, e há muitas funções convenientes, como máximo, mínimo e média etc Além disso, essas operações podem ser bem combinadas, para que possamos juntá-las para implementar a lógica de negócios, que é simples e requer menos código. Além disso, o código escrito é altamente legível porque é logicamente consistente com a ordem de descrição do problema. Veremos vários exemplos desse tipo no Capítulo 2, Usando coleções, na página 19, e este livro está repleto de exemplos desse tipo.
Aplicar estratégia
As políticas são implementadas em todos os aplicativos corporativos. Por exemplo, precisamos confirmar se uma operação foi autenticada corretamente por questões de segurança, precisamos garantir que a transação possa ser executada rapidamente e que o log de modificação seja atualizado corretamente. Essas tarefas geralmente acabam sendo um trecho de código comum no lado do servidor, semelhante ao seguinte pseudocódigo:
Copie o código do código da seguinte forma:
Transação transação = getFromTransactionFactory();
//... operação a ser executada dentro da transação...
checkProgressAndCommitOrRollbackTransaction();
UpdateAuditTrail();
Existem dois problemas com esta abordagem. Primeiro, muitas vezes resulta na duplicação de esforços e também aumenta os custos de manutenção. Em segundo lugar, é fácil esquecer as exceções que podem ser lançadas no código comercial, o que pode afetar o ciclo de vida da transação e a atualização do log de modificação. Isso deve ser implementado usando blocos try e finalmente, mas toda vez que alguém toca neste código, temos que reconfirmar que esta estratégia não foi destruída.
Existe outra maneira, podemos remover a fábrica e colocar esse código na frente dela. Em vez de obter o objeto de transação, passe o código executado para uma função bem mantida, como esta:
Copie o código do código da seguinte forma:
runWithinTransaction((transação de transação) -> {
//... operação a ser executada dentro da transação...
});
É um pequeno passo para você, mas evita muitos problemas. A estratégia de verificar o status e atualizar o log ao mesmo tempo é abstraída e encapsulada no método runWithinTransaction. Enviamos para esse método um trecho de código que precisa ser executado no contexto de uma transação. Não precisamos mais nos preocupar com alguém se esquecendo de realizar esta etapa ou não tratando a exceção adequadamente. A função que implementa a política já cuida disso.
Abordaremos como usar expressões lambda para aplicar essa estratégia no Capítulo 5.
Estratégia de expansão
As estratégias parecem estar em toda parte. Além de aplicá-los, os aplicativos empresariais também precisam ampliá-los. Esperamos adicionar ou excluir algumas operações por meio de algumas informações de configuração. Em outras palavras, podemos processá-las antes que a lógica central do módulo seja executada. Isso é muito comum em Java, mas precisa ser pensado e projetado com antecedência.
Os componentes que precisam ser estendidos geralmente possuem uma ou mais interfaces. Precisamos projetar cuidadosamente a interface e a estrutura hierárquica das classes de implementação. Isso pode funcionar bem, mas deixará você com um monte de interfaces e classes que precisam ser mantidas. Tal projeto pode facilmente se tornar pesado e difícil de manter, anulando, em última análise, o propósito do dimensionamento.
Existe outra solução – interfaces funcionais e expressões lambda, que podemos usar para projetar estratégias escalonáveis. Não precisamos criar uma nova interface ou seguir o mesmo nome de método. Podemos nos concentrar mais na lógica de negócio a ser implementada, que mencionaremos no uso de expressões lambda para decoração na página 73.
Simultaneidade facilitada
Um grande aplicativo está se aproximando do marco de lançamento quando, de repente, surge um sério problema de desempenho. A equipe rapidamente determinou que o gargalo de desempenho estava em um módulo enorme que processa grandes quantidades de dados. Alguém da equipe sugeriu que o desempenho do sistema poderia ser melhorado se as vantagens do multi-core pudessem ser totalmente exploradas. No entanto, se este enorme módulo for escrito no antigo estilo Java, a alegria trazida por esta sugestão logo será destruída.
A equipe percebeu rapidamente que mudar esse gigante da execução serial para a execução paralela exigiria muito esforço, adicionaria complexidade extra e causaria facilmente BUGs relacionados a multithreading. Não existe uma maneira melhor de melhorar o desempenho?
É possível que o código serial e paralelo sejam iguais, independentemente de você escolher a execução serial ou paralela, como apertar um botão e expressar sua ideia?
Parece que isso só é possível em Nárnia, mas se nos desenvolvermos completamente em termos funcionais, tudo isso se tornará realidade. Iteradores integrados e estilo funcional removerão o último obstáculo à paralelização. O design do JDK permite alternar entre execução serial e paralela com apenas algumas alterações imperceptíveis de código, que mencionaremos em "Concluindo o salto para a paralelização" na página 145.
contar histórias
Muitas coisas são perdidas no processo de transformar requisitos de negócios em implementação de código. Quanto mais se perde, maior é a probabilidade de erro e o custo da gestão. Se o código parecer descrever os requisitos, será mais fácil de ler, será mais fácil discutir com o pessoal dos requisitos e será mais fácil atender às suas necessidades.
Por exemplo, você ouve o gerente de produto dizendo: “Obtenha os preços de todas as ações, encontre aquelas com preços superiores a 500 yuans e calcule o total de ativos que podem pagar dividendos”. Usando os novos recursos fornecidos pelo Java, você pode escrever:
Copie o código do código da seguinte forma:
tickers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum()
Este processo de conversão é quase sem perdas porque basicamente não há nada para converter. Este é o estilo funcional em ação, e você verá muitos outros exemplos disso ao longo do livro, especialmente no Capítulo 8, Construindo Programas com Expressões Lambda, página 137.
Foco na quarentena
No desenvolvimento de sistemas, o negócio principal e a lógica refinada que ele exige geralmente precisam ser isolados. Por exemplo, um sistema de processamento de pedidos pode querer utilizar diferentes estratégias de tributação para diferentes fontes de transação. Isolar os cálculos de impostos do restante da lógica de processamento torna o código mais reutilizável e escalonável.
Na programação orientada a objetos chamamos essa preocupação de isolamento, e o padrão de estratégia geralmente é usado para resolver esse problema. A solução geralmente é criar algumas interfaces e classes de implementação.
Podemos conseguir o mesmo efeito com menos código. Também podemos testar rapidamente nossas próprias ideias de produtos sem ter que criar um monte de códigos e estagnar. Exploraremos mais detalhadamente como criar esse padrão e realizar o isolamento de preocupações por meio de funções leves em Isolamento de preocupações usando expressões lambda na página 63.
avaliação preguiçosa
Ao desenvolver aplicações de nível empresarial, podemos interagir com serviços WEB, chamar bancos de dados, processar XML, etc. Há muitas operações que precisamos realizar, mas nem todas são necessárias o tempo todo. Evitar certas operações ou pelo menos atrasar algumas operações temporariamente desnecessárias é uma das maneiras mais fáceis de melhorar o desempenho ou reduzir a inicialização do programa e o tempo de resposta.
Isso é apenas uma coisa pequena, mas dá muito trabalho para implementá-lo de uma forma pura OOP. Para atrasar a inicialização de alguns objetos pesados, temos que lidar com várias referências de objetos, verificar se há ponteiros nulos, etc.
No entanto, se você usar a nova classe Optinal e algumas APIs de estilo funcional que ela fornece, esse processo se tornará muito simples e o código será mais claro. Discutiremos isso na inicialização lenta na página 105.
Melhore a testabilidade
Quanto menos lógica de processamento o código tiver, menor será a probabilidade de os erros serem corrigidos. De modo geral, o código funcional é mais fácil de modificar e testar.
Além disso, assim como no Capítulo 4, Projetando com Expressões Lambda e no Capítulo 5, Usando Recursos, as expressões lambda podem ser usadas como um objeto simulado leve para tornar o teste de exceção mais claro e fácil de entender. Expressões lambda também podem servir como um excelente auxílio para testes. Muitos casos de teste comuns podem aceitar e manipular expressões lambda. Os casos de teste escritos dessa maneira podem capturar a essência da funcionalidade que precisa ser testada por regressão. Ao mesmo tempo, várias implementações que precisam ser testadas podem ser concluídas passando diferentes expressões lambda.
Os próprios casos de teste automatizados do JDK também são um bom exemplo de aplicação de expressões lambda – se você quiser saber mais, pode dar uma olhada no código-fonte no repositório OpenJDK. Através desses programas de teste, você pode ver como as expressões lambda parametrizam os principais comportamentos do caso de teste, por exemplo, eles constroem o programa de teste assim, "Criar um contêiner para os resultados" e depois "Adicionar algumas pós-condições parametrizadas".
Vimos que a programação funcional não apenas nos permite escrever código de alta qualidade, mas também resolve vários problemas com elegância durante o processo de desenvolvimento. Isso significa que o desenvolvimento de programas se tornará mais rápido e fácil, com menos erros – desde que você siga algumas orientações que apresentaremos mais tarde.
Seção 4: Evolução, não revolução
Não precisamos mudar para outra linguagem para aproveitar os benefícios da programação funcional, tudo o que precisamos mudar é a forma como usamos Java; Linguagens como C++, Java e C# suportam programação imperativa e orientada a objetos. Mas agora eles estão começando a adotar a programação funcional. Acabamos de examinar os dois estilos de código e discutir os benefícios que a programação funcional pode trazer. Agora vamos dar uma olhada em alguns de seus principais conceitos e exemplos para nos ajudar a aprender esse novo estilo.
A equipe de desenvolvimento da linguagem Java gastou muito tempo e energia adicionando recursos de programação funcional à linguagem Java e ao JDK. Para aproveitar os benefícios que ele traz, primeiro temos que introduzir alguns conceitos novos. Podemos melhorar a qualidade do nosso código desde que sigamos as seguintes regras:
1. Declarativo
2. Promova a imutabilidade
3. Evite efeitos colaterais
4. Prefira expressões a declarações
5. Projete usando funções de ordem superior
Vamos dar uma olhada nessas diretrizes práticas.
declarativo
O núcleo do que conhecemos como programação imperativa é a variabilidade e a programação orientada por comandos. Criamos variáveis e depois modificamos continuamente seus valores. Também fornecemos instruções detalhadas para serem executadas, como gerar o flag de índice da iteração, incrementar seu valor, verificar se o loop terminou, atualizar o enésimo elemento do array, etc. No passado, devido às características das ferramentas e limitações de hardware, só podíamos escrever código desta forma. Também vimos que em uma coleção imutável, o método declarativo contains é mais fácil de usar do que o imperativo. Todos os problemas difíceis e operações de baixo nível são implementados nas funções da biblioteca e não precisamos mais nos preocupar com esses detalhes. Por uma questão de simplicidade, devemos também usar programação declarativa. A imutabilidade e a programação declarativa são a essência da programação funcional, e agora o Java finalmente torna isso realidade.
Promova a imutabilidade
Código com variáveis mutáveis terá muitos caminhos de atividade. Quanto mais coisas você muda, mais fácil é destruir a estrutura original e introduzir mais erros. Código com múltiplas variáveis sendo modificadas é difícil de entender e paralelizar. A imutabilidade elimina essencialmente essas preocupações. Java suporta imutabilidade, mas não exige isso - mas nós podemos. Precisamos mudar o velho hábito de modificar o estado do objeto. Devemos usar objetos imutáveis tanto quanto possível. Ao declarar variáveis, membros e parâmetros, tente declará-los como finais, assim como o famoso ditado de Joshua Bloch em "Java Efetivo", "Trate objetos como imutáveis". Ao criar objetos, tente criar objetos imutáveis, como String. Ao criar uma coleção, tente criar uma coleção imutável ou não modificável, como usar métodos como Arrays.asList() e Collections' unmodifiableList(). Ao evitar a variabilidade podemos escrever funções puras - isto é, funções sem efeitos colaterais.
evitar efeitos colaterais
Suponha que você esteja escrevendo um código para obter o preço de uma ação na Internet e gravá-lo em uma variável compartilhada. Se tivermos muitos preços para buscar, teremos que realizar essas operações demoradas em série. Se quisermos aproveitar o poder do multithreading, teremos que lidar com os problemas de threading e sincronização para evitar condições de corrida. O resultado final é que o desempenho do programa é muito ruim e as pessoas se esquecem de comer e dormir para manter o fio. Se os efeitos colaterais fossem eliminados, poderíamos evitar completamente esses problemas. Uma função sem efeitos colaterais promove a imutabilidade e não modifica nenhuma entrada ou qualquer outra coisa dentro de seu escopo. Esse tipo de função é altamente legível, apresenta poucos erros e é fácil de otimizar. Como não há efeitos colaterais, não há necessidade de se preocupar com condições de corrida ou modificações simultâneas. Além disso, podemos executar facilmente essas funções em paralelo, o que discutiremos na página 145.
Prefira expressões
As declarações são uma batata quente porque forçam a modificação. As expressões promovem a imutabilidade e a composição de funções. Por exemplo, primeiro usamos a instrução for para calcular o preço total após descontos. Esse código leva à variabilidade e ao código detalhado. Usar versões mais expressivas e declarativas dos métodos map e sum não apenas evita operações de modificação, mas também permite que funções sejam encadeadas. Ao escrever código, você deve tentar usar expressões em vez de instruções. Isso torna o código mais simples e fácil de entender. O código será executado de acordo com a lógica de negócios, assim como quando descrevemos o problema. Uma versão concisa é sem dúvida mais fácil de modificar se os requisitos mudarem.
Projete usando funções de ordem superior
Java não impõe imutabilidade como linguagens funcionais como Haskell, mas nos permite modificar variáveis. Portanto, Java não é e nunca será uma linguagem de programação puramente funcional. No entanto, podemos usar funções de ordem superior para programação funcional em Java. Funções de ordem superior levam a reutilização para o próximo nível. Com funções de alta ordem, podemos reutilizar facilmente código maduro que é pequeno, especializado e altamente coeso. Na POO, estamos acostumados a passar objetos para métodos, criar novos objetos nos métodos e depois retornar os objetos. Funções de ordem superior fazem com as funções as mesmas coisas que os métodos fazem com os objetos. Com funções de ordem superior, podemos.
1. Passe função para função
2. Crie uma nova função dentro da função
3. Retornar funções dentro de funções
Já vimos um exemplo de passagem de parâmetros de uma função para outra função, e mais tarde veremos exemplos de criação e retorno de funções. Vejamos o exemplo de “passagem de parâmetros para uma função” novamente:
Copie o código do código da seguinte forma:
preços.stream()
.filter(preço -> preço.compareTo(BigDecimal.valueOf(20)) > 0) .map(preço -> preço.multiply(BigDecimal.valueOf(0.9)))
errata do relatório • discutir
.reduce(BigDecimal.ZERO, BigDecimal::add);
Neste código, passamos a função price -> price.multiply(BigDecimal.valueOf(0.9)) para a função map. A função passada é criada quando o mapa de funções de ordem superior é chamado. De modo geral, uma função possui um corpo de função, um nome de função, uma lista de parâmetros e um valor de retorno. Esta função criada dinamicamente tem uma lista de parâmetros seguida por uma seta (->) e, em seguida, um pequeno corpo de função. Os tipos de parâmetros são deduzidos pelo compilador Java e o tipo de retorno também está implícito. Esta é uma função anônima, não tem nome. Mas não a chamamos de função anônima, chamamos de expressão lambda. Passar funções anônimas como parâmetros não é novidade em Java; muitas vezes já passamos classes internas anônimas; Mesmo que uma classe anônima tenha apenas um método, ainda teremos que passar pelo ritual de criar uma classe e instanciá-la. Com expressões lambda podemos desfrutar de uma sintaxe leve. Além disso, sempre estivemos acostumados a abstrair alguns conceitos em vários objetos, mas agora podemos abstrair alguns comportamentos em expressões lambda. Programar com esse estilo de codificação ainda requer alguma reflexão. Temos que transformar nosso pensamento imperativo já arraigado em pensamento funcional. Pode ser um pouco doloroso no início, mas logo você se acostumará. À medida que continuar a se aprofundar, essas APIs não funcionais serão gradualmente deixadas para trás. Vamos parar com este tópico primeiro. Vamos dar uma olhada em como Java lida com expressões lambda. Costumávamos sempre passar objetos para métodos, agora podemos armazenar funções e passá-las. Vamos dar uma olhada no segredo por trás da capacidade do Java de utilizar funções como parâmetros.
Seção 5: Adicionado um pouco de açúcar de sintaxe
Isso também pode ser conseguido usando as funções originais do Java, mas as expressões lambda adicionam um pouco de açúcar sintático, economizando algumas etapas e tornando nosso trabalho mais simples. O código escrito desta forma não apenas se desenvolve mais rapidamente, mas também expressa melhor nossas ideias. Muitas interfaces que usamos no passado tinham apenas um método: Runnable, Callable, etc. Essas interfaces podem ser encontradas em qualquer lugar da biblioteca JDK e, onde são usadas, geralmente podem ser feitas com uma função. Funções de biblioteca que antes exigiam apenas uma interface de método único agora podem passar funções leves, graças ao açúcar sintático fornecido pelas interfaces funcionais. Interface funcional é uma interface com apenas um método abstrato. Observe aquelas interfaces com apenas um método, Runnable, Callable, etc., esta definição se aplica a elas. Existem mais interfaces desse tipo no JDK8 - Função, Predicado, Consumidor, Fornecedor, etc. (página 157, o Apêndice 1 tem uma lista de interfaces mais detalhada). As interfaces funcionais podem ter vários métodos estáticos e métodos padrão, que são implementados na interface. Podemos usar a anotação @FunctionalInterface para anotar uma interface funcional. O compilador não utiliza essa anotação, mas consegue identificar com mais clareza o tipo dessa interface. Além disso, se anotarmos uma interface com esta anotação, o compilador verificará forçosamente se ela está em conformidade com as regras das interfaces funcionais. Se um método receber uma interface funcional como um parâmetro, os parâmetros que podemos passar incluem:
1. Classes internas anônimas, a maneira mais antiga
2. Lambda Expressão, assim como fizemos no método do mapa
3. Referência a um método ou construtor (falaremos sobre isso mais tarde)
Se o parâmetro do método for uma interface funcional, o compilador aceitará alegremente uma expressão ou referência de método lambda como um parâmetro. Se passarmos uma expressão lambda para um método, o compilador primeiro converterá a expressão em uma instância da interface funcional correspondente. Essa transformação é mais do que apenas gerar uma classe interna. Os métodos dessa instância gerada síncrona correspondem aos métodos abstratos da interface funcional do parâmetro. Por exemplo, o método do mapa recebe a função de interface funcional como um parâmetro. Ao chamar o método do mapa, o compilador Java o gerará de forma síncrona, como mostrado na figura abaixo.
Os parâmetros da expressão lambda devem corresponder aos parâmetros do método abstrato da interface. Este método gerado retornará o resultado da expressão lambda. Se o tipo de retorno não corresponder diretamente ao método abstrato, esse método converterá o valor de retorno no tipo apropriado. Já tivemos uma visão geral de como as expressões Lambda são passadas para os métodos. Vamos revisar rapidamente o que acabamos de falar e iniciar nossa exploração das expressões Lambda.
Resumir
Esta é uma área completamente nova do Java. Por meio de funções de ordem superior, agora podemos escrever um código de estilo funcional elegante e fluente. O código escrito dessa maneira é conciso e fácil de entender, tem poucos erros e é propício à manutenção e paralelização. O compilador Java trabalha sua mágica e, onde recebemos parâmetros de interface funcional, podemos passar em expressões lambda ou referências de método. Agora podemos entrar no mundo das expressões Lambda e as bibliotecas JDK adaptadas para que elas sintam a diversão deles. No próximo capítulo, começaremos com as operações mais comuns de programação e liberaremos o poder das expressões Lambda.