Java 8 lambdas, Function.identity () ou t-> t

240

Eu tenho uma pergunta sobre o uso do Function.identity()método.

Imagine o seguinte código:

Arrays.asList("a", "b", "c")
          .stream()
          .map(Function.identity()) // <- This,
          .map(str -> str)          // <- is the same as this.
          .collect(Collectors.toMap(
                       Function.identity(), // <-- And this,
                       str -> str));        // <-- is the same as this.

Existe alguma razão para você usar em Function.identity()vez de str->str(ou vice-versa). Eu acho que a segunda opção é mais legível (uma questão de gosto, é claro). Mas, existe alguma razão "real" pela qual alguém deve ser preferido?

Przemysław Głębocki
fonte
6
Em última análise, não, isso não fará diferença.
fge
50
Qualquer um está bom. Vá com o que achar mais legível. (Não se preocupe, seja feliz.)
Brian Goetz
3
Eu preferiria t -> tsimplesmente porque é mais sucinto.
David Conrad
3
Pergunta ligeiramente não relacionada, mas alguém sabe por que os designers de linguagem fazem identity () retornar uma instância de Function em vez de ter um parâmetro do tipo T e retorná-lo para que o método possa ser usado com referências de método?
precisa saber é o seguinte
Eu argumentaria que é útil estar familiarizado com a palavra "identidade", pois ela tem um significado importante em outras áreas da programação funcional.
orbfish

Respostas:

312

Na implementação atual do JRE, Function.identity()sempre retornará a mesma instância, enquanto cada ocorrência de identifier -> identifiernão apenas criará sua própria instância, mas também terá uma classe de implementação distinta. Para mais detalhes, veja aqui .

O motivo é que o compilador gera um método sintético que mantém o corpo trivial dessa expressão lambda (no caso de x->x, equivalente a return identifier;) e informa ao tempo de execução para criar uma implementação da interface funcional que chama esse método. Portanto, o tempo de execução vê apenas métodos de destino diferentes e a implementação atual não analisa os métodos para descobrir se certos métodos são equivalentes.

Portanto, usar em Function.identity()vez de x -> xpode economizar um pouco de memória, mas isso não deve conduzir sua decisão se você realmente acha que x -> xé mais legível do que Function.identity().

Você também pode considerar que, ao compilar com as informações de depuração ativadas, o método sintético terá um atributo de depuração de linha apontando para a (s) linha (s) de código-fonte que contém a expressão lambda, portanto, você poderá encontrar a origem de uma Functioninstância específica durante a depuração . Por outro lado, ao encontrar a instância retornada Function.identity()durante a depuração de uma operação, você não saberá quem chamou esse método e passou a instância para a operação.

Holger
fonte
5
Boa resposta. Eu tenho algumas dúvidas sobre depuração. Como isso pode ser útil? É muito improvável obter o rastreamento da pilha de exceções envolvendo o x -> xquadro. Você sugere definir o ponto de interrupção para este lambda? Normalmente não é tão fácil de colocar o ponto de interrupção no lambda-expressão única (pelo menos em Eclipse) ...
Tagir Valeev
14
@ Tagir Valeev: você pode depurar o código que recebe uma função arbitrária e entrar no método de aplicação dessa função. Então você pode acabar no código fonte de uma expressão lambda. No caso de uma expressão lambda explícita, você saberá de onde vem a função e terá a chance de reconhecer em que lugar a decisão deve passar, embora uma função de identidade tenha sido tomada. Ao usar Function.identity()essa informação, é perdida. Em seguida, a cadeia de chamada pode ajudar em casos simples de pensar, a avaliação por exemplo multi-thread onde o iniciador original não está no rastreamento de pilha ...
Holger
2
Interessante neste contexto: blog.codefx.org/java/instances-non-capturing-lambdas
Wim Deblauwe 26/10/15
13
@Wim Deblauwe: Interessante, mas eu sempre veria o contrário: se um método de fábrica não declarar explicitamente em sua documentação que ele retornará uma nova instância em cada chamada, você não pode assumir que sim. Portanto, não deveria ser surpreendente se não acontecer. Afinal, esse é um grande motivo para usar métodos de fábrica em vez de new. new Foo(…)garantias para criar uma nova instância do tipo exato Foo, enquanto que, Foo.getInstance(…)pode retornar uma instância existente de (um subtipo de) Foo
Holger
93

No seu exemplo, não há grande diferença entre str -> stre Function.identity()uma vez que internamente é simples t->t.

Mas às vezes não podemos usar Function.identityporque não podemos usar a Function. Dê uma olhada aqui:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

isso irá compilar bem

int[] arrayOK = list.stream().mapToInt(i -> i).toArray();

mas se você tentar compilar

int[] arrayProblem = list.stream().mapToInt(Function.identity()).toArray();

você receberá um erro de compilação, pois mapToIntespera ToIntFunction, o que não está relacionado Function. Também ToIntFunctionnão tem identity()método.

Pshemo
fonte
3
Consulte stackoverflow.com/q/38034982/14731 para outro exemplo em que substituir i -> ipor Function.identity()resultará em um erro do compilador.
Gili
19
Eu prefiro mapToInt(Integer::intValue).
Shmosel
4
@shmosel está bem, mas vale a pena mencionar que ambas as soluções funcionarão de maneira semelhante, pois mapToInt(i -> i)é a simplificação de mapToInt( (Integer i) -> i.intValue()). Use a versão que você achar mais clara, pois para mim mapToInt(i -> i)mostra melhor as intenções desse código.
Pshemo
1
Eu acho que pode haver benefícios de desempenho no uso de referências de método, mas é principalmente apenas uma preferência pessoal. Acho isso mais descritivo, porque i -> iparece uma função de identidade, que não é neste caso.
Shmosel
@shmosel Não posso dizer muito sobre a diferença de desempenho, então você pode estar certo. Mas se o desempenho não for um problema, ficarei com ele, i -> ipois meu objetivo é mapear Integer para int (o que mapToIntsugere bastante bem) para não chamar explicitamente o intValue()método. Como esse mapeamento será alcançado não é realmente tão importante. Então, vamos apenas concordar em discordar, mas obrigado por apontar uma possível diferença de desempenho, precisarei dar uma olhada mais de perto nisso algum dia.
Pshemo
44

Da fonte JDK :

static <T> Function<T, T> identity() {
    return t -> t;
}

Portanto, não, desde que esteja sintaticamente correto.

JasonN
fonte
8
Gostaria de saber se isso invalida a resposta acima relacionada a um lambda criando um objeto - ou se esta é uma implementação específica.
orbfish
28
@orbfish: isso está perfeitamente alinhado. Cada ocorrência t->tno código-fonte pode criar um objeto e a implementação de Function.identity()é uma ocorrência. Portanto, todos os sites que chamam identity()compartilharão esse objeto, enquanto todos os sites que usarem explicitamente a expressão lambda t->tcriarão seu próprio objeto. O método Function.identity()não é especial de forma alguma, sempre que você cria um método de fábrica que encapsula uma expressão lambda comumente usada e chama esse método em vez de repetir a expressão lambda, você pode economizar memória, dada a implementação atual .
Holger
Estou supondo que isso ocorre porque o compilador otimiza a criação de um novo t->tobjeto cada vez que o método é chamado e recicla o mesmo sempre que o método é chamado?
Daniel Grey
1
@DanielGray a decisão é tomada em tempo de execução. O compilador insere uma invokedynamicinstrução que é vinculada em sua primeira execução executando o chamado método de autoinicialização, que no caso de expressões lambda está localizado no LambdaMetafactory. Essa implementação decide retornar um identificador para um construtor, um método de fábrica ou código sempre retornando o mesmo objeto. Ele também pode decidir retornar um link para um identificador já existente (o que atualmente não acontece).
21419 Holger
@ Holger Você tem certeza de que essa chamada à identidade não seria incorporada e, em seguida, seria potencialmente monomorfizada (e incorporada novamente)?
JasonN 01/09/19