java - una - Tipos anónimos implícitos dentro de las lambdas
no se puede convertir expresion lambda en el tipo delegado (3)
En esta pregunta , el usuario @Holger proporcionó una respuesta que muestra un uso poco común de clases anónimas, que no conocía.
Esa respuesta utiliza streams, pero esta pregunta no es sobre streams, ya que esta construcción de tipo anónimo se puede usar en otros contextos, es decir:
String s = "Digging into Java''s intricacies";
Optional.of(new Object() { String field = s; })
.map(anonymous -> anonymous.field) // anonymous implied type
.ifPresent(System.out::println);
Para mi sorpresa, esto compila e imprime el resultado esperado.
Nota: Soy consciente de que, desde la antigüedad, es posible construir una clase interna anónima y usar sus miembros de la siguiente manera:
int result = new Object() { int incr(int i) {return i + 1; } }.incr(3);
System.out.println(result); // 4
Sin embargo, esto no es lo que estoy preguntando aquí. Mi caso es diferente, porque el tipo anónimo se propaga a través de la cadena de método Optional
.
Ahora, puedo imaginar un uso muy útil para esta función ... Muchas veces, he necesitado emitir algunas operaciones de map
través de una tubería de Stream
y al mismo tiempo conservar el elemento original, es decir, supongo que tengo una lista de personas:
public class Person {
Long id;
String name, lastName;
// getters, setters, hashCode, equals...
}
List<Person> people = ...;
Y que necesito almacenar una representación JSON de mis instancias de Person
en algún repositorio, para lo cual necesito la cadena JSON para cada instancia de Person
, así como cada ID de Person
:
public static String toJson(Object obj) {
String json = ...; // serialize obj with some JSON lib
return json;
}
people.stream()
.map(person -> toJson(person))
.forEach(json -> repository.add(ID, json)); // where''s the ID?
En este ejemplo, he perdido el campo Person.id
, ya que he transformado a cada persona a su cadena json correspondiente.
Para evitar esto, he visto a muchas personas usar algún tipo de clase de Holder
, Pair
, o incluso Tuple
, o simplemente AbstractMap.SimpleEntry
:
people.stream()
.map(p -> new Pair<Long, String>(p.getId(), toJson(p)))
.forEach(pair -> repository.add(pair.getLeft(), pair.getRight()));
Si bien esto es lo suficientemente bueno para este simple ejemplo, aún requiere la existencia de una clase de Pair
genérico. Y si necesitamos propagar 3 valores a través del flujo, creo que podríamos usar una clase Tuple3
, etc. El uso de una matriz también es una opción, sin embargo, no es de tipo seguro, a menos que todos los valores sean del mismo tipo.
Por lo tanto, utilizando un tipo anónimo implícito, el mismo código anterior podría reescribirse de la siguiente manera:
people.stream()
.map(p -> new Object() { Long id = p.getId(); String json = toJson(p); })
.forEach(it -> repository.add(it.id, it.json));
¡Es mágico! Ahora podemos tener tantos campos como se desee, al mismo tiempo que se preserva la seguridad del tipo.
Al probar esto, no pude usar el tipo implícito en líneas de código separadas. Si modifico mi código original de la siguiente manera:
String s = "Digging into Java''s intricacies";
Optional<Object> optional = Optional.of(new Object() { String field = s; });
optional.map(anonymous -> anonymous.field)
.ifPresent(System.out::println);
Me sale un error de compilación:
Error: java: cannot find symbol
symbol: variable field
location: variable anonymous of type java.lang.Object
Y esto es de esperar, porque no hay ningún miembro llamado field
en la clase Object
.
Así que me gustaría saber:
- ¿Está esto documentado en alguna parte o hay algo sobre esto en el JLS?
- ¿Qué limitaciones tiene esto, si las hay?
- ¿Es realmente seguro escribir código como este?
- ¿Hay una sintaxis abreviada para esto, o es lo mejor que podemos hacer?
¿Está esto documentado en alguna parte o hay algo sobre esto en el JLS?
Creo que no es un caso especial en clase anónima que deba introducirse en JLS. Como mencionó en su pregunta, puede acceder directamente a los miembros anónimos de la clase, por ejemplo: incr(3)
.
Primero, veamos un ejemplo de clase local en su lugar, esto representará por qué la cadena con la clase anónima podría acceder a sus miembros. por ejemplo:
@Test
void localClass() throws Throwable {
class Foo {
private String foo = "bar";
}
Foo it = new Foo();
assertThat(it.foo, equalTo("bar"));
}
Como podemos ver, se puede acceder a los miembros de una clase local fuera de su alcance, incluso si sus miembros son privados.
Como @Holger ha mencionado anteriormente en su respuesta, el compilador creará una clase interna como EnclosingClass${digit}
para cada clase anónima. así que el Object{...}
tiene su propio tipo derivado del Object
. debido a que los métodos de la cadena lo devuelven, es de tipo propio EnclosingClass${digit}
lugar del tipo que se deriva de Object
. esta es la razón por la que encadena la instancia de clase anónima podría funcionar bien.
@Test
void chainingAnonymousClassInstance() throws Throwable {
String foo = chain(new Object() { String foo = "bar"; }).foo;
assertThat(foo,equalTo("bar"));
}
private <T> T chain(T instance) {
return instance;
}
Debido a que no podemos hacer referencia directa a la clase anónima, por lo tanto, cuando dividimos los métodos de la cadena en dos líneas, en realidad hacemos referencia al tipo Object
que se deriva
Y el resto pregunta @Holger ha contestado.
Editar
podemos concluir que esta construcción es posible siempre que el tipo anónimo esté representado por una variable de tipo genérico?
Lo siento, no puedo volver a encontrar la referencia de JLS ya que mi inglés es malo. Pero te puedo decir que sí. Puedes usar el comando javap
para ver los detalles. por ejemplo:
public class Main {
void test() {
int count = chain(new Object() { int count = 1; }).count;
}
<T> T chain(T it) {
return it;
}
}
y puedes ver que la instrucción de checkcast
ha invocado a continuación:
void test();
descriptor: ()V
0: aload_0
1: new #2 // class Main$1
4: dup
5: aload_0
6: invokespecial #3 // Method Main$1."<init>":(LMain;)V
9: invokevirtual #4 // Method chain:(Ljava/lang/Object;)Ljava/lang/Object;
12: checkcast #2 // class Main$1
15: getfield #5 // Field Main$1.count:I
18: istore_1
19: return
Absolutamente no es una respuesta, pero más de 0.02$
.
Esto es posible porque las lambdas le dan una variable que es inferida por el compilador; se infiere del contexto. Es por eso que solo es posible para los tipos que se deducen , no para los tipos que podemos declarar .
El compilador puede deduce
el tipo como anónimo, es solo que no puede expresarlo para que podamos usarlo por su nombre . Así que la información está ahí , pero debido a las restricciones de idioma no podemos acceder a ella.
Es como decir:
Stream<TypeICanUseButTypeICantName> // Stream<YouKnowWho>?
No funciona en su último ejemplo porque obviamente le ha dicho al compilador que el tipo sea: Optional<Object> optional
, rompiendo así la inferencia de anonymous type
.
Estos tipos anónimos ahora están disponibles (de forma java-10
) de una manera mucho más sencilla:
var x = new Object() {
int y;
int z;
};
int test = x.y;
Como var x
es inferido por el compilador, int test = xy;
trabajará también
Este tipo de uso no se ha mencionado en el JLS, pero, por supuesto, la especificación no funciona al enumerar todas las posibilidades, ofrece el lenguaje de programación. En su lugar, tiene que aplicar las reglas formales con respecto a los tipos y ellos no hacen excepciones para los tipos anónimos, en otras palabras, la especificación no dice en ningún momento que el tipo de una expresión tiene que recurrir al tipo super indicado en El caso de clases anónimas.
Por supuesto, podría haber pasado por alto tal afirmación en las profundidades de la especificación, pero para mí, siempre me pareció natural que la única restricción con respecto a los tipos anónimos se derive de su naturaleza anónima , es decir, cada construcción de lenguaje que requiera referirse al tipo por nombre , puede No trabaje con el tipo directamente, por lo que debe elegir un supertipo.
Entonces, si el tipo de la expresión new Object() { String field; }
new Object() { String field; }
es el tipo anónimo que contiene el campo " field
", no solo el acceso al new Object() { String field; }.field
new Object() { String field; }.field
funcionará, pero también Collections.singletonList(new Object() { String field; }).get(0).field
, a menos que una regla explícita lo prohíba y de manera consistente, lo mismo se aplica a las expresiones lambda.
A partir de Java 10, puede usar var
para declarar variables locales cuyo tipo se deduce del inicializador. De esa manera, ahora puede declarar variables locales arbitrarias, no solo los parámetros lambda, que tienen el tipo de una clase anónima. Por ejemplo, las siguientes obras.
var obj = new Object() { int i = 42; String s = "blah"; };
obj.i += 10;
System.out.println(obj.s);
Del mismo modo, podemos hacer que el ejemplo de su pregunta funcione:
var optional = Optional.of(new Object() { String field = s; });
optional.map(anonymous -> anonymous.field).ifPresent(System.out::println);
En este caso, podemos referirnos a la especificación que muestra un ejemplo similar que indica que esto no es una supervisión sino un comportamiento previsto:
var d = new Object() {}; // d has the type of the anonymous class
y otro que sugiere la posibilidad general de que una variable pueda tener un tipo no denotable:
var e = (CharSequence & Comparable<String>) "x"; // e has type CharSequence & Comparable<String>
Dicho esto, debo advertir sobre el uso excesivo de la función. Además de los problemas de legibilidad (usted mismo lo llamó un "uso poco común"), cada lugar donde lo usa, está creando una nueva clase distinta (en comparación con la "inicialización de doble refuerzo"). No es como un tipo de tupla real o un tipo sin nombre de otros lenguajes de programación que trataría a todas las ocurrencias del mismo conjunto de miembros por igual.
Además, las instancias creadas como new Object() { String field = s; }
new Object() { String field = s; }
consume el doble de memoria que la necesaria, ya que no solo contendrá los campos declarados, sino también los valores capturados utilizados para inicializar los campos. En el new Object() { Long id = p.getId(); String json = toJson(p); }
new Object() { Long id = p.getId(); String json = toJson(p); }
new Object() { Long id = p.getId(); String json = toJson(p); }
ejemplo, usted paga por el almacenamiento de tres referencias en lugar de dos, ya que p
ha sido capturado. En un contexto no estático, la clase interna anónima también siempre captura el entorno.