Useful stream utilities not found in Java

scottshipp

scottshipp

Posted on January 15, 2019

Useful stream utilities not found in Java

Photo by Karan Chawla on Unsplash

A bit of context

The standard Java library makes a reasonable attempt to provide useful utility methods for Java's functional Stream object. For example, if a Stream contains null objects a quick filter can remove them with a method reference to the handy library-provided Objects.nonNull method. Furthermore, perhaps you want to obtain a comma-delimited list of the objects' String representation. The standard library provides the Collectors.joining collector to facilitate that.

You can put these standard library methods together and you get some level of elegance in streams that you otherwise wouldn't. And they allow programmers to address common use cases such as the following example, which is to display the list of fruit available in the produce section of the local grocery store:

The old imperative way

StringBuilder produceDescImperative = new StringBuilder();
for(Fruit fruit : fruits) {
  if(fruit != null) {
    produceDescImperative.append(fruit).append(", ");
  }
}
produceDescImperative.deleteCharAt(produceDescImperative.lastIndexOf(", "));
return produceDescImperative.toString();
Enter fullscreen mode Exit fullscreen mode

The new functional way

return fruits.stream()
        .filter(Objects::nonNull)
        .map(Object::toString)
        .collect(Collectors.joining(", "));
Enter fullscreen mode Exit fullscreen mode

Methods such as Objects::nonNull and Collectors.joining() point the way toward a readable, maintainable functional style in Java, which allows the language to maintain its personality while expanding it to include functional idioms.

Some shortcomings

Unfortunately, the standard library often seems to stop partway to a developer-first orientation. For example, is there really a need to always map the Stream of objects to a String manually, as in line 3:

return fruits.stream()
        .filter(Objects::nonNull)
        .map(Object::toString) // why ?
        .collect(Collectors.joining(", "));
Enter fullscreen mode Exit fullscreen mode

Why can't the Collector returned by Collectors.joining(", ") figure out the toString call for itself? Almost anywhere else in the Java standard library, when an obvious String result is expected, the toString() call is made invisibly. (For example, System.out.println(someObject) is a valid call with the expected result, and saves the programmer from typing out "System.out.println(someObject.toString())."

It's likely that the library implementers didn't have the time or the context to use these utilities day-to-day and experience their shortcomings. Unfortunately, the rest of us have. That's not to say I blame them for not having the time or the context, just that there seems to be room for a better way.

Introducing Mill

I'd rather offer solutions than problems and I'd like to do so with Mill, an open-source library that, among other things, makes working with streams in Java a little more elegant. With a MoreCollectors.joining() call in Mill, the prior example doesn't need the extra mapping to the Object::toString:

import com.scottshipp.code.mill.stream.MoreCollectors;

// ...

return fruits.stream()
        .filter(Objects::nonNull)
        .collect(MoreCollectors.joining(", "));
Enter fullscreen mode Exit fullscreen mode

Mill also expands on the standard library offerings in various other ways. I'd like to run through some of them quickly.

Concatenate More Than Two Streams

Two streams can be concatenated with Stream.concat. I always wondered why they didn't make this a method with varargs, allowing me to concatenate any number of streams?

In Mill, such a method exists.

Stream<Fruit> fruitInStore = StreamOps.concat(fruitInDeli, fruitInProduceSection, fruitOnEndCaps);
Enter fullscreen mode Exit fullscreen mode

Non-empty Strings

A lot of existing methods in Java's standard library can be used as Predicates for filtering streams. For example, you can use String::isEmpty as a predicate in a filter. But why would you want to? The most common String::isEmpty usage in normal code is to make sure that a given string is not empty!

Which prompted this depressing Stack Overflow question showing that Java 8 doesn't even provide such a thing. Nor does Java 9 or 10.

In the Java 11 standard library, there's a clumsy Predicate.not utility to get the equivalent:

long nonEmptyStrings = s.filter(Predicate.not(String::isEmpty)).count();
Enter fullscreen mode Exit fullscreen mode

Mill, however, offers such a predicate:

s.filter(StringPredicates.nonEmpty());
Enter fullscreen mode Exit fullscreen mode

Filtering by comparison

Another common use case is to filter a list down to a given range of the objects. Continuing our fruit example, let's say we want to list only the fruits that start with A-H.

The easiest way I know with the standard library is something like this (assuming moreFruits is a Stream):

moreFruits.stream()
    .filter(s -> String.CASE_INSENSITIVE_ORDER.compare(s, "h") <= 0)
    .collect(Collectors.joining(", ")));
Enter fullscreen mode Exit fullscreen mode

It feels rather clumsy, and has some cognitive overhead where we remember that thing about how comparators return integers and negative numbers means less than.

How about the Mill way, instead?

moreFruits.stream()
    .filter(where(String.CASE_INSENSITIVE_ORDER).isLessThanOrEqualTo("h"))
    .collect(MoreCollectors.joining(", ")));
Enter fullscreen mode Exit fullscreen mode

As you can see, Mill offers a fluent interface for constructing such range-based filters.

But, wait, there's more

Rather than continue to belabor the point, I'll refer you to Mill's Javadoc where you can find out more about what the library has to offer.

If you think Mill can be useful to you, you will want to use Jitpack to set Mill as one of your application's dependencies, which is as easy as adding both of the following blocks in your pom:

pom.xml

<repositories>
    <repository>
      <id>jitpack.io</id>
      <url>https://jitpack.io</url>
    </repository>
  </repositories>

<dependencies>
    <dependency>
      <groupId>com.github.scottashipp</groupId>
      <artifactId>mill</artifactId>
      <version>v1.0</version>
    </dependency>

  </dependencies>
Enter fullscreen mode Exit fullscreen mode

See the Jitpack site for Gradle directions.

Contributors

Mill is also open to contributors. For example, it may make sense one day to get the library distributed via Maven Central. I plan on opening some Help Wanted issues soon for things I think could be useful. One of these is a String predicate for not null and not empty. I'm not sure why I haven't included that yet. Another one is, maybe it makes sense to make a MoreCollectors.joining() that takes no arguments and defaults to a comma-delimited list.

Final words

If you're interested, I also described how Mill offers something similar to the null-conditional operator in "Better null-checking in Java."

GitHub logo scottashipp / mill

Java library to make run-of-the-mill tasks more elegant. Compatible with Java 8+.

mill Build Status

logo

Java library to make run-of-the-mill tasks more elegant.

Note: This library is deprecated.

Update March, 2020. I will no longer be maintaining Mill. There are now other libraries with more widespread adoption and some actual funding and a set of active maintainers. (See StreamEx).

In addition, Mill is now the name of a build tool for Java and Scala.

Nevertheless, it is still useful at providing some examples of the things you can add to your own codebase to improve working with Java streams or lambdas.

Compatible with Java 8+.

In this README

How to add Mill to your application

If you are using Maven or another supported dependency management tool, you can use Jitpack to add Mill to your application.

Maven

First, add Jitpack (if you haven't already) to the repositories…

💖 💪 🙅 🚩
scottshipp
scottshipp

Posted on January 15, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related