Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Field-Access Interfaces for More Readable Java Stream Code

DZone's Guide to

Field-Access Interfaces for More Readable Java Stream Code

Streams are certainly useful, but they can make your code hard to understand and reason about. Here is an idea for a technique using field-access interfaces to help.

· Java Zone ·
Free Resource

Get the Edge with a Professional Java IDE. 30-day free trial.

Code using Java's Stream API is not very readable. To be fair, that is partially because the Stream API is based on the functional programming paradigm, which most Java programmers are not familiar with.

Another problem, arguably, is the rigidity of the API. Well actually, it is the rigidity of the language. By that, I mean that the strict syntax and the strong typing of Java force us to write code using terminologies that do not make much sense (names like map, flatMap), are verbose and unconcise for lambdas (lack of default variable name like 'it' in Groovy), as well as the generic typing of function types (Function).

In a previous post, I discuss Lombok's extension methods (here) and touch a bit on how they can be used to make streams more readable. This post explores another way to make code more readable. 

Disclaimer: Due to this exploratory nature, it might not necessarily result in useful techniques compared to existing methods. The aim here is to document the attempts just in case they are useful in some situation. Additionally, we will learn about classes the in java.util.function package along the way and might be able to use some of the techniques found here in our own code.

In this post, we will talk about field-access interfaces. A field-access interface is a representation of a field getter with added functionalities. These functionalities will allow us to write a more readable stream by allowing a more expressive use of the field.

Field Access

Accessing a field is normally done by an associated getter method. Java already has a way to easily access getter methods — by using method references.

@Data
public class Person {
  private String name;
}

val persons = Arrays.asList( ... some persons ...);
val personNames = persons.stream()
    .map(Person::getName)
    .collect(toList());
System.out.println(personNames);


Noticed that I make use of lombok.Data, lombok.val (type inference), and a static import of java.util.function.Collectors.toList() for the purpose of brevity. The term Person::getName in the code is the method reference to the getter getName of the field name. This reference is then cast to Function<Person, String>, meaning that, given a Person, a string can be returned (the name). This is a better and shorter form of the lambda person->person.getName(). So what's more to it? Let's say we want to get people whose names start with 'smi', a typical use case for autocompletion (like looking for 'smith'). We can write....

public List&let;Person> autoCompleteByName(String term, int limit) {
    val allPersons = getAllPersons();
    val foundPersons = persons.stream()
        .filter(person->person.getName().startsWith(term))
        .limit(limit)
        .collect(toList());
    return foundPersons;
}


Because we are not just using getName, but also perform some processing (a filter in this case), we can't just use the method reference. The code works, but the  person->person ...  part of the code might irritate some people (e.g., me — OCD of some kind). I wish Java had something like ' it ' in Groovy (it might be coming in the future in the form of a single underscore ' _ '). So let's see if we can do better. We can extract that portion of the code out as a method like this:

private <HOST, FIELD> Predicate <HOST> startsWith(Function <HOST, FIELD> access, FIELD value) {
    return host -> access.apply(host).startsWith(value);
}
public List <Person> autoCompleteByName(String term, int limit) {
    val allPersons = getAllPersons();
    val foundPersons = persons.stream()
        .filter(startsWith(Person::getName, term))
        .limit(limit)
        .collect(toList());
    return foundPersons;
}


The above code is reasonably readable. In most cases, we can be satisfied with just that. In fact, I would recommend using this type of refactoring for most practical situations. But something inside me is still itching for a more naturally readable result. Perhaps it is the fact that the code calls verb(object, subject). So let's go further with the following code.

@FunctionalInterface
public interface StringField <HOST> extends Function <HOST, String> {
    public
    default Predicate <HOST> startsWith(String term) {
        return host -> this.apply(host).startsWith(term);
    }
}

private static final StringField < Person > personName = Person::getName;

public List <Person> autoCompleteByName(String term, int limit) {
    val allPersons = getAllPersons();
    val foundPersons = persons.stream()
        .filter(personName.startsWith(term))
        .limit(limit)
        .collect(toList());
    return foundPersons;
}


Now the filter is done using personName.startsWith(term), which reads more naturally (object.verb(subject)). Now let me explain how all this works. Basically, we create a StringField interface that extends Function<HOST, String>. The extension inherits the method String apply(HOST host), which stays abstract. Then, we add a default method, Predicate<HOST> startsWith(String term), so that the field can check if the host's field starts with the term.

Since there is only one abstract method in the interface, we can annotate it with @FunctionalInterface. Then, we create a personName constant of the type StringField, given the value of the field getter reference Person::getName. When I use this, I normally put this constant in the host class, i.e., Person . This getter reference provides the implementation to the abstract method String apply(HOST host), where HOST is Person. With all that done, we can use personName.startsWith(term) to create a Predicate<Person> that the filter method wants.

Armed with this technique, we can write many similar interfaces and methods. For example:

@FunctionalInterface
public interface FieldAccess<HOST, TYPE> extends Function<HOST, TYPE> {
    public default Predicate<HOST> is(TYPE value) {
        return host -> Objects.equals(this.apply(host), value);
    }
    public default Predicate<HOST> isNot(TYPE value) {
        return host -> !Objects.equals(this.apply(host), value);
    }
    public default Predicate<HOST> isNull() {
        return host -> Objects.isNull(this.apply(host));
    }
    public default Predicate<HOST> isNotNull() {
        return host -> Objects.nonNull(this.apply(host));
    }
}

@FunctionalInterface
public interface StringField<HOST> extends FieldAccess {
  public default Predicate<HOST> startsWith(String term) {
      return host -> this.apply(host).startsWith(term);
  }
  public default Predicate<HOST> matches(String regex) {
      return host -> this.apply(host).matches(regex);
  }
  public default Predicate<HOST> isEmpty() {
      return host -> this.apply(host).isEmpty();
  }
  public default IntField<HOST> length() {
      return host -> this.apply(host).length();
  }
}
@FunctionalInterface
public interface CollectionField<HOST, TYPE, COLLECTION extends Collection<TYPE>>
                    extends FieldAccess<HOST, COLLECTION> {
    public default Predicate<HOST> contains(TYPE value) {
        return host -> this.apply(host).contains(value);
    }
    public default Predicate<HOST> contains(Predicate check) {
        return host -> this.apply(host).stream().anyMatch(check);
    }
    public default StreamField<HOST, TYPE> stream() {
        return host -> this.apply(host).stream();
    }
}
@FunctionalInterface
public interface StreamField<HOST, TYPE> 
                    extends FieldAccess<HOST, Stream<TYPE>> {

}
@FunctionalInterface
public interface IntField<HOST> extends ToIntFunction<HOST> {
    public default Predicate<HOST> equalsTo(int value) {
        return host->this.applyAsInt(host) == value;
    }
}


I hope you can easily guess what each of the examples in the code above is about. And with those field-access interfaces, you can write the following.

val total = orders.stream()
        .filter(orderStatus.isNot("CANCEL"))
        .filter(orderItems.contains(itemId.equalTo(1)))
        .flatMap(orderItems.stream())
        .mapToInt(itemPrice)
        .sum();


Except for the stream API method names, the code now reads mostly like English.

Discussion

So, how do you like it? To me, the last bit of code is much more pleasant to my eyes. It makes the code looks more like a DSL and friendlier to non-developers like domain experts — to the point they might be able to help validate the intention of the code.

This technique might be worthwhile if code deals a lot with domain-specific terminologies, or if it often involves more than a line of code to be put in a lambda while still readable. The technique might also make sense if there is a lot of code dealing with sub-objects while continuing to stream the parent objects. Operations such as:

  •  .filter(orderItems.contains(itemId.equalTo(1))) 
  •  .filter(orderItems.contains(itemPrice.lessThan(5))) 
  •  .filter(orderItems.contains(itemCategory.equalTo("PART"))) 

When doing a lot of these, it might be worth creating a field access for them.

Another situation is when working with something like BigDecimal (used a lot in finance or scientific-related applications). Operations done to BigDecimal are quite verbose and often involve hidden contexts (MathContext — precision and rounding mode in this case). Those context properties can be embedded into BigDecimalField, out of sight.

Finally, some people like to understand how the code does its job at a glance. They might not like it that some of the code is put somewhere else. To me, what more important is what the code does or attempts to do. It is better if it also shows how it does it. But every so often, the "how" parts obscure the overall code and make it harder to find things or understand the big picture.

I prefer to see what the code is trying to do, then zoom in to see how it does it. That way, I can scan and locate part of the code that I am looking for. Thus, I lean toward the code that has its implementation details extracted out. That said, it is up to you and your team to agree on what is readable.

Conclusion

This article explored field-access interfaces as a way to make stream code more readable. Field-access interfaces can access the field and also include additional operations that return appropriate function objects required by the Stream API. This makes code read more naturally.

So, how do you like it? Do you see yourself using this technique? What techniques do you use to make your streaming code more readable? Do you like code that explains what it does, even if it hides the implementation? Or you like code that shows you everything?

Happy coding!

Get the Java IDE that understands code & makes developing enjoyable. Level up your code with IntelliJ IDEA. Download the free trial.

Topics:
java ,stream api ,field-access interface ,code readability ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}