Best Practices When Coding Java
Best Practices When Coding Java
@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.
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.
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.
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.
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.
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:
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;
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (other == null) return false;
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.
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.