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?