Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 8

Let me share with you 10 Subtle Best Practices When Coding Java:

1. Remember C++ destructors


Remember C++ destructors? No? Then you might be lucky as you never had to debug through
any code leaving memory leaks due to allocated memory not having been freed after an object
was removed. Thanks Sun/Oracle for implementing garbage collection!
But nonetheless, destructors have an interesting trait to them. It often makes sense to free
memory in the inverse order of allocation. Keep this in mind in Java as well, when you’re
operating with destructor-like semantics:
 When using @Before and @After JUnit annotations
 When allocating, freeing JDBC resources
 When calling super methods
There are various other use cases. Here’s a concrete example showing how you might
implement some event listener SPI:

@Override
public void beforeEvent(EventContext e) {
super.beforeEvent(e);
// Super code before my code
}

@Override
public void afterEvent(EventContext e) {
// Super code after my code
super.afterEvent(e);
}8.
The Rule: Whenever you implement logic using before/after, allocate/free, take/return
semantics, think about whether the after/free/return operation should perform stuff in the
inverse order.

2. Don’t trust your early SPI evolution judgement


Providing an SPI to your consumers is an easy way to allow them to inject custom behaviour into
your library / code. Beware, though, that your SPI evolution judgement may trick you into
thinking that you’re (not) going to need that additional parameter. True, no functionality should
be added early. But once you’ve published your SPI and once you’ve decided followingsemantic
versioning, you’ll really regret having added a silly, one-argument method to your SPI when you
realise that you might need another argument in some cases:
interface EventListener {
// Bad
void message(String message);
}
What if you also need a message ID and a message source? API evolution will prevent you from
adding that parameter easily, to the above type. Granted, with Java 8, you could add a defender
method, to “defend” you bad early design decision:

interface EventListener {
// Bad
default void message(String message) {
message(message, null, null);
}
// Better?
void message(
String message,
Integer id,
MessageSource source
);
}
Note, unfortunately, the defender method cannot be made final.
But much better than polluting your SPI with dozens of methods, use a context object (or
argument object) just for this purpose.
interface MessageContext {
String message();
Integer id();
MessageSource source();
}

interface EventListener {
// Awesome!
void message(MessageContext context);
}
You can evolve the MessageContext API much more easily than the EventListener SPI as fewer
users will have implemented it.

The Rule: Whenever you specify an SPI, consider using context / parameter objects instead of
writing methods with a fixed amount of parameters.

Remark: It is often a good idea to also communicate results through a dedicated


MessageResult type, which can be constructed through a builder API. This will add even more
SPI evolution flexibility to your SPI.
3. Avoid returning anonymous, local, or inner classes
Swing programmers probably have a couple of keyboard shortcuts to generate the code for
their hundreds of anonymous classes. In many cases, creating them is nice as you can locally
adhere to an interface, without going through the “hassle” of thinking about a full SPI subtype
lifecycle.

But you should not use anonymous, local, or inner classes too often for a simple reason: They
keep a reference to the outer instance. And they will drag that outer instance to where ever they
go, e.g. to some scope outside of your local class if you’re not careful. This can be a major
source for memory leaks, as your whole object graph will suddenly entangle in subtle ways.

The Rule: Whenever you write an anonymous, local or inner class, check if you can make it
static or even a regular top-level class. Avoid returning anonymous, local or inner class
instances from methods to the outside scope.

Remark: There has been some clever practice around double-curly braces for simple object
instantiation:
new HashMap<String, String>() {{
put("1", "a");
put("2", "b");
}}
This leverages Java’s instance initializer as specified by the JLS §8.6. Looks nice (maybe a bit
weird), but is really a bad idea. What would otherwise be a completely independent HashMap
instance now keeps a reference to the outer instance, whatever that just happens to be.
Besides, you’ll create an additional class for the class loader to manage.

4. Start writing SAMs now!


Java 8 is knocking on the door. And with Java 8 come lambdas, whether you like them or not.
Your API consumers may like them, though, and you better make sure that they can make use
of them as often as possible. Hence, unless your API accepts simple “scalar” types such
as int, long, String,Date, let your API accept SAMs as often as possible.
What’s a SAM? A SAM is a Single Abstract Method [Type]. Also known as afunctional interface,
soon to be annotated with the @FunctionalInterface annotation. This goes well with rule number
2, where EventListener is in fact a SAM. The best SAMs are those with single arguments, as they
will further simplify writing of a lambda. Imagine writing
listeners.add(c -> System.out.println(c.message()));
Instead of

listeners.add(new EventListener() {
@Override
public void message(MessageContext c) {
System.out.println(c.message()));
}
});
Imagine XML processing through jOOX, which features a couple of SAMs:
$(document)
// Find elements with an ID
.find(c -> $(c).id() != null)
// Find their child elements
.children(c -> $(c).tag().equals("order"))
// Print all matches
.each(c -> System.out.println($(c)))
The Rule: Be nice with your API consumers and write SAMs / Functional interfaces
already now.

5. Avoid returning null from API methods


While NULLs and NullPointerExceptions will probably stay a major pain in Java for a while, you
can still design your API in a way that users will not run into any issues. Try to avoid returning
null from API methods whenever possible. Your API consumers should be able to chain methods
whenever applicable:

initialise(someArgument).calculate(data).dispatch();
In the above snippet, none of the methods should ever return null. In fact, using null’s
semantics (the absence of a value) should be rather exceptional in general. In libraries
like jQuery (or jOOX, a Java port thereof), nulls are completely avoided as you’re always
operating on iterable objects. Whether you match something or not is irrelevant to the next
method call.
Nulls often arise also because of lazy initialisation. In many cases, lazy initialisation can be
avoided too, without any significant performance impact. In fact, lazy initialisation should be
used carefully, only. If large data structures are involved.

The Rule: Avoid returning nulls from methods whenever possible. Use null only for the
“uninitialised” or “absent” semantics.

6. Never return null arrays or lists from API methods


While there are some cases when returning nulls from methods is OK, there is absolutely no use
case of returning null arrays or null collections! Let’s consider the
hideous java.io.File.list() method. It returns:
An array of strings naming the files and directories in the directory denoted by this abstract
pathname. The array will be empty if the directory is empty. Returns null if this abstract
pathname does not denote a directory, or if an I/O error occurs.
Hence, the correct way to deal with this method is:

File directory = // ...

if (directory.isDirectory()) {
String[] list = directory.list();

if (list != null) {
for (String file : list) {
// ...
}
}
}
Was that null check really necessary? Most I/O operations produce IOExceptions, but this one
returns null. Null cannot hold any error message indicating why the I/O error occurred. So this
is wrong in three ways:

 Null does not help in finding the error


 Null does not allow to distinguish I/O errors from the File instance not being a directory
 Everyone will keep forgetting about null, here
In collection contexts, the notion of “absence” is best implemented by empty arrays or
collections. Having an “absent” array or collection is hardly ever useful, except again, for lazy
initialisation.

The Rule: Arrays or Collections should never be null.

7. Avoid state, be functional


What’s nice about HTTP is the fact that it is stateless. All relevant state is transferred in each
request and in each response. This is essential to the naming of REST: Representational State
Transfer. This is awesome when done in Java as well. Think of it in terms of rule number 2 when
methods receive stateful parameter objects. Things can be so much simpler if state is
transferred in such objects, rather than manipulated from the outside. Take JDBC, for instance.
The following example fetches a cursor from a stored procedure:
CallableStatement s =
connection.prepareCall("{ ? = ... }");

// Verbose manipulation of statement state:


s.registerOutParameter(1, cursor);
s.setString(2, "abc");
s.execute();
ResultSet rs = s.getObject(1);
// Verbose manipulation of result set state:
rs.next();
rs.next();
These are the things that make JDBC such an awkward API to deal with. Each object is incredibly
stateful and hard to manipulate. Concretely, there are two major issues:

 It is very hard to correctly deal with stateful APIs in multi-threaded environments


 It is very hard to make stateful resources globally available,It is believed that the above
usage fulfils what is known as Fair Use
The Rule: Implement more of a functional style. Pass state through method arguments.
Manipulate less object state.

8. Short-circuit equals()
This is a low-hanging fruit. In large object graphs, you can gain significantly in terms of
performance, if all your objects’ equals() methods dirt-cheaply compare for identity first:
@Override
public boolean equals(Object other) {
if (this == other) return true;

// Rest of equality logic...


}
Note, other short-circuit checks may involve null checks, which should be there as well:

@Override
public boolean equals(Object other) {
if (this == other) return true;
if (other == null) return false;

// Rest of equality logic...


}
The Rule: Short-circuit all your equals() methods to gain performance.

9. Try to make methods final by default


Some will disagree on this, as making things final by default is quite the opposite of what Java
developers are used to. But if you’re in full control of all source code, there’s absolutely nothing
wrong with making methods final by default, because:

 If you do need to override a method (do you really?), you can still remove the final
keyword
 You will never accidentally override any method anymore
This specifically applies for static methods, where “overriding” (actually, shadowing) hardly ever
makes sense. I’ve come across a very bad example of shadowing static methods in Apache Tika,
recently. Consider:
 TaggedInputStream.get(InputStream)
 TikaInputStream.get(InputStream)
TikaInputStream extends TaggedInputStream and shadows its static get() method with quite a
different implementation.

Unlike regular methods, static methods don’t override each other, as the call-site binds a static
method invocation at compile-time. If you’re unlucky, you might just get the wrong method
accidentally.

The Rule: If you’re in full control of your API, try making as many methods as possible final by
default.

10. Avoid the method(T…) signature


There’s nothing wrong with the occasional “accept-all” varargs method that accepts
an Object... argument:
void acceptAll(Object... all);
Writing such a method brings a little JavaScript feeling to the Java ecosystem. Of course, you
probably want to restrict the actual type to something more confined in a real-world situation,
e.g. String.... And because you don’t want to confine too much, you might think it is a good
idea to replace Object by a generic T:
void acceptAll(T... all);
But it’s not. T can always be inferred to Object. In fact, you might as well just not use generics
with the above methods. More importantly, you may think that you can overload the above
method, but you cannot:

void acceptAll(T... all);


void acceptAll(String message, T... all);
This looks as though you could optionally pass a String message to the method. But what
happens to this call here?

acceptAll("Message", 123, "abc");


The compiler will infer <? extends Serializable & Comparable<?>> forT, which makes the call
ambiguous!
So, whenever you have an “accept-all” signature (even if it is generic), you will never again be
able to typesafely overload it. API consumers may just be lucky enough to “accidentally” have
the compiler chose the “right” most specific method. But they may as well be tricked into using
the “accept-all” method or they may not be able to call any method at all.
The Rule: Avoid “accept-all” signatures if you can. And if you cannot, never overload such a
method.

Conclusion
Java is a beast. Unlike other, fancier languages, it has evolved slowly to what it is today. And
that’s probably a good thing, because already at the speed of development of Java, there are
hundreds of caveats, which can only be mastered through years of experience.

You might also like