Graduating from Minimal to Rich Java APIs

Combining ease of learning with reduced code duplication for greater productivity.

Happy Birthday Java!

Happy Birthday Java

Java turned 24 years old on May 23rd, 2019. This is an impressive amount of time for a programming language to be successful and to continue growing its developer community. Java continues to evolve, and the Java Community continues to innovate and grow.

Up until Java 8 (released in 2014), Java has more or less adhered to a minimal API design philosophy. This has worked out extremely well for Java, as a minimal API design tends to lower the initial learning curve and increase adoption by only requiring developers to learn a few basic concepts.

Minimal API Design of Collections

The Collection interface has been available in Java since December 1998 when Java 2 was released. Collection has several mutating APIs including add, addAll, remove, removeAll, retainAll and clear. There are several testing methods including contains, containsAll, isEmpty and size. Every Collection can return its contents using the method toArray. The method iterator allows for any iteration pattern to be implemented using a for loop or while loop. In March 2014, Java 8 was released and the Stream interface was added with built-in iteration patterns (e.g. filter, map, reduce). The Collection interface was updated to include new default methods including the forEach, stream and parallelStream methods.

Collection API (1998), and Stream API (2014), with new default methods added on Collection

Graduating to higher-level APIs

As a programming language matures and enjoys mass adoption, it becomes important to address the needs of a more experienced developer population and to remain competitive with newer more nimble programming languages that may not be as concerned with preserving backwards compatibility between versions. The Java developer population (estimates around 12 million now) is already very familiar with the Java Collections APIs that have been available for over 20 years. Java is a great general purpose programming language, and gives developers the basic tools they need to create great applications. Since Java 8 was released, Java has started introducing higher-level APIs, especially in the Collections space. The Stream API which was added in Java 8 adds functional APIs to the existing Collection interfaces.

The List interface has also had a few new default methods added since Java 8. For example, we finally have a sort method available directly on List. Developers have been using the Collections class to sort instances of List since Java 2. Here is what the List interface looks like since Java 8.

List interface (December 1998) with default methods added (March 2014)

Most of the new functionality provided in Java 8 for Collection and List is available by calling the stream method, and then calling an appropriate method available on the Stream interface. The behaviors available on Stream interface are further extended by the collect method which can be passed a Collector. The method named collect on Stream is often described as a mutable reduction. There are a stock set of Collector implementations available on the Collectors class included since Java 8. Eclipse Collections adds its own set of Collector implementations in its Collectors2 class.

If we combine all of the functionality available in the List, Stream and Collectors class, you will see an evolution from the minimal API design of old, to a new richer API design.

Combining the List, Stream and Collectors APIs

Compare the combination API of List, Stream and Collectors with the existing rich API design in the Eclipse Collections MutableList class, and you will begin to see some common patterns between them (sometimes with different names). These patterns are not new. They have been known and available for a very long time in different programming languages, but are now being discovered and learned by the Java developer community.

MutableList methods in Eclipse Collections

Higher-level APIs help developers address a fundamental problem that minimal APIs can help create — Code Duplication. As you can see in the Eclipse Collections MutableList interface, there are a large number of iteration patterns that were not included in the original minimal API design of the Java List interface. Because these iteration patterns were not provided in higher-level Java APIs along with the language, developers were required to implement these patterns on their own using the basic building blocks of iterator and indexed list access.

See my previous blog on Code Duplication to learn more about iteration patterns.

Advanced Learning vs. Basic Duplication

Proponents of minimal API design will usually argue that a minimal API is easier to learn. This is true. If you believe that all you need to write code with Collections is an iterator and a for loop, you are correct. However, if all you have is an iterator, you will have to iterate the same patterns over and over again (Hammer, meet nail). More importantly, you will be leaving duplicate code for future readers to have to read.

The more code you have to write, the more code others have to read.

A Minimal API design results in more code having to be written, tested and maintained by application developers. A Rich API design results in more code having to be written, tested and maintained by library developers. If we all had shared code ownership over all the code written in the world, we would probably agree the least costly alternative would be to lean on library and language developers when appropriate to provide rich APIs for developers to use.

Once you know the basics, learning more advanced concepts is a good thing

Minimal API design has served its purpose well for the first 24 years of Java. It has led to mass adoption the Java programming language. Java developers have learned the basic patterns of using iterators, and are ready and willing to learn and graduate to using higher-level APIs.

Too Minimal == Anemic && Too Rich == Bloated

It is both possible to have too little or too much API. Somewhere in the middle, an API will feel “Just Right”. Java complicates this goal for API designers by having support of both Object and primitive types.

Eclipse Collections provides as much interoperability as possible between Object and primitive types in Java. This results sometimes in a multiplicative effect in the number of APIs required. For example, the collect and sort methods on MutableList all have primitive versions for each of the primitive types. If we reduce the total number methods down to the number of total core concepts, the number of new concepts a developer has to learn is more reasonable. For instance, if you learn the collect pattern in Eclipse Collections, which is a core concept, you will already know how all of the collect* (e.g. collectInt, collectShort, etc.) methods work.

In the following picture, I’ve included only the core conceptual API of MutableList. I have dropped all “With” methods and all of the primitive methods except for “Int”. This removed around 40 methods total from the output.

MutableList Core Concepts

It is enough to know how one primitive method works in order to understand how all of the other primitive methods work. If the developers of an API have Symmetric Sympathy, you will be able to easily understand and use each core API concept everywhere it is provided.

The evolution of Java Date and Time

Another example of an evolutionary change from a minimal API to a Rich API is in the Java Time library. Compare the methods on the Date class and LocalDate class available since Java 8.

Date compared to LocalDate

The Java time library has a much more humane API for dealing with dates and time. It does a great job balancing richness and minimalism.

The source of the source

The following source code was used to output all of the above APIs. You can use it output any methods of any classes you want. Try changing the Function for the grouping to get a different view into an API.

public void outputMethodsByFirstLetter(Class<?>... classes)
{
Function<Method, Character> firstLetter =
method -> Character.valueOf(method.getName().charAt(0));

String classNamesString =
ArrayIterate.collect(classes, Class::getSimpleName)
.makeString();

System.out.println(
classNamesString);
System.out.println(
StringIterate.repeat('-', classNamesString.length()));

MutableList<Method> methods =
ArrayIterate.flatCollect(
classes,
each -> ArrayAdapter.adapt(each.getMethods()));

String output = methods.groupBy(firstLetter)
.collectValues(
Method::getName,
TreeSortedSetMultimap.newMultimap())
.keyMultiValuePairsView()
.toSortedListBy(Pair::getOne)
.makeString("\n");

System.out.println(output);
System.out.println();
}

Update: June 3, 2019

The following source will show you the symmetric difference and intersection of two APIs. Just change the two classes you want to compare in the first method.

@Test
public void symmetricDifferenceAndIntersectionOfApis()
{
this.symmetricDiffAndIntersectionOfApis(Collectors.class, Collectors2.class);
}

public void symmetricDiffAndIntersectionOfApis(Class<?> classOne, Class<?> classTwo)
{
MutableSet<String> leftMethods =
Sets.mutable.with(classOne.getMethods())
.collect(this::methodNamePlusParms);
MutableSet<String> rightMethods =
Sets.mutable.with(classTwo.getMethods())
.collect(this::methodNamePlusParms);

String classNames = classOne.getSimpleName() +
", " +
classTwo.getSimpleName();
this.symmetricDifference(leftMethods, rightMethods, classNames);
this.intersection(leftMethods, rightMethods, classNames);
}

private String methodNamePlusParms(Method method)
{
return method.getName() + "(" +
ArrayIterate.collect(method.getParameters(), Parameter::getType)
.collect(Class::getSimpleName)
.makeString() + ")";
}

private void symmetricDifference(
MutableSet<String> leftMethods,
MutableSet<String> rightMethods,
String classNames)
{
System.out.println("Symmetric Difference (" + classNames + ")");
System.out.println(
StringIterate.repeat('-',
("Symmetric Difference (" + classNames + ")")
.length()));
this.outputGroupByToString(leftMethods.symmetricDifference(rightMethods));
}

private void intersection(
MutableSet<String> leftMethods,
MutableSet<String> rightMethods,
String classNames)
{
System.out.println("Intersection (" + classNames + ")");
System.out.println(
StringIterate.repeat('-',
("Intersection (" + classNames + ")")
.length()));
this.outputGroupByToString(leftMethods.intersect(rightMethods));
}

private void outputGroupByToString(RichIterable<String> methods)
{
Function<String, Character> firstLetter = string -> Character.valueOf(string.charAt(0));

String output = methods.groupBy(firstLetter)
.collectValues(
each -> each,
TreeSortedSetMultimap.newMultimap())
.keyMultiValuePairsView()
.toSortedListBy(Pair::getOne)
.makeString("\n");

System.out.println(output);
System.out.println();
}

Update: June 5, 2019

I refactored the code for calculating the symmetric difference and intersection to use MutableSortedSet instead of MutableSet.

@Test
public void symmetricDifferenceAndIntersectionOfApis()
{
this.symmetricDiffAndIntersectionOfApis(
Tuples.twin(StringBuffer.class,
StringBuilder.class));
}

private void symmetricDiffAndIntersectionOfApis(Twin<Class<?>> classPair)
{
MutableSortedSet<String> leftMethods =
this.getMethodNames(classPair.getOne());
MutableSortedSet<String> rightMethods =
this.getMethodNames(classPair.getTwo());

this.output(classPair, "Symmetric Difference",
leftMethods.symmetricDifference(rightMethods));
this.output(classPair, "Intersection",
leftMethods.intersect(rightMethods));
}

private void output(Twin<Class<?>> classPair,
String operation,
RichIterable<String> strings)
{
this.outputTitle(operation + " (" + this.classNames(classPair) + ")");
this.outputGroupByToString(strings);
}

private String classNames(Twin<Class<?>> classPair)
{
return classPair.getOne().getSimpleName() +
", " +
classPair.getTwo().getSimpleName();
}

private MutableSortedSet<String> getMethodNames(Class<?> classOne)
{
return ArrayIterate.collect(
classOne.getMethods(),
this::methodNamePlusParms,
SortedSets.mutable.empty());
}

private String methodNamePlusParms(Method method)
{
return method.getName() + "(" + this.parameterNames(method) + ")";
}

private String parameterNames(Method method)
{
return ArrayIterate.collect(
method.getParameters(),
parm -> parm.getType().getSimpleName())
.makeString();
}

private void outputTitle(String title)
{
System.out.println(title);
System.out.println(
StringIterate.repeat('-', (title).length()));
}

private void outputGroupByToString(RichIterable<String> methods)
{
String output = methods.groupBy(string -> string.charAt(0))
.keyMultiValuePairsView()
.toSortedListBy(Pair::getOne)
.makeString("\n");

System.out.println(output);
System.out.println();
}

Learn something new every day

Some developers may find a comprehensive API like Eclipse Collections intimidating. The good news is that with Eclipse Collections, you can learn and use the API at your own pace. For instance, MutableList extends java.util.List, so if you are familiar with the List API in Java, you are already familiar with the basics of the MutableList API in Eclipse Collections. If you have already been investing in learning Java Streams and Collectors, you can use these concepts with Eclipse Collections types as well. There are also more convenient eager methods directly on the collection types like MutableList themselves. If some of the different method names in Eclipse Collections seem strange, don’t worry there is a blog that can help you translate from Streams method names to Eclipse Collections method names.

Learning new things like iteration patterns will help you become a better developer in any language you program in. Over the years, I have learned and experimented with iteration pattern APIs in Smalltalk, Ruby, Scala, Groovy, Haskell, Clojure, Python and JavaScript. I’ve also written katas comparing five different Java Collections Frameworks. This makes it easier for me to travel between any of these languages and libraries when dealing with collections.

Eclipse Collections is open for contributions. If you like the library, you can let us know by starring it on GitHub.