Implied anonymous types inside lambdas

Federico Peralta Schaffner :

In this question, user @Holger provided an answer that shows an uncommon usage of anonymous classes, which I wasn't aware of.

That answer uses streams, but this question is not about streams, since this anonymous type construction can be used in other contexts, i.e.:

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);

To my surprise, this compiles and prints the expected output.


Note: I'm well aware that, since ancient times, it is possible to construct an anonymous inner class and use its members as follows:

int result = new Object() { int incr(int i) {return i + 1; } }.incr(3);
System.out.println(result); // 4

However, this is not what I'm asking here. My case is different, because the anonymous type is propagated through the Optional method chain.


Now, I can imagine a very useful usage for this feature... Many times, I've needed to issue some map operation over a Stream pipeline while also preserving the original element, i.e. suppose I have a list of people:

public class Person {
    Long id;
    String name, lastName;
    // getters, setters, hashCode, equals...
}

List<Person> people = ...;

And that I need to store a JSON representation of my Person instances in some repository, for which I need the JSON string for every Person instance, as well as each Person id:

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?

In this example, I have lost the Person.id field, since I've transformed every person to its corresponding json string.

To circumvent this, I've seen many people use some sort of Holder class, or Pair, or even Tuple, or just AbstractMap.SimpleEntry:

people.stream()
    .map(p -> new Pair<Long, String>(p.getId(), toJson(p)))
    .forEach(pair -> repository.add(pair.getLeft(), pair.getRight()));

While this is good enough for this simple example, it still requires the existence of a generic Pair class. And if we need to propagate 3 values through the stream, I think we could use a Tuple3 class, etc. Using an array is also an option, however it's not type safe, unless all the values are of the same type.

So, using an implied anonymous type, the same code above could be rewritten as follows:

people.stream()
    .map(p -> new Object() { Long id = p.getId(); String json = toJson(p); })
    .forEach(it -> repository.add(it.id, it.json));

It is magic! Now we can have as many fields as desired, while also preserving type safety.

While testing this, I wasn't able to use the implied type in separate lines of code. If I modify my original code as follows:

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);

I get a compilation error:

Error: java: cannot find symbol
  symbol:   variable field
  location: variable anonymous of type java.lang.Object

And this is to be expected, because there's no member named field in the Object class.

So I would like to know:

  • Is this documented somewhere or is there something about this in the JLS?
  • What limitations does this have, if any?
  • Is it actually safe to write code like this?
  • Is there a shorthand syntax for this, or is this the best we can do?
Holger :

This kind of usage has not been mentioned in the JLS, but, of course, the specification doesn’t work by enumerating all possibilities, the programming language offers. Instead, you have to apply the formal rules regarding types and they make no exceptions for anonymous types, in other words, the specification doesn’t say at any point, that the type of an expression has to fall back to the named super type in the case of anonymous classes.

Granted, I could have overlooked such a statement in the depths of the specification, but to me, it always looked natural that the only restriction regarding anonymous types stems from their anonymous nature, i.e. every language construct requiring referring to the type by name, can’t work with the type directly, so you have to pick a supertype.

So if the type of the expression new Object() { String field; } is the anonymous type containing the field “field”, not only the access new Object() { String field; }.field will work, but also Collections.singletonList(new Object() { String field; }).get(0).field, unless an explicit rule forbids it and consistently, the same applies to lambda expressions.

Starting with Java 10, you can use var to declare local variables whose type is inferred from the initializer. That way, you can now declare arbitrary local variables, not only lambda parameters, having the type of an anonymous class. E.g., the following works

var obj = new Object() { int i = 42; String s = "blah"; };
obj.i += 10;
System.out.println(obj.s);

Likewise, we can make the example of your question work:

var optional = Optional.of(new Object() { String field = s; });
optional.map(anonymous -> anonymous.field).ifPresent(System.out::println);

In this case, we can refer to the specification showing a similar example indicating that this is not an oversight but intended behavior:

var d = new Object() {};  // d has the type of the anonymous class

and another one hinting at the general possibility that a variable may have a non-denotable type:

var e = (CharSequence & Comparable<String>) "x";
                          // e has type CharSequence & Comparable<String>

That said, I have to warn about overusing the feature. Besides the readability concerns (you called it yourself an “uncommon usage”), each place where you use it, you are creating a distinct new class (compare to the “double brace initialization”). It’s not like an actual tuple type or unnamed type of other programming languages that would treat all occurrences of the same set of members equally.

Also, instances created like new Object() { String field = s; } consume twice as much memory as needed, as it will not only contain the declared fields, but also the captured values used to initialize the fields. In the new Object() { Long id = p.getId(); String json = toJson(p); } example, you pay for the storage of three references instead of two, as p has been captured. In a non-static context, anonymous inner class also always capture the surrounding this.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=428047&siteId=1