The Curse of Convenience Methods

In the old days, many Java APIs were fairly low level and pretty generic. You often had to explicitly select a concrete implementation, provide lots of parameters, and generally needed to know how things worked. This has changed in recent years - modern APIs provide lots of convenience functionality that raises the level of abstraction and increases productivity. I like this as much as anybody else, but unfortunately it can also lead to subtle bugs.

Suppose you want to convert a byte array to a String. Using your IDE's code completion feature, you find a constructor that looks useful:

public String(byte[] bytes)

In your unit test, it works fine and you're happy. But looking further, you see another constructor:

public String(byte[] bytes, Charset charset)

This is a situation where we have to be cautious. The first constructor will do the trick, but which charset will it use if you don't provide one? It's not necessarily UTF-8 which many people assume. It depends on your system's configuration so it may differ from machine to machine.

You get this particular problem in any situation where you need to turn bytes into Strings and vice versa, for example when you layer a Reader on top of an InputStream. There's always a charset/encoding required or you will end up with some default.

The next example is from Java's great java.util.concurrent framework. In Executors, it provides a set of convenience methods, i.e. the following one:

public static ExecutorService newFixedThreadPool(int nThreads)

Looks nice and safe, right? Unlike other methods on Executors, the number of threads is fixed and won't grow out of bounds when there's a lot of work coming your way. But there can still be trouble: Suppose there's a lot more work than your threads can handle. When thinking a bit how the ExecutorService API works, we quickly conclude that there must be some data structure that queues the work. A queue that we didn't specify.

Looking at the implementation of Executors in Java 8, we see that it uses a ThreadPoolExecutor behind the scenes and a LinkedBlockingQueue with its capacity set to Integer.MAX_VALUE. An OutOfMemoryException waiting to happen.

The number of parameters on ThreadPoolExecutor is surprising:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
     long keepAliveTime, TimeUnit unit,
     BlockingQueue<Runnable> workQueue,
     ThreadFactory threadFactory,
     RejectedExecutionHandler handler)

It can't be helped though, for building robust systems, you have to provide these parameters.

My last example is from cryptography. It's well established in the industry that you don't roll your own crypto algorithms. Where even world-class cryptographers struggle, it's safe to assume that the rest of us won't succeed. But even seemingly simple things like symmetrically encrypting a string can be quite error-prone. Let's have a look at the following method signature which probably exists in many in-house libraries (I've seen quite a few):

public String encrypt(String secret, String value)

From the Javadoc we learn that it encrypts the given value using AES and encodes the result in Base64. Very convenient, but potentially very dangerous. Java's JCA framework uses bytes internally, but let's assume that the method correctly handles charsets and also picks a secure mode of operation. People with a basic understanding of cryptography will notice that the method doesn't expect an Initialization Vector (IV). In many cases I've seen, people just set the IV to a static value, making the whole thing vulnerable to chosen plaintext attacks because the same value with the same secret will always yield the same cipher text. Another case where convenience can bite you in unexpected ways.

But not all is lost - some excellent libraries like Google's Guava protect us from ourselves. While their HashFunction interface provides a convenience method to hash a String, it requires a Charset, too, so you don't fall into the trap of platform dependent behavior. I think this is a very useful thing to adopt for our own APIs.

To sum things up: Not all abstractions are perfect. In many cases, you still need to know what's going on underneath the surface. Whenever you encounter overloaded methods where one requires fewer arguments than the others, it often uses some default value and delegates to another method. Always make sure that the defaults are appropriate for your particular use case.

social