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

240

Tengo una pregunta sobre el uso del Function.identity()método.

Imagine el siguiente 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.

¿Hay alguna razón por la que deba usar en Function.identity()lugar de str->str(o viceversa). Creo que la segunda opción es más legible (una cuestión de gustos, por supuesto). Pero, ¿hay alguna razón "real" por la que uno debería ser preferido?

Przemysław Głębocki
fuente
66
Finalmente, no, esto no hará la diferencia.
fge
50
Cualquiera esta bien. Ve con lo que creas que es más legible. (No te preocupes, sé feliz.)
Brian Goetz
3
Preferiría t -> tsimplemente porque es más sucinto.
David Conrad
3
Pregunta ligeramente no relacionada, pero ¿alguien sabe por qué los diseñadores de lenguaje hacen que la identidad () devuelva una instancia de Function en lugar de tener un parámetro de tipo T y devolverlo para que el método pueda usarse con referencias de método?
Kirill Rakhman
Yo diría que es útil estar familiarizado con la palabra "identidad", ya que tiene un significado importante en otras áreas de la programación funcional.
pez orb

Respuestas:

312

A partir de la implementación actual de JRE, Function.identity()siempre devolverá la misma instancia, mientras que cada aparición identifier -> identifierno solo creará su propia instancia, sino que incluso tendrá una clase de implementación distinta. Para más detalles, ver aquí .

La razón es que el compilador genera un método sintético que contiene el cuerpo trivial de esa expresión lambda (en el caso de x->x, equivalente a return identifier;) y le dice al tiempo de ejecución que cree una implementación de la interfaz funcional que llama a este método. Por lo tanto, el tiempo de ejecución solo ve diferentes métodos de destino y la implementación actual no analiza los métodos para averiguar si ciertos métodos son equivalentes.

Por lo tanto, usar en Function.identity()lugar de x -> xpodría ahorrar algo de memoria, pero eso no debería impulsar su decisión si realmente cree que x -> xes más legible que Function.identity().

También puede considerar que al compilar con la información de depuración habilitada, el método sintético tendrá un atributo de depuración de línea que apunta a la (s) línea (s) del código fuente que contiene la expresión lambda, por lo tanto, tiene la posibilidad de encontrar el origen de una Functioninstancia particular mientras se depura . Por el contrario, al encontrar la instancia devuelta por Function.identity()durante la depuración de una operación, no sabrá quién llamó a ese método y pasó la instancia a la operación.

Holger
fuente
55
Buena respuesta. Tengo algunas dudas sobre la depuración. ¿Cómo puede ser útil? Es muy poco probable que obtenga el seguimiento de la pila de excepciones que involucra el x -> xmarco. ¿Sugiere establecer el punto de interrupción para esta lambda? Por lo general, no es tan fácil poner el punto de interrupción en el lambda de expresión única (al menos en Eclipse) ...
Tagir Valeev
14
@Tagir Valeev: puede depurar el código que recibe una función arbitraria y pasar al método de aplicación de esa función. Entonces puede terminar en el código fuente de una expresión lambda. En el caso de una expresión lambda explícita, sabrá de dónde proviene la función y tendrá la oportunidad de reconocer en qué lugar se tomó la decisión de pasar a través de una función de identidad. Al usar Function.identity()esa información se pierde. Entonces, la cadena de llamadas puede ayudar en casos simples, pero piense, por ejemplo, en una evaluación de subprocesos múltiples donde el iniciador original no está en el seguimiento de la pila ...
Holger
2
Interesante en este contexto: blog.codefx.org/java/instances-non-capturing-lambdas
Wim Deblauwe
13
@Wim Deblauwe: Interesante, pero siempre lo vería al revés: si un método de fábrica no declara explícitamente en su documentación que devolverá una nueva instancia en cada invocación, no puede suponer que lo hará. Por lo tanto, no debería sorprender si no lo hace. Después de todo, esa es una gran razón para usar métodos de fábrica en lugar de new. new Foo(…)garantías para crear una nueva instancia del tipo exacto Foo, mientras que, Foo.getInstance(…)puede devolver una instancia existente de (un subtipo de) Foo...
Holger
93

En su ejemplo, no hay una gran diferencia entre str -> stry Function.identity()dado que internamente es simple t->t.

Pero a veces no podemos usar Function.identityporque no podemos usar a Function. Echa un vistazo aquí:

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

esto compilará bien

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

pero si intentas compilar

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

obtendrá un error de compilación como se mapToIntespera ToIntFunction, que no está relacionado con Function. Además ToIntFunctionno tiene identity()método.

Pshemo
fuente
3
Ver stackoverflow.com/q/38034982/14731 para otro ejemplo en el que la sustitución i -> icon Function.identity()dará lugar a un error de compilación.
Gili
19
Yo prefiero mapToInt(Integer::intValue).
shmosel
44
@shmosel está bien, pero vale la pena mencionar que ambas soluciones funcionarán de manera similar ya que mapToInt(i -> i)es una simplificación mapToInt( (Integer i) -> i.intValue()). Usa la versión que creas que es más clara, para mí mapToInt(i -> i)muestra mejor las intenciones de este código.
Pshemo
1
Creo que puede haber beneficios de rendimiento al usar referencias de métodos, pero es principalmente una preferencia personal. Lo encuentro más descriptivo, porque i -> iparece una función de identidad, que no es en este caso.
shmosel
@shmosel No puedo decir mucho sobre la diferencia de rendimiento, así que puede que tengas razón. Pero si el rendimiento no es un problema, me quedaré i -> iya que mi objetivo es asignar Integer a int (lo que mapToIntsugiere bastante bien) no llamar explícitamente al intValue()método. Cómo se logrará este mapeo no es realmente tan importante. Así que aceptemos estar en desacuerdo, pero gracias por señalar la posible diferencia de rendimiento, algún día tendré que analizarlo más de cerca.
Pshemo
44

De la fuente JDK :

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

Entonces, no, siempre que sea sintácticamente correcto.

JasonN
fuente
8
Me pregunto si esto invalida la respuesta anterior relacionada con una lambda que crea un objeto, o si esta es una implementación particular.
pez orb
28
@orbfish: eso está perfectamente en línea. Cada aparición t->ten el código fuente puede crear un objeto y la implementación de Function.identity()es una ocurrencia. Por lo tanto, todos los sitios de llamadas que invoquen identity()compartirán ese objeto, mientras que todos los sitios que usan explícitamente la expresión lambda t->tcrearán su propio objeto. El método Function.identity()no es especial de ninguna manera, siempre que cree un método de fábrica que encapsule una expresión lambda de uso común y llame a ese método en lugar de repetir la expresión lambda, puede ahorrar algo de memoria, dada la implementación actual .
Holger
Supongo que esto se debe a que el compilador optimiza la creación de un nuevo t->tobjeto cada vez que se llama al método y recicla el mismo cada vez que se llama al método.
Daniel Gray
1
@DanielGray la decisión se toma en tiempo de ejecución. El compilador inserta una invokedynamicinstrucción que se vincula en su primera ejecución ejecutando el llamado método bootstrap, que en el caso de las expresiones lambda se encuentra en el LambdaMetafactory. Esta implementación decide devolver un identificador a un constructor, un método de fábrica o un código que siempre devuelve el mismo objeto. También puede decidir devolver un enlace a un identificador ya existente (que actualmente no sucede).
Holger
@Holger ¿Está seguro de que esta llamada a la identidad no se alineará y luego podría volverse monomorfizada (y volver a alinearse)?
JasonN