Java 8: Get A New Level of Abstraction

Original author: Marius Herring
  • Transfer
One of the many reasons why I like to work with functional programming is a high level of abstraction. This is due to the fact that in the end we are dealing with a more readable and concise code, which, undoubtedly, helps to bring closer to the logic of the subject area.

This article focuses more on four things introduced in Java 8 that will help you master a new level of abstraction.




1. No more cycles


I have said this before, and will say it again. Say goodbye to loops, and welcome the Stream API. The days of Java discussion about element loops are drawing to a close. With the Stream API in Java, we can say what we want to get, instead of talking about how this can be achieved.

Let's look at the following example.

We have a list of articles, each of which has its own list of tags. Now we want to get the first article containing the "Java" tag.

Take a look at the standard approach.
public Article getFirstJavaArticle() {
    for (Article article: articles) {
        if (article.getTags().contains("Java")) {
            return article;
        }
    }
    return null;
}


Let's solve the problem using the Stream API.
public Optional
getFirstJavaArticle() { return articles.stream() .filter(article -> article.getTags().contains("Java")) .findFirst(); }


Pretty cool, right?

First we use filter to find all the articles that contain the Java tag, then with findFirst we get the first inclusion.

The question arises: why should we filter the entire list if we need only the first inclusion? Since streams ... are lazy and filter returns a stream, the calculation takes place until the first inclusion is found.

I had previously written an article on replacing loops with a stream API . Read it if you need more examples.

2. Get rid of null checks


You may notice that in the previous example we can return Optional
.

Optional is an object container that may or may not contain a non-zero value.

This object has some higher-order functions, eliminating the need for adding repeated if null / notNull checks, which allows us to focus on what we want to do.

Now we ’ll improve the getFirstJavaArticle method . If there is no Java article, we will be happy to receive the latest article.

Let's look at what a typical solution looks like.
    Article article = getFirstJavaArticle();
    if (article == null) {
        article = fetchLatestArticle();
    }


And now the solution using Optional.
getFirstJavaArticle()  
    .orElseGet(this::fetchLatestArticle);


Looks great, doesn't it?

No extra variables, no if constructs, or any mention of null . We just use Optional.orElseGet to say what we want to get if no value is found.

Take a look at another example using Optional . Suppose we want to get the name of the first Java article if it is found.

Again, using a typical solution, we would have to add a null check, but ... you know what? Optional here to save this day.
playGround.getFirstJavaArticle()  
    .map(Article::getTitle);


As you can see, Optional implements a higher-order map function , helping us apply the function to the result, if any.

For more information on Optional, see the documentation .

3. Create your higher order functions


As you can see, Java 8 comes with a ready-made set of higher-order functions, and you can work wonders with them. But why stop there? And why not create your own higher-order functions?

The only thing needed to make a higher-order function is to take one of the Java functional interfaces, or a SAM-type interface, as an argument and / or return one of them.

To illustrate this, consider the following scenario.

We have a printer that can print various types of documents. Before printing, the printer should warm up, and after printing go into sleep mode.

Now we want to be able to send commands to the printer without worrying about its on and off procedures. This can be solved by creating a higher order function.
public void print(Consumer toPrint) {
    printer.prepare();
    toPrint.accept(printer);
    printer.sleep();
}


As you can see, we use Consumer, which is one of the functional interfaces, as an argument. Then we perform this function as a step between start-up and shutdown procedures.

Now we can easily use our printer without worrying about anything else except what we want to print.
// Распечатать одну статью
printHandler.print(p -> p.print(oneArticle));
// Распечатать несколько статей
printHandler.print(p -> allArticles.forEach(p::print));


For a more detailed example, read my article on how to create a TransactionHandler .

4. Beware of duplication. DRY principle


Writing functions is easy and quick. However, with easy code writing comes the desire for duplication.

Consider the following example.
public Optional
getFirstJavaArticle() { return articles.stream() .filter(article -> article.getTags().contains("Java")) .findFirst(); }


This method has served us well once, but it is not universal. We need a method that will be able to find articles based on other tags and requirements in general.

It is very tempting to create new threads. They are so small and so easy to make, how can it hurt? In contrast, small sections of code should motivate the DRY principle further.

Let's reorganize our code. To begin with, we will make our getFirstJavaArticle function more universal - it will take a predicate as an argument to filter articles according to what we need.
public Optional
getFirst(Predicate
predicate) { return articles.stream() .filter(predicate) .findFirst(); }


Now let's try to use this function to get a few different articles.
getFirst(article -> article.getTags().contains("Java"));
getFirst(article -> article.getTags().contains("Scala"));
getFirst(article -> article.getTitle().contains("Clojure"));


And yet, we are still dealing with duplicate code. You can see that the same predicate is used for different values. Let's try to remove these duplicates through the Function interface . Now our code is as follows.
Function> basedOnTag = tag -> article -> article.getTags().contains(tag);
getFirst(basedOnTag.apply("Java"));
getFirst(basedOnTag.apply("Scala"));


Excellent! I would say that this code complies with the DRY principle, right?

I hope this article has been useful to you and has provided some ideas on what you could do at a higher level of abstraction using Java 8 and functional features.

Also popular now: