Abstract: Anonymous inner classes can be used effectively in Java 8 Streams to create tuples. See how this can improve our refactoring of old procedural code into the functional paradigm.
Welcome to the 263rd edition of The Java(tm) Specialists' Newsletter. In my previous newsletter, I mentioned my misery brought on by a lifetime of sitting. Encouraged by my old boss, I got a Lifespan walking desk, so this newsletter was written over two and a half hours, whilst I strolled 4.5km. That's slow, even for me. I overdid it on Monday by walking 14 km at my desk and my knees are a bit tender. Not used to walking this much. The whole experience of walking whilst typing is still quite new to me. It certainly makes my back feel much better and sleep comes quickly. On Monday I walked 14 km whilst working at my desk. The reason it works well for programmers is that we tend to get into a state of mind where we do not notice anything around us. It has happened to me a few times that whilst "in the zone" I had walk 5km without noticing it.
Free Heinz's Happy Hour Season 03 webinar series starting Thursday the 6th December: Join us every first Thursday of the month at 16:00 UTC for your cup of hot Java at Heinz's Happy Hour. Free to attend and take part. Already over 400 of your colleagues are coming. Topics will be whatever we dream up that month. Satisfaction guaranteed or your money back! www.javaspecialists.eu/webinars/
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
I try learn something new each day. This morning on the way home from the school run, I listened to the audio book "Why We Sleep" by Matthew Walker. Seems that it is a good idea to sleep in between lectures, because our brain can then process the data that we learned. I used to multitask this at university and combine lectures with sleep. Somehow that did not work as well as what Walker describes in his book. Even though I learn something new each day, usually the subject matter is not the Java Programming Language. Obviously there are hundreds of Java frameworks and classes that I am unfamiliar with, but I thought I had the basic language pretty much figured out. Until I watched Henri Tremblay at Oracle Code One and saw something that I did not know, dating back to Java 1.1. Did you know that this code is legal?
public class AnonymousClass { public static void main(String[] args) { new Object() { private void test() { System.out.println("anonymous test"); } }.test(); } }
I fired up an old VMWare instance with Windows XP and a copy of JDK 1.1.8. Curiously, the code did not compile in JDK 1.1, spewing out the error:
AnonymousClass.java:7: Method test() not found in class java.lang.Object. }.test(); ^ 1 error
It did compile in JDK 1.2 and I could even set the target to 1.1. I would thus assume a compiler bug in 1.1, rather than a deliberate language restriction, caused the compile to fail. Should I log a bug?
This would not have worked in any version of Java:
public class AnonymousClassBroken { public static void main(String[] args) { Object obj = new Object() { private void test() { System.out.println("anonymous test"); } }; obj.test(); // <-- cannot find symbol } }
However, since Java 10, we can use the new var
construct to create a handle to the anonymous object, like so:
public class AnonymousClassVar { public static void main(String... args) { var obj = new Object() { private void test() { System.out.println("anonymous test"); } }; obj.test(); // works! } }
This is not dynamic typing. Java still verifies at static compile time that the test() method exists.
Henri Tremblay used this trick nicely to simulate the idea of tuples in Java, using anonymous inner classes. We will illustrate this by means of some refactoring from the Apache OfBiz project. The method we want to improve is in class ModelReader:
public Map<String, TreeSet<String>> getEntitiesByPackage( Set<String> packageFilterSet, Set<String> entityFilterSet) { Map<String, TreeSet<String>> entitiesByPackage = new HashMap<>(); //put the entityNames TreeSets in a HashMap by packageName Iterator<String> ecIter = this.getEntityNames().iterator(); while (ecIter.hasNext()) { String entityName = ecIter.next(); ModelEntity entity = this.getModelEntity(entityName); String packageName = entity.getPackageName(); if (UtilValidate.isNotEmpty(packageFilterSet)) { // does it match any of these? boolean foundMatch = false; for (String packageFilter : packageFilterSet) { if (packageName.contains(packageFilter)) { foundMatch = true; } } if (!foundMatch) { continue; } } if (UtilValidate.isNotEmpty(entityFilterSet) && !entityFilterSet.contains(entityName)) { continue; } TreeSet<String> entities = entitiesByPackage.get(packageName); if (entities == null) { entities = new TreeSet<>(); entitiesByPackage.put(packageName, entities); } entities.add(entityName); } return entitiesByPackage; }
Please don't judge me - I did not write that code. A lot of different programmers probably did, over a long period of time. If you think you can do it better, I am sure the folks over at OfBiz would be grateful for your contributions :-)
The way I usually refactored this in my Refactoring to Java 8 Streams and Lambdas Self-Study Workshop is to do something like this (details of filters left out for now):
public Map<String, TreeSet<String>> getEntitiesByPackage( Set<String> packageFilterSet, Set<String> entityFilterSet) { return this.getEntityNames().stream() .map(this::getModelEntity) // Stream<ModelEntity> .filter(packageFilter) // Stream<ModelEntity> .filter(entityFilter) // Stream<ModelEntity> .collect(Collectors.groupingBy(ModelEntity::getPackageName, Collectors.mapping(ModelEntity::getEntityName, Collectors.toCollection(TreeSet::new)))); }
Some comments about the refactoring. We start with the names from
this.getEntityNames()
as Strings. We then find the relevant
ModelEntity
object. In our code, we can get back from the ModelEntity
to the name by calling getEntityName()
. But what if we
were not able to? How is it possible to keep both the original String and
the ModelEntity? This is what we will solve in this newsletter.
Let's refactor one step at a time. The first thing to improve is
to get rid of the continue
statements and to rewire the if
statements.
// does it match any of these? boolean foundMatch = false; for (String packageFilter : packageFilterSet) { if (packageName.contains(packageFilter)) { foundMatch = true; } }
could be rewritten as the comment implies:
boolean foundMatch = packageFilterSet.stream() .anyMatch(packageName::contains);
And then the if/continue logic could be inverted. Instead of this:
if (UtilValidate.isNotEmpty(packageFilterSet)) { boolean foundMatch = packageFilterSet.stream() .anyMatch(packageName::contains); if (!foundMatch) { continue; } } if (UtilValidate.isNotEmpty(entityFilterSet) && !entityFilterSet.contains(entityName)) { continue; } TreeSet<String> entities = entitiesByPackage.get(packageName); // ...
We should rather write it like this:
if ((UtilValidate.isEmpty(packageFilterSet) || packageFilterSet.stream().anyMatch(packageName::contains)) && (UtilValidate.isEmpty(entityFilterSet) || entityFilterSet.contains(entityName))) { TreeSet<String> entities = entitiesByPackage.get(packageName); // ...
Let's extract the filters into methods:
private boolean matchesPackageFilter( Set<String> packageFilterSet, String packageName) { return UtilValidate.isEmpty(packageFilterSet) || packageFilterSet.stream() .anyMatch(packageName::contains); } private boolean matchesEntityFilter( Set<String> entityFilterSet, String entityName) { return UtilValidate.isEmpty(entityFilterSet) || entityFilterSet.contains(entityName); }
Thus the getEntitiesByPackage() method would now look like so:
public Map<String, TreeSet<String>> getEntitiesByPackage( Set<String> packageFilterSet, Set<String> entityFilterSet) { Map<String, TreeSet<String>> entitiesByPackage = new HashMap<>(); //put the entityNames TreeSets in a HashMap by packageName Iterator<String> ecIter = this.getEntityNames().iterator(); while (ecIter.hasNext()) { String entityName = ecIter.next(); ModelEntity entity = this.getModelEntity(entityName); String packageName = entity.getPackageName(); if (matchesPackageFilter(packageFilterSet, packageName) && matchesEntityFilter(entityFilterSet, entityName)) { TreeSet<String> entities = entitiesByPackage.get(packageName); if (entities == null) { entities = new TreeSet<>(); entitiesByPackage.put(packageName, entities); } entities.add(entityName); } } return entitiesByPackage; }
We are now in a good position to refactor this code to use Java 8 streams, according to the original outline that we presented above. The only catch is that inside the filters and in the final grouping, we are using both entityName and packageName. In our case we can get this information from the EntityModel, but there may be cases where we cannot. We thus can employ Henri Tremplay's idea of using an anonymous inner class as a tuple:
public Map<String, TreeSet<String>> getEntitiesByPackage( Set<String> packageFilterSet, Set<String> entityFilterSet) { return this.getEntityNames().stream() .map(_entityName -> new Object() { // anonymous inner class String entityName = _entityName; // need to redeclare it ModelEntity entity = getModelEntity(_entityName); String packageName = entity.getPackageName(); }) .filter(tuple -> matchesPackageFilter( packageFilterSet, tuple.packageName)) .filter(tuple -> matchesEntityFilter( entityFilterSet, tuple.entityName)) .collect(Collectors.groupingBy(tuple -> tuple.packageName, Collectors.mapping(tuple -> tuple.entityName, Collectors.toCollection(TreeSet::new)))); }
ModelEntity has both getEntityName() and getPackageName() methods. We can thus map from ModelEntity and get the information we need from the entity. But that might not always be possible. Here is how the code would look without the tuple:
public Map<String, TreeSet<String>> getEntitiesByPackage( Set<String> packageFilterSet, Set<String> entityFilterSet) { return this.getEntityNames().stream() .map(this::getModelEntity) .filter(entity -> matchesPackageFilter( packageFilterSet, entity.getPackageName())) .filter(entity -> matchesEntityFilter( entityFilterSet, entity.getEntityName())) .collect(Collectors.groupingBy(ModelEntity::getPackageName, Collectors.mapping(ModelEntity::getEntityName, Collectors.toCollection(TreeSet::new)))); }
Getting back to the previous refactoring with the anonymous
inner class, prior to Java 10 we would not have been able to
declare a local variable of the tuple stream, but now we can
with var
:
public Map<String, TreeSet<String>> getEntitiesByPackage( Set<String> packageFilterSet, Set<String> entityFilterSet) { var tuples = this.getEntityNames().stream() .map(_entityName -> new Object() { String entityName = _entityName; ModelEntity entity = getModelEntity(_entityName); String packageName = entity.getPackageName(); }); var filteredTuples = tuples .filter(tuple -> matchesPackageFilter( packageFilterSet, tuple.packageName)) .filter(tuple -> matchesEntityFilter( entityFilterSet, tuple.entityName)); return filteredTuples .collect(Collectors.groupingBy(tuple -> tuple.packageName, Collectors.mapping(tuple -> tuple.entityName, Collectors.toCollection(TreeSet::new)))); }
Henri showed other tricks too and I recommend you watch his excellent presentation: Java 5, 6, 7, 8, 9, 10, 11: What Did You Miss? I did not stay for the entire talk. I'm used to listening to presentations at 2x speed, thanks to YouTube, so didn't have the patience to sit there for 2 hours :-)
In case you try to refactor the OfBiz method yourself, please be aware that there is an issue with checked exceptions from lambdas that I did not cover in this newsletter. Maybe another time. Or grab my Refactoring to Streams course as an early Christmas present :-)
Kind regards from Crete
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.