Introdução ao Lambda
As expressões Lambda são um novo recurso importante nas expressões Java SE 8. Lambda permite que você substitua interfaces funcionais por expressões. A expressão Lambda é exatamente como o método, que fornece uma lista de parâmetros normal e um corpo (corpo, que pode ser uma expressão ou um bloco de código) que usa esses parâmetros.
As expressões Lambda também aprimoram a biblioteca de coleções. O Java SE 8 adiciona 2 pacotes que operam operações em lote nos dados da coleção: pacote java.util.function e pacote java.util.stream . Um fluxo é como um iterador, mas com muitos recursos adicionais anexados. Em geral, as expressões e fluxos Lambda são as maiores mudanças, pois o idioma Java adiciona genéricos e anotações.
As expressões Lambda são essencialmente métodos anônimos, e sua camada subjacente é implementada por meio de diretrizes invokedynamic para gerar classes anônimas. Ele fornece um método de sintaxe e escrita mais simples, permitindo substituir interfaces funcionais por expressões. Aos olhos de algumas pessoas, o Lambda pode tornar seu código mais conciso e não usá -lo - essa visão é certamente boa, mas o importante é que Lambda traz fechamentos para Java. Graças ao apoio da Lamdba às coleções, o Lambda melhorou bastante o desempenho ao atravessar coleções sob condições de processador multi-core. Além disso, podemos processar coleções na forma de fluxos de dados - o que é muito atraente.
Sintaxe Lambda
A sintaxe de Lambda é extremamente simples, semelhante à seguinte estrutura:
(parâmetros) -> expressão
ou
(parâmetros) -> {declarações; }As expressões lambda são compostas por três partes:
1. Parâmetros: Uma lista de parâmetros formais em métodos semelhantes, os parâmetros aqui são parâmetros na interface funcional. Os tipos de parâmetros aqui podem ser declarados explicitamente ou não declarados, mas implicitamente inferidos pela JVM. Além disso, quando existe apenas um tipo de inferência, os parênteses podem ser omitidos.
2. ->: Pode ser entendido como "sendo usado"
3. Método Corpo: Pode ser uma expressão ou um bloco de código, é a implementação do método na interface funcional. Um bloco de código pode retornar um valor ou reversão nada. O bloco de código aqui é equivalente ao corpo do método do método. Se for uma expressão, você também pode retornar um valor ou não retornar nada.
Vamos usar os seguintes exemplos para ilustrar:
// Exemplo 1: Não é necessário aceitar parâmetros, retorne diretamente 10 ()-> 10 // Exemplo 2: Aceite dois parâmetros do tipo int e retorne a soma desses dois parâmetros (int x, int y)-> x+y; // Exemplo 2: aceite dois parâmetros de x e y, o tipo deste parâmetro é inferido pelo jvm, com base no contexto e retorna e retorna o retorno do contexto e retorno do contexto e retorno do contexto, e o tipo de retorno do contexto e o que retorna do contexto e do tipo de contexto e, o tipo de 1, o tipo de 1, //). Aceite uma string e imprima a string para controlar, sem reverter o resultado (nome da string)-> system.out.println (nome); // Exemplo 4: Aceite um nome de parâmetro de tipo inferido e imprima a string para console o nome-> system.out.println (nome); // Exemplo 5: aceite dois strings e os parâmetros de string e oscendam-os separadamente, sem reverso (Nome); sexo)-> {System.out.println (nome); System.out.println (sexo)} // Exemplo 6: Aceite um parâmetro x e retorne duas vezes o parâmetro x-> 2*xOnde usar lambda
Na [interface funcional] [1] Sabemos que o tipo de expressão de lambda alvo é uma interface funcional - cada lambda pode corresponder a um determinado tipo através de uma interface funcional específica. Portanto, uma expressão de lambda pode ser aplicada em qualquer lugar que corresponda ao seu tipo de destino. A expressão lambda deve ter o mesmo tipo de parâmetro que a descrição da função abstrata da interface funcional, seu tipo de retorno também deve ser compatível com o tipo de retorno da função abstrata, e as exceções que ele pode lançar são limitadas ao intervalo de descrição da função.
Em seguida, vejamos um exemplo de interface funcional personalizada:
@FunctionAlInterface Interface Converter <f, t> {t convert (f de);}Primeiro, use a interface da maneira tradicional:
Converter <String, Integer> converter = new Converter <String, Integer> () {@Override public Integer convert (String de) {return Integer.ValueOf (de); }}; Inteiro resultado = converter.convert ("200"); System.out.println (resultado);Obviamente, não há problema com isso, então a próxima coisa é o momento em que Lambda entra em campo, usando o Lambda para implementar a interface do conversor:
Converter <String, Integer> converter = (param) -> Integer.valueof (param); Inteiro resultado = converter.convert ("101"); System.out.println (resultado);Através do exemplo acima, acho que você tem um entendimento simples do uso do Lambda. Abaixo, estamos usando um Runnable comumente usado para demonstrar:
No passado, poderíamos ter escrito este código:
novo thread (novo runnable () {@Override public void run () {System.out.println ("Hello Lambda");}}). start ();Em alguns casos, um grande número de classes anônimas pode fazer com que o código pareça confuso. Agora você pode usar o Lambda para simplificar:
novo thread (() -> system.out.println ("hello lambda")). start ();Referência do método
A referência do método é uma maneira simplificada de escrever expressões lambda. O método referenciado é na verdade uma implementação do corpo do método da expressão de Lambda, e sua estrutura de sintaxe é:
Objectref :: MethodName
O lado esquerdo pode ser o nome da classe ou o nome da instância, o meio é o símbolo de referência do método "::", e o lado direito é o nome do método correspondente.
As referências de método são divididas em três categorias:
1. Referência de método estático
Em alguns casos, podemos escrever código como este:
classe pública referenceCetest {public static void main (string [] args) {conversor <string, inteiro> converter = novo conversor <string, inteiro> () {@Override public Integer convert (string de) {return referenceTest.string2int (de); }}; converter.convert ("120"); } @FunctionAlInterface Interface Converter <f, t> {t convert (f de); } static int string2int (string de) {return integer.valueof (de); }}No momento, se você usar referências estáticas, o código será mais conciso:
Conversor <string, Integer> converter = referenceTest :: string2int; converter.convert ("120");2. Referência do método da instância
Também podemos escrever código como este:
classe pública referenceCetest {public static void main (string [] args) {converster <string, integer> converter = new Converter <string, inteiro> () {@Override public Integer convert (string de) {return helper (). string2int (de); }}; converter.convert ("120"); } @FunctionAlInterface Interface Converter <f, t> {t convert (f de); } classe estática auxiliar {public int string2int (string de) {return integer.valueof (de); }}}Além disso, o uso de métodos de exemplo para referência parecerá mais conciso:
Ajudante auxiliar = novo ajudante (); Conversor <string, Integer> converter = Helper :: string2int; converter.convert ("120");3. Referência do método construtor
Agora vamos demonstrar referências aos construtores. Primeiro, definimos um animal da classe pai:
classe animal {nome de string privado; private Int Age; Animal público (nome da corda, Int Age) {this.name = name; this.age = idade; } public void comportamento () {}} Em seguida, estamos definindo duas subclasses de animal: Dog、Bird
public class Bird estende Animal {Public Bird (nome da corda, Int Age) {super (nome, idade); } @Override public void Comportament () {System.out.println ("Fly"); }} classe cão estende Animal {public Dog (Nome da String, Int Age) {Super (Nome, Age); } @Override public void Comportament () {System.out.println ("Run"); }}Em seguida, definimos a interface da fábrica:
Factory de interface <t estende animal> {t cria (nome da string, Int Age); }Em seguida, usaremos o método tradicional para criar objetos de aulas de cães e pássaros:
Factory Factory = new Factory () {@Override Public Animal Create (Nome da String, Int Age) {Return New Dog (Nome, Age); }}; Factory.create ("Alias", 3); Factory = new Factory () {@Override Public Animal Create (Nome da String, Int Age) {Return New Bird (Nome, Age); }}; Factory.create ("Smook", 2);Eu escrevi mais de dez códigos apenas para criar dois objetos. Agora vamos tentar usar a referência do construtor:
Factory <iminal> DogFactory = Dog :: New; Cão de animais = DogFactory.Create ("Alias", 4); Fábrica <dird> pássarofactory = pássaro :: novo; Pássaro pássaro = pássarofactory.create ("smook", 3); Dessa forma, o código parece limpo e arrumado. Ao usar Dog::new para penetrar em objetos, selecione a função de criação correspondente assinando a função Factory.create .
Domínio de Lambda e restrições de acesso
O domínio é o escopo e os parâmetros na lista de parâmetros na expressão de Lambda são válidos dentro do escopo da expressão Lambda (domínio). Na expressão lambda, variáveis externas podem ser acessadas: variáveis locais, variáveis de classe e variáveis estáticas, mas o grau de limitações de operação é diferente.
Acesse variáveis locais
As variáveis locais fora da expressão de Lambda serão implicitamente compiladas pela JVM para o tipo final, para que possam ser acessadas apenas, mas não modificadas.
classe pública referenceCetest {public static void main (string [] args) {int n = 3; Calcular calcular = param -> {// n = 10; Erro de compilação retorna n + param; }; calcular.calculate (10); } @FunctionAlInterface Interface calcular {int calcular (int value); }}Acesse variáveis estáticas e de membros
Expressões lambda internas, variáveis estáticas e de membros são legíveis e graváveis.
classe pública referenceCetest {public int count = 1; public static int num = 2; public void test () {calcular calcular = param -> {num = 10; // modifique a variável estática contagem = 3; // modifique o retorno da variável de membro n + param; }; calcular.calculate (10); } public static void main (string [] args) {} @functionalInterface interface calcular {int calcular (int value); }}Lambda não pode acessar o método padrão de interface de função
O Java8 aprimora as interfaces, incluindo métodos padrão que podem adicionar definições de palavras -chave padrão às interfaces. Precisamos observar aqui que o acesso aos métodos padrão não suporta internamente.
Prática Lambda
Na seção [Interface funcional] [2], mencionamos que muitas interfaces funcionais são incorporadas no pacote java.util.function e agora explicaremos as interfaces funcionais comumente usadas.
Interface predicada
Digite um parâmetro e retorne um valor Boolean , que contém muitos métodos padrão para julgamento lógico:
@Test public void previctTest () {predicado <string> previc = (s) -> s.Length ()> 0; teste booleano = previc.test ("teste"); System.out.println ("O comprimento da string é maior que 0:" + teste); teste = previct.test (""); System.out.println ("O comprimento da string é maior que 0:" + teste); Predicado <ject> pre = objetos :: não -nulo; Objeto ob = null; teste = pre.test (OB); System.out.println ("Objeto não está vazio:" + teste); ob = new Object (); teste = pre.test (OB); System.out.println ("Objeto não está vazio:" + teste); }Interface da função
Receba um parâmetro e retorne um único resultado. O método padrão ( andThen ) pode encadear várias funções para formar um resultado Funtion composta (com entrada, saída).
@Test public void functionTest () {function <string, Integer> ToInteger = Integer :: Valueof; // O resultado da execução do ToInteger é usado como entrada para a segunda função de backtoString <String, string> backtoString = ToInteger.andThen (String :: valueof); String resultado = backtoString.Apply ("1234"); System.out.println (resultado); Função <inteiro, número inteiro> add = (i) -> {System.out.println ("Frist entrada:" + i); retornar i * 2; }; Função <inteiro, número inteiro> zero = add.andthen ((i) -> {System.out.println ("Segunda entrada:" + i); retorna i * 0;}); Inteiro res = zero.Apply (8); System.out.println (res); }Interface do fornecedor
Retorna o resultado de um determinado tipo. Ao contrário Function , Supplier não precisa aceitar parâmetros (fornecedor, com saída, mas sem entrada)
@Test public void SupplierTest () {Fornecedor <String> fornecedor = () -> "Valor do tipo especial"; String s = fornecedor.get (); System.out.println (s); }Interface do consumidor
Representa as operações que precisam ser executadas em um único parâmetro de entrada. Ao contrário Function , Consumer não retorna valor (consumidor, entrada, sem saída)
@Test public void ConsumerTest () {Consumer <Teger> add5 = (p) -> {System.out.println ("Valor antigo:" + p); p = p + 5; System.out.println ("novo valor:" + p); }; add5.Acept (10); } O uso das quatro interfaces acima representa os quatro tipos no pacote java.util.function . Depois de entender essas quatro interfaces funcionais, outras interfaces serão fáceis de entender. Agora vamos fazer um resumo simples:
Predicate é usado para julgamento lógico, Function é usada em locais onde existem entradas e saídas, Supplier é usado em locais onde não há entrada e saídas, e Consumer é usado em locais onde há entrada e nenhuma saída. Você pode conhecer os cenários de uso com base no significado de seu nome.
Fluxo
A Lambda traz fechamentos para o Java 8, o que é particularmente importante nas operações de coleta: o Java 8 suporta operações funcionais no fluxo de objetos de coleta. Além disso, a API do fluxo também é integrada à API de coleção, permitindo operações de lote nos objetos de coleta.
Vamos saber o fluxo.
O fluxo representa um fluxo de dados. Não possui estrutura de dados e não armazena elementos. Suas operações não alterarão o fluxo de origem, mas gerarão um novo fluxo. Como uma interface para dados operacionais, fornece filtragem, classificação, mapeamento e regulamentação. Esses métodos são divididos em duas categorias de acordo com o tipo de retorno: qualquer método que retorne o tipo de fluxo é chamado de método intermediário (operação intermediária) e o restante são métodos de conclusão (operação completa). O método de conclusão retorna um valor de algum tipo, enquanto o método intermediário retorna um novo fluxo. A chamada dos métodos intermediários geralmente é encadeada e o processo formará um pipeline. Quando o método final for chamado, ele fará com que o valor seja consumido imediatamente do pipeline. Aqui devemos lembrar: as operações de fluxo são executadas o mais "atrasado" possível, que é o que chamamos de "operações preguiçosas", o que ajudará a reduzir o uso de recursos e melhorar o desempenho. Para todas as operações intermediárias (exceto classificadas), elas são executadas no modo de atraso.
O fluxo não apenas fornece recursos poderosos de operação de dados, mas, mais importante, o fluxo suporta serial e paralelismo. O paralelismo permite que o fluxo tenha um melhor desempenho em processadores de vários núcleos.
O processo de uso do fluxo tem um padrão fixo:
1. Crie um fluxo
2. Através de operações intermediárias, "mudar" o fluxo original e gerar um novo fluxo
3. Use a operação de conclusão para gerar o resultado final
Aquilo é
Criar -> Alterar -> Completo
Criação de fluxo
Para uma coleção, ele pode ser criado chamando o stream() ou parallelStream() . Além disso, esses dois métodos também são implementados na interface de coleta. Para matrizes, eles podem ser criados pelo método estático de Stream of(T … values) . Além disso, as matrizes também fornecem suporte para fluxos.
Além de criar fluxos com base em coleções ou matrizes acima, você também pode criar um fluxo vazio através Steam.empty() ou usar o fluxo generate() para criar fluxos infinitos.
Vamos tomar o fluxo serial como exemplo para ilustrar vários métodos de fluxo intermediários e de conclusão comumente usados. Primeiro, crie uma coleção de listas:
List <String> lists = new ArrayList <String> (); lists.add ("A1"); lists.add ("A2"); lists.add ("B1"); lists.add ("B2"); lists.add ("B3"); lists.add ("O1");Método intermediário
Filtro
Combinado com a interface do predicado, filtra todos os elementos no objeto de streaming. Esta operação é uma operação intermediária, o que significa que você pode executar outras operações com base no resultado retornado pela operação.
public static void streamFilterTest () {lists.stream (). filtro ((s -> s.startswith ("a")))). foreach (system.out :: println); // equivalente ao predicado da operação acima <String> predicado = (s) -> s.startswith ("a"); lists.stream (). filtro (predicado) .foreach (system.out :: println); // predicado de filtragem contínua <string> predicado1 = (s -> s.endswith ("1")); lists.stream (). filtro (predicado) .Filter (predicado1) .foreach (System.out :: println); }Classificar (classificado)
Combinado com a interface do comparador, esta operação retorna uma visualização do fluxo classificado e a ordem do fluxo original não será alterada. As regras de agrupamento são especificadas através do comparador e o padrão é classificá -las em ordem natural.
public static void streamSortEdTest () {System.out.println ("comparador padrão"); lists.stream (). classificado (). filtro ((s -> s.startswith ("a"))). foreach (system.out :: println); System.out.println ("Comparador personalizado"); lists.stream (). classificado ((p1, p2) -> p2.compareto (p1)). filtro ((s -> s.startswith ("a")))). foreach (system.out :: println); }Mapa (mapa)
Combinado com a interface Function , esta operação pode mapear cada elemento no objeto Stream em outro elemento, percebendo a conversão do tipo de elemento.
public static void streammapTest () {lists.stream (). map (string :: touppercase) .sorted ((a, b) -> b.compareto (a)). foreach (system.out :: println); System.out.println ("Regras de mapeamento personalizado"); Function <string, string> function = (p) -> {return p + ".txt"; }; lists.stream (). map (string :: touppercase) .map (função) .sorted ((a, b) -> b.compareto (a)). foreach (system.out :: println); }O acima introduz brevemente três operações comumente usadas, o que simplifica bastante o processamento da coleção. Em seguida, apresentamos várias maneiras de concluir:
Método de acabamento
Após o processo de "transformação", o resultado precisa ser obtido, ou seja, a operação é concluída. Vejamos as operações relacionadas abaixo:
Corresponder
Usado para determinar se um predicate corresponde ao objeto de fluxo e, finalmente, retorna um resultado do tipo Boolean , por exemplo:
public static void streamMatchTest () {// retorna true, desde que um elemento no objeto Stream corresponda a boolean anystartwitha = lists.stream (). anymatch ((s -> s.startswith ("a"))); System.out.println (anystartwitha); // retorna true quando cada elemento no objeto Stream corresponde a boolean allStartwitha = lists.stream (). AllMatch ((s -> s.startswith ("a"))); System.out.println (Allstartwitha); }Coletar
Após a transformação, coletamos os elementos do fluxo transformado, como salvar esses elementos em uma coleção. No momento, podemos usar o método de coleta fornecido pelo Stream, por exemplo:
public static void streamcollectTest () {list <string> list = lists.stream (). filtro ((p) -> p.startswith ("a")). classificado (). colecion (collectors.tolist ()); System.out.println (list); }Contar
A contagem do tipo SQL é usada para contar o número total de elementos no fluxo, por exemplo:
public static void streamCountTest () {long count = lists.stream (). filtro ((s -> s.startswith ("a"))). count (); System.out.println (contagem); }Reduzir
reduce nos permite calcular elementos à nossa maneira ou associar elementos em um fluxo com algum padrão, por exemplo:
public static void streamReduceTest () {opcional <string> opcional = lists.stream (). classificado (). Reduce ((s1, s2) -> {System.out.println (s1 + "|" + s2); retorna s1 + "|" + s2;}); }Os resultados da execução são os seguintes:
A1 | A2A1 | A2 | B1A1 | A2 | B1 | B2A1 | A2 | B1 | B2 | B3A1 | A2 | B1 | B2 | B3 | O1
Fluxo paralelo versus fluxo serial
Até agora, introduzimos as operações intermediárias e concluídas comumente usadas. É claro que todos os exemplos são baseados no fluxo serial. Em seguida, apresentaremos o drama -chave - fluxo paralelo (fluxo paralelo). O fluxo paralelo é implementado com base na estrutura de decomposição paralela do fork-join e divide o conjunto de big data em vários dados pequenos e o entrega a diferentes encadeamentos para processamento. Dessa forma, o desempenho será bastante aprimorado sob a situação do processamento de vários núcleos. Isso é consistente com o conceito de design do MapReduce: grandes tarefas se tornam menores e pequenas tarefas são reatribuídas a diferentes máquinas para execução. Mas a pequena tarefa aqui é entregue a diferentes processadores.
Crie um fluxo paralelo via parallelStream() . Para verificar se os fluxos paralelos podem realmente melhorar o desempenho, executamos o seguinte código de teste:
Primeiro, crie uma coleção maior:
List <string> biglists = new ArrayList <> (); for (int i = 0; i <10000000; i ++) {uuid uuid = uuid.randomuuid (); biglists.add (uuid.toString ()); }Teste o tempo para classificar em fluxos seriais:
Void estático privado NotParalLelsTreamSortEdTest (List <String> biglists) {long startTime = System.nanotime (); long count = biglists.stream (). classificada (). count (); Long Endtime = System.nanotime (); Millis longo = timeUnit.nanoseconds.tomillis (EndTime - StartTime); System.out.println (System.out.printf ("Classificação serial: %d ms", milis)); }Teste o tempo para classificar em fluxos paralelos:
ParallelStreamSortEdTest (lista <String> biglists) {long startTime = System.nanotime (); long count = biglists.paralLelsTream (). classificada (). count (); Long Endtime = System.nanotime (); Millis longo = timeUnit.nanoseconds.tomillis (EndTime - StartTime); System.out.println (System.out.printf ("Paralelsorting: %D Ms", Millis)); }Os resultados são os seguintes:
Torno em série: 13336 ms
Classificação paralela: 6755 ms
Depois de ver isso, descobrimos que o desempenho melhorou em cerca de 50%. Você também acha que poderá usar parallel Stream no futuro? De fato, não é o caso. Se você ainda é um processador de núcleo único e o volume de dados não é grande, o streaming serial ainda é uma opção tão boa. Você também descobrirá que, em alguns casos, o desempenho dos fluxos seriais é melhor. Quanto ao uso específico, você precisa testá -lo primeiro e depois decidir de acordo com o cenário real.
Operação preguiçosa
Acima, conversamos sobre o fluxo em funcionamento o mais tarde possível, e aqui explicamos criando um fluxo infinito:
Primeiro, use o método generate o fluxo para criar uma sequência de números naturais e depois transformar o fluxo através map :
// classe de sequência incremental NatureSeq implementa o fornecedor <long> {long value = 0; @Override public Long get () {value ++; valor de retorno; }} public void streamcreateTest () {stream <long> stream = stream.GeReRe (new NatureSeq ()); System.out.println ("Número de elementos:"+stream.map ((param) -> {return param;}). Limite (1000) .count ()); }O resultado da execução é:
Número de elementos: 1000
Descobrimos que, no início, quaisquer operações intermediárias (como filter,map etc., mas sorted não podem ser feitas) estão bem. Ou seja, o processo de execução de operações intermediárias no fluxo e sobreviver a um novo fluxo não entra em vigor imediatamente (ou a operação map neste exemplo será executada para sempre e será bloqueada), e o fluxo começa a calcular quando o método de conclusão é encontrado. Através limit() , converta esse fluxo infinito em um fluxo finito.
Resumir
O exposto acima é todo o conteúdo da rápida introdução ao Java Lambda. Depois de ler este artigo, você tem um entendimento mais profundo do Java Lambda? Espero que este artigo seja útil para que todos aprendam Java Lambda.