Hack Java Streams : Print Progress using Lombok

Yassin Hajaj
Jul 22 · 4 min read
Hacker

Introduction

Java streams have been introduced with Java 8 in 2014. These are no data structures and so, a few benefits of having static data structures disappear while using them.

Name it : treating tuples, sequential logic application, progress measurement, etc.

In this article we’ll see how we can hack the Java streams to allows for a print of the progress using extensions functions from Lombok.

Extension Functions

What are extension functions?

From Wikipedia

In object-oriented computer programming, an extension method is a method added to an object after the original object was compiled. The modified object is often a class, a prototype or a type. Extension methods are features of some object-oriented programming languages. There is no syntactic difference between calling an extension method and calling a method declared in the type definition.

They come natively in some languages : name C#, Kotlin, Swift. But they do not always have the same behaviour. Some extension functions modify effectively the target class while others just create new static methods where the first argument is generally an instance of the class.

Multiple libraries exist to write extension functions in Java : Manifold, Lombok.

We’ll use Lombok here, even though the feature is still experimental for the moment, it does what we expect it to do.

Implementation

We’ll begin by writing our extension function following Lombok’s requirements. For that we need to create a class and write those extension functions as static methods.

Remember, we need to write those functions for Streams and we’ll do it in a generic way

public class StreamExtensions {
public static <T, U> Stream<T> printLoadingEvolution(Stream<T> stream, Collection<U> collection) {
return printLoadingEvolution(stream, source, System.out::println);
}
public static <T, U> Stream<T> printLoadingEvolution(Stream<T> stream, Collection<U> collection, Consumer<String> printConsumer) {
AtomicInteger counter = new AtomicInteger();
int total = source.size();
int fivePercent = total / 20;
return stream.map(t -> {
if (counter.incrementAndGet() % fivePercent == 0) {
String message = counter.get() + " elements on " + total + " treated";
printConsumer.accept(message);
}
return t;
});
}
}

A lot to say about the implementation, let’s start by the first method.
It accepts a Stream and a Collection , this is for a simple reason : a stream has no way to understand in what state it is regarding the amount of elements it has processed or it will process, it is a stateless pipeline of intermediate operations. So we need to get the original collection as input, actually we could also accept the size of the collection, since it is the only data we’ll use from it.

Note that this implementation only works if the method is invoked before any filter , flatMap or other stream count altering operation.

We notice also that we give a default printConsumer as System.out::println , this is simply to not force a printing method in the invocation. We’ll see in the examples that another printing method is very easy to use.

The generic types of Stream & Collection are not the same, why? Because we could have some map invocations before the invocation of our method that could change the type of the Stream and make it different from the generic type of the Collection .

Now to the second method, the real implementation.

What we want to do with this one is simple

  • Get the count of the original elements
  • Calculate 5 percent of it, and print something each time we reach 5 more percent of processing

For this, as lambdas only accept and modify final or effectively final local variables, we do not have any choice but to use a mutable reference to an Integer : an AtomicInteger .

We added an if block and enter it only when we reach 5 percent of our total.

Testing

Time for testing

Before everything, we need to annotate the class where we’ll use the extension.

@ExtensionMethod({StreamExtensions.class})
public class MyClass {
...
}

Then create a method that uses the extensions

public <T> void printLoadingMethod(Collection<T> collection) {
collection.stream()
.printLoadingEvolution(collection)
.collect(toList());
}

And now a test method

@Test
public void printLoadingMethod() {
List<Integer> collect = IntStream.rangeClosed(1, 100).boxed().collect(toList());
new CustomerResource().printLoadingMethod(collect);
}

It correctly prints to the console as we can show here

5 elements on 100 treated
10 elements on 100 treated
15 elements on 100 treated
20 elements on 100 treated
25 elements on 100 treated
30 elements on 100 treated
35 elements on 100 treated
40 elements on 100 treated
45 elements on 100 treated
50 elements on 100 treated
55 elements on 100 treated
60 elements on 100 treated
65 elements on 100 treated
70 elements on 100 treated
75 elements on 100 treated
80 elements on 100 treated
85 elements on 100 treated
90 elements on 100 treated
95 elements on 100 treated
100 elements on 100 treated
Process finished with exit code 0

Success !

Use it with your Logger

That’s a success, but in your application, you probably do not use the console to print out logs.

Here is where the second implementation, accepting the Consumer<String> comes in play.

We basically only need to feed it with a logger and it’s done.

public <T> void printLoadingMethod(Collection<T> collection) {
collection.stream()
.printLoadingEvolution(collection, e -> logger.debug(e))
.collect(toList());
}

This works like a charm !

And what if we want more information printed, like the LocalDateTime.now() for example, we’ll just prepend it

public <T> void printLoadingMethod(Collection<T> collection) {
collection.stream()
.printLoadingEvolution(collection, e -> logger.debug(now() + " : " + e)
.collect(toList());
}

How it does it look like compiled ?

This is something also interesting to see, how does our method look like when compiler into bytecode?

As you can imagine, it’s just wrapped in the invocation of our StreamExtensions class.

public <T> void printLoadingMethod(Collection<T> collection) {
StreamExtensions.printLoadingEvolution(collection.stream(), collection).collect(Collectors.toList());
}

Conclusion

Hacking into Java streams is not as complicated as it seems to be, even more when you use libraries like Lombok which do a great work at getting rid of the boilerplate code and providing utilities like this one.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade