Java 8: No more loops

As I've written before, the new functional features in Java 8 is a game changer. It’s a new world for the Java developer and it's time to adjust to it.

In this post we’ll look at some alternative solutions to the traditional loop. The great thing about the new functional features in Java 8, is that it allows us to say what we want to be done instead of saying how to do it. This is where loops fall short. Sure loops are flexible, but this flexibility doesn’t come without a price. A return, break or continue dramatically changes how the loop will act, forcing us not only to understand what the code is trying to achieve, but also understand how the loop works.

By the introduction of streams in Java 8, we got some great functional operations to use on collections. We’ll now see how we can transform these loops to more concise and readable code.

Let the coding begin!

Ok, enough talk, time for some examples!

This time we’re going to work with articles. An article has a title, an author and several tags.

private class Article {

    private final String title;
    private final String author;
    private final List<String> tags;

    private Article(String title, String author, List<String> tags) {
        this.title = title;
        this.author = author;
        this.tags = tags;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }

    public List<String> getTags() {
        return tags;
    }
}

Each of the examples will contain a traditional loop solution, and a solution using the new features in Java 8.

In the first example we want to find the first article in the collection that has the tag “Java”.

Let’s look at a solution using a for-loop.

public Article getFirstJavaArticle() {

    for (Article article : articles) {
        if (article.getTags().contains("Java")) {
            return article;
        }
    }

    return null;
}

Now let’s solve the problem using operations from the stream API.

public Optional<Article> getFirstJavaArticle() {  
    return articles.stream()
        .filter(article -> article.getTags().contains("Java"))
        .findFirst();
    }

Pretty cool right? We first use the filter operation to find all articles that have the Java tag, then used the findFirst() operation to get the first occurrence. Since streams are lazy and filter returns a stream, this approach only processes elements until it finds the first match.

Now, lets get all the elements that match instead of just the first.

First, the solution using a for-loop.

public List<Article> getAllJavaArticles() {

    List<Article> result = new ArrayList<>();

    for (Article article : articles) {
        if (article.getTags().contains("Java")) {
            result.add(article);
        }
    }

    return result;
}

Solution using stream operations.

public List<Article> getAllJavaArticles() {  
    return articles.stream()
        .filter(article -> article.getTags().contains("Java"))
        .collect(Collectors.toList());
    }

In this example we used the collect operation to perform a reduction on the result stream instead of declaring a collection ourselves and explicitly add the articles if they match.

So far so good. Time to create a few examples that really make the stream API shine!

Let's group all the articles based on the author.

As usual, we start with the solution using a loop.

public Map<String, List<Article>> groupByAuthor() {

    Map<String, List<Article>> result = new HashMap<>();

    for (Article article : articles) {
        if (result.containsKey(article.getAuthor())) {
            result.get(article.getAuthor()).add(article);
        } else {
            ArrayList<Article> articles = new ArrayList<>();
            articles.add(article);
            result.put(article.getAuthor(), articles);
        }
    }

    return result;
}

Can we find a clean solution for this problem using the stream operation?

public Map<String, List<Article>> groupByAuthor() {  
    return articles.stream()
        .collect(Collectors.groupingBy(Article::getAuthor));
}    

Great! Using the groupingBy operation and a method reference to getAuthor we got some clean and readable code.

Now, let's find all the different tags used in the collection.

We start with the loop example.

public Set<String> getDistinctTags() {

    Set<String> result = new HashSet<>();

    for (Article article : articles) {
        result.addAll(article.getTags());
    }

    return result;
}

Ok, let's see how we can solve this with the stream operations.

public Set<String> getDistinctTags() {  
    return articles.stream()
        .flatMap(article -> article.getTags().stream())
        .collect(Collectors.toSet());
}

Nice! flatmap helpes us flatten the tag lists into one result stream, then we use collect to create a set to return.

Endless possibilities

That was 4 examples of how you can replace the loops with more readable code. Make sure to take a closer look at the stream API, because this post has barely scratched the surface.


Updates
  • Updated getDistinctTags() example to use Set as a result collection after receiving inputs from solarfuse and dhruvgairola in the comments.

Enjoyed the post?

If you don't want to miss future posts, make sure to subscribe