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

240

J'ai une question concernant l'utilisation de la Function.identity()méthode.

Imaginez le code suivant:

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.

Y a-t-il une raison pour laquelle vous devriez utiliser à la Function.identity()place de str->str(ou vice versa). Je pense que la deuxième option est plus lisible (une question de goût bien sûr). Mais, y a-t-il une «vraie» raison pour laquelle on devrait être préféré?

Przemysław Głębocki
la source
6
En fin de compte, non, cela ne fera aucune différence.
fge
50
N'importe quel. Allez avec ce que vous pensez être plus lisible. (Ne vous inquiétez pas, soyez heureux.)
Brian Goetz
3
Je préférerais t -> tsimplement parce que c'est plus succinct.
David Conrad
3
Question légèrement indépendante, mais quelqu'un sait-il pourquoi les concepteurs de langage font qu'identity () retourne une instance de Function au lieu d'avoir un paramètre de type T et le renvoie pour que la méthode puisse être utilisée avec des références de méthode?
Kirill Rakhman
Je dirais qu'il est utile d'être familier avec le mot «identité», car il a une signification importante dans d'autres domaines de la programmation fonctionnelle.
orbfish

Réponses:

312

À partir de l'implémentation JRE actuelle, Function.identity()retournera toujours la même instance tandis que chaque occurrence de identifier -> identifiercréera non seulement sa propre instance mais aura même une classe d'implémentation distincte. Pour plus de détails, voir ici .

La raison en est que le compilateur génère une méthode synthétique contenant le corps trivial de cette expression lambda (dans le cas de x->x, équivalent à return identifier;) et indique au runtime de créer une implémentation de l'interface fonctionnelle appelant cette méthode. Le runtime ne voit donc que différentes méthodes cibles et l'implémentation actuelle n'analyse pas les méthodes pour savoir si certaines méthodes sont équivalentes.

Donc, utiliser Function.identity()au lieu de x -> xpourrait économiser de la mémoire, mais cela ne devrait pas motiver votre décision si vous pensez vraiment que c'est x -> xplus lisible que Function.identity().

Vous pouvez également considérer que lors de la compilation avec les informations de débogage activées, la méthode synthétique aura un attribut de débogage de ligne pointant vers la ou les lignes de code source contenant l'expression lambda, vous avez donc une chance de trouver la source d'une Functioninstance particulière lors du débogage . En revanche, lorsque vous rencontrez l'instance retournée par Function.identity()lors du débogage d'une opération, vous ne saurez pas qui a appelé cette méthode et transmis l'instance à l'opération.

Holger
la source
5
Bonne réponse. J'ai quelques doutes sur le débogage. Comment cela peut-il être utile? Il est très peu probable d'obtenir la trace de pile d'exceptions impliquant une x -> xtrame. Suggérez-vous de définir le point d'arrêt à cette lambda? Habituellement, il n'est pas si facile de mettre le point d'arrêt dans la lambda à expression simple (au moins dans Eclipse) ...
Tagir Valeev
14
@Tagir Valeev: vous pouvez déboguer du code qui reçoit une fonction arbitraire et entrer dans la méthode d'application de cette fonction. Ensuite, vous pouvez vous retrouver au code source d'une expression lambda. Dans le cas d'une expression lambda explicite, vous saurez d'où vient la fonction et aurez une chance de reconnaître à quel endroit la décision de passer si une fonction d'identité a été prise. Lors de l'utilisation de Function.identity()cette information est perdue. Ensuite, la chaîne d'appel peut aider dans des cas simples, mais pensez, par exemple, à une évaluation multithread où l'initiateur d'origine n'est pas dans la trace de la pile…
Holger
2
Intéressant dans ce contexte: blog.codefx.org/java/instances-non-capturing-lambdas
Wim Deblauwe
13
@Wim Deblauwe: Intéressant, mais je verrais toujours l'inverse: si une méthode d'usine ne précise pas explicitement dans sa documentation qu'elle renverra une nouvelle instance à chaque appel, vous ne pouvez pas supposer qu'elle le sera. Il ne devrait donc pas être surprenant que ce ne soit pas le cas. Après tout, c'est une grande raison d'utiliser des méthodes d'usine au lieu de new. new Foo(…)garanties pour créer une nouvelle instance du type exact Foo, alors que, Foo.getInstance(…)peut renvoyer une instance existante de (un sous-type de) Foo
Holger
93

Dans votre exemple, il n'y a pas de grande différence entre str -> stret Function.identity()puisque c'est en interne simplement t->t.

Mais parfois, nous ne pouvons pas utiliser Function.identityparce que nous ne pouvons pas utiliser un Function. Jetez un œil ici:

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

cela compilera bien

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

mais si vous essayez de compiler

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

vous obtiendrez une erreur de compilation depuis mapToIntattend ToIntFunction, ce qui n'est pas lié à Function. N'a ToIntFunctionpas non plus de identity()méthode.

Pshemo
la source
3
Voir stackoverflow.com/q/38034982/14731 pour un autre exemple où le remplacement i -> ipar Function.identity()entraînera une erreur de compilation.
Gili
19
Je préfère mapToInt(Integer::intValue).
shmosel
4
@shmosel c'est OK mais il vaut la peine de mentionner que les deux solutions fonctionneront de la même manière puisque mapToInt(i -> i)c'est la simplification de mapToInt( (Integer i) -> i.intValue()). Utilisez la version que vous pensez la plus claire, pour moi, cela mapToInt(i -> i)montre mieux les intentions de ce code.
Pshemo
1
Je pense qu'il peut y avoir des avantages en termes de performances à utiliser des références de méthode, mais c'est surtout une préférence personnelle. Je le trouve plus descriptif, car il i -> iressemble à une fonction d'identité, ce qui n'est pas le cas dans ce cas.
shmosel
@shmosel Je ne peux pas dire grand-chose sur la différence de performances, vous avez donc peut-être raison. Mais si les performances ne sont pas un problème, je resterai avec i -> icar mon objectif est de mapper Integer à int (ce qui mapToIntsuggère très bien) de ne pas appeler explicitement la intValue()méthode. La manière dont cette cartographie sera réalisée n'est pas vraiment importante. Donc, acceptons simplement d'être en désaccord, mais merci d'avoir signalé une éventuelle différence de performances, je devrai y regarder un jour de plus près.
Pshemo
44

De la source JDK :

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

Donc, non, tant qu'il est syntaxiquement correct.

JasonN
la source
8
Je me demande si cela invalide la réponse ci-dessus concernant un lambda créant un objet - ou si c'est une implémentation particulière.
orbfish
28
@orbfish: c'est parfaitement en ligne. Chaque occurrence de t->tdans le code source peut créer un objet et l'implémentation de Function.identity()est une occurrence. Ainsi, tous les sites d'appel invoquant identity()partageront cet objet tandis que tous les sites utilisant explicitement l'expression lambda t->tcréeront leur propre objet. La méthode Function.identity()n'a rien de spécial, chaque fois que vous créez une méthode d'usine encapsulant une expression lambda couramment utilisée et appelez cette méthode au lieu de répéter l'expression lambda, vous pouvez économiser de la mémoire, compte tenu de l'implémentation actuelle .
Holger
Je suppose que c'est parce que le compilateur optimise la création d'un nouvel t->tobjet à chaque appel de la méthode et recycle le même chaque fois que la méthode est appelée?
Daniel Gray
1
@DanielGray la décision est prise lors de l'exécution. Le compilateur insère une invokedynamicinstruction qui est liée lors de sa première exécution en exécutant une méthode dite de bootstrap, qui dans le cas des expressions lambda se trouve dans le LambdaMetafactory. Cette implémentation décide de renvoyer un handle vers un constructeur, une méthode d'usine ou un code renvoyant toujours le même objet. Il peut également décider de renvoyer un lien vers un descripteur déjà existant (ce qui ne se produit pas actuellement).
Holger
@Holger Êtes-vous sûr que cet appel à l'identité ne serait pas intégré, puis potentiellement monomorphisé (et à nouveau intégré)?
JasonN