Object Oriented Programming: Utilities - Next Generation

In the previous article we discussed how utility methods create reusable usage patterns. These patterns are accessible from everywhere as is required. The patterns are accessible through the use of static methods, which means that they can be used even without needing to instantiate an object of some type. In short, these methods are convenient ways of sharing common usage patterns between otherwise unrelated objects.

We also discussed how utilities are (generalized) usage patterns which means that they do not rely on the implementation details that are specific to each concrete type. The opposite is true. Usage patterns rely on the underlying (common) concept, meaning that they are applicable for multiple concrete types as long as they share the common concept, often represented by an interface definition.

“Advanced” utility methods in Java 8

The ease of access and the general applicability is what makes utility methods useful. However, you will still need to find these utility methods. As discussed in the previous article, there are number of naming schemes available through which we can reasonably predict where we will find such utilities if they exist. However, we still need to look for them.

In Java 8, with the introduction of so called default methods syntax to the language, it is now possible to write usage logic that is bound directly to an interface. This is very similar to the well-known static utility methods in several ways:

  1. We are restricted to usage logic for the default method, as is the case with utility methods. (As noted before, we rely on the concept/interface instead of the implementation details of a concrete type.)
  2. Methods are easily accessible: no need for any instantiation.
  3. Methods are widely accessible: no scoping issues/package constraints.

A short example demonstrates an interface Representable with a method representation(). The method representation() provides the caller with a string-representation of type. The default method reverse() is a utility that provides the caller with a reverse representation. The default method uses the public API as defined by the interface to perform a certain usage pattern.

As with utility methods, it is recommended but not enforced to have these characteristics:

  1. Pure: Execution leaves no side-effects.
  2. Context-free: There are no hidden assumptions that predisposition the usage pattern towards a certain type of use case. (Bias)

Default methods do have a number of advantages over utility methods.

  1. Default methods are easier to find, for the plain and simple reason that they are provided through the same interface that is implemented by the concrete type. This is of course a huge plus, since you will not have to guess whether or not utility methods exist and if so in what class they might be.

  2. There is no need for type checks in order to access optimized implementations. Where utility methods would need to check for certain (concrete) types in order to determine whether a more optimized usage pattern would be applicable, we can now simply override the default method on the specific concrete type for which there exists a “short-cut”.
    Previously, we accomplished this by accessing an existing method of the concrete type’s public API, once we confirmed that the provided instance is indeed of that particular concrete type. With default methods we let a concrete type override the default implementation with a better implementation.
    This has the added advantage that there is no dependency needed to the concrete type, as there would have been for “optimized” utility methods that check for a specific type.

An example of default methods, including an overriding implementation that leverages internal state, can be found in java.util. List.sort provides common logic for sorting. This logic will work for any concrete type implementing List. However, sometimes we can do better. ArrayList.sort provides an override-implementation that leverages internal state access to provide a more efficient implementation. In this case, ArrayList’s logic directly sorts on the internal state, instead of first constructing an array, sorting that and then traversing the original list and setting elements corresponding to the sorted array.

Java 8 Default Methods fully match to the characteristics we defined for utility methods. The restriction to usage logic only is likely the most telling commonality. This holds trivially, since interfaces cannot contain any implementation details (fields or logic). Furthermore, it is a simplification of a pattern, a trick, that we already described for common utility methods.

There is even a common disadvantage that default methods share with classes containing utility methods: we cannot add default methods to existing interfaces that are not provided by us.

Some would say that default methods open up the possibility for “multiple inheritance”. Given that default methods are constrained to usage logic only, i.e. they have no privileged access to internal state, I do not consider this multiple inheritance. There is, however, a chance that 2 interfaces define the exact same default method signature. If this is the case, the user is forced to override this method and define a concrete implementation that is expected to fit both definitions of the default method.

Kotlin’s extension functions

Kotlin is a very new language that aims to be an improvement to Java that is also fully backwards compatible. Kotlin aims to preserve the nature of the Java language and improve upon the developer experience by providing a new syntax that is more concise because it is constructed in such a way that recommended language constructs are assumed.

Kotlin provides a language feature called extensions that allows you to extend existing types. Kotlin provides this as a better and more convenient alternative to utility methods.

Kotlin’s extensions have the same characteristics as Java’s default methods except that it is possible to add (utility) methods to any type. Extensions are by default limited to the same package, though, and have to be imported in order to be accessible in other packages.

An unfortunate consequence

There is an unfortunate consequence to using Java 8’s default methods. Let’s look at an example where we have a separate API package and (possibly, if it exists) a separate implementation package. The API package typically has few or even no dependencies at all. This makes the API package very accessible for users.

Now, by adding default methods, we start to add logic to the API package. With this logic we may require including other dependencies. This makes the API package less independent than it used to be. Previously, we would have separated utility methods from the API and as such, all necessary requirements would be in the utilities module and the API module would be (reasonably) dependency-free.

This should be no more than a minor inconvenience, although it is a good trade-off to know about in advance.

This post is part of the Object Oriented Programming series.
Other posts in this series: