If you are a good Java programmer but want to become a great Java programmer, here are 22 tips to help you write better, clearer, more-maintainable Java code.

These tips all derive from mistakes I see frequently in Java code in production. Some of these tips try to correct common misunderstandings of how Java works, such as how constructors instantiate objects and that enums are singletons. But several of the other tips just point out better ways to write Java so the code is easier to understand, easier to use, easier to maintain, and easier to debug when things go wrong.

Understanding these tips not only will make you a better Java programmer, they also will help you impress interviewers when you are looking for a job.


Here is a summary of the Java coding tips with links to where they are explained, below.

General tips

  1. Avoid needless and risky calls to toString()
  2. Don’t instantiate strings with “new”
  3. Favor StringBuilder
  4. Preallocate buffers
  5. Directly compare enums
  6. String literal concatenation is OK
  7. Don’t use the no-arg super()
  8. Design methods for unit-testability

Exception coding and handling tips

  1. Catch only application-specific exceptions
  2. Define methods to throw specific exceptions
  3. Favor runtime exceptions
  4. Chain original exception when translating and re-throwing an-exception
  5. Don’t catch an exception just to log and re-throw it

Javadoc tips

  1. Document preconditions
  2. Document returns
  3. Document side-effects
  4. Document expected exceptions

Logging with SLF4J tips

  1. Use SLF4J Logger API to avoid wasteful string concatenation
  2. Use the appropriate log level
  3. Include helpful information in logging statements
  4. Log to a logger
  5. Take advantage of Logger’s knowledge of null

General tips

Tips in this section focus on ways to use the features built into Java to code simply and effectively. Following these tips will result in code that generates more efficient bytecode and better runtime performance.

Tip 1: Avoid needless and risky calls to toString()

When concatenating strings and other objects, you can take advantage of a null check provided automatically – and for free – by the JVM. For example:

// Risky version: unneeded call to toString() and not null-safe
String msg = "Your user ID is '" + user.toString() + "'";

// Better, and null-safe:
String msg = "Your user ID is '" + user + "'";

If the user object reference is null in the above code, the first version throws a NullPointerException. The second version sets msg to the string “Your user ID is ‘null’”

When Java concatenates two objects with the + operator, it automatically checks whether either of the objects is null. If so, the JVM will replace the object with the string “null”. Otherwise, it will – again, automatically – invoke toString() on the object to get its string value. But if you short-circuit this useful JVM feature by explicitly invoking toString on the object, the JVM obliges and throws a NullPointerException if the object reference is null.

Tip 2: Don’t instantiate strings with “new”

Strings almost never need to be explicitly constructed. E.g. the following are needless, memory-consuming “initializations” of a string object:

String s1 = new String();                // Better: String s1 = "";
String s2 = new String("");              // Better: String s2 = "";
String s3 = new String("Initial value"); // Better: String s3 = "Initial value";

Strings are special objects in the JVM. You need to construct a new String instance using the new operator only if you absolutely, positively cannot tolerate the situation where two strings containing the same sequence of characters are equal using the == operator. In all other cases, use string literals (a string between quotes) like “Hello, World.”

Using string literals helps the JVM optimize strings by reusing the same (immutable) string object. Since instances of the String class are immutable once created, strings containing a particular character sequence can be reused without harming the logic of the program.

To test your understanding, this JUnit test passes:

public void testStringEquality() {
    String helloLiteral = "hello";
    String helloNew = new String(helloLiteral); // Copies "hello" to a new string object

    assertEquals("hello", helloLiteral); // Confirm these contain the same string sequence
    assertEquals("hello", helloNew); // Confirm two difference instances contain same sequence
    assertTrue("hello" == helloLiteral); // Confirm these strings refer to the same *instance*
    assertFalse(helloLiteral == helloNew); // Confirm new operator created a separate instance

Tip 3: Favor StringBuilder

You undoubtedly already have learned that if your code needs to build a string at runtime from multiple, smaller strings, the + string concatenation operator performs poorly because the JVM internally builds and throws away several objects while creating the final string object.

In the pre-Java 1.5 days, those objects were instances of the synchronized StringBuffer class. Since version 1.5, Java uses StringBuilder, which is an unsynchronized version of StringBuffer that is more efficient. If your code is building a string using method-local variables (inherently thread-safe), there is no reason to use StringBuffer.

Tip 4: Preallocate buffers

Java’s collections framework contains useful classes to manage data structures, such as ArrayList, HashMap, and StringBuilder. These collections store their data internally as regular Java arrays, which as you know are fixed size. Each of these classes thus needs to allocate an initial array to hold the values, and then allocate a larger array if the collection outgrows the size of its current array.

Growing the array requires a runtime construction of a new (larger) array object, copying of the objects from the old array to the new array, and, in the case of hash data structures, re-hashing the keys into the larger array. This extra work happens behind the scenes and is invisible to the developer, but can needlessly waste resources at runtime if you can reduce the number of resize cycles.

The default initial array size for an ArrayList is 10 and for HashMap and StringBuilder it is 16. Each of these classes uses an algorithm to determine how large to make the size of the new array when it is no longer big enough to hold the data being stored. For an ArrayList, the new size is calculated as currentCapacity * 1.5. For StringBuilder, it is currentCapacity * 2 + 2. What this growth strategy means is code like the following will instantiate three char arrays, throwing away the first two almost immediately after creating them.

StringBuilder getty = new StringBuilder(); // Allocates a char array with size 16.
getty.append("Four score and seven"); // won't fit in 16 bytes. Grows array to 16*2+2 = 34
getty.append(" years ago, our fathers"); // Larger than 34 chars, so grows array to 70.

Since this approach is wasteful when you know you will be creating data structures with more than a dozen or so items, the collection classes have constructors (and sometimes methods like ensureCapacity) to inform the collection object how much data you might be adding to these structures.

Here is a more efficient version of the above code:

StringBuilder getty = new StringBuilder(128); // Allocates char array with size 128.
getty.append("Four score and seven"); // Adds to existing array
getty.append(" years ago, our fathers"); // Adds to existing array

This new version, using the StringBuilder(int capacity) constructor, instantiates one and only one array. No char arrays were needlessly created and destroyed during the process.

Tip 5: Directly compare enums

If two enum objects of the same type represent the same enum value, they will be the same object. It therefore always valid and correct to compare enums of the same type using == and !=. Using the equals method is unnecessary.

You do not need to define or use an internal “getValue()” method in the enum class and then use equals to compare those values for equality. I have seen many developers do this because they don’t realize the JVM guarantees that enums representing the same value will be the same object. Each enum value is a singleton within the JVM. You can safely and efficiently use == to compare for equality.

Tip 6: String literal concatenation is OK

If you need to build a long string literal in your source code composed only of other string literals, there is no performance hit by splitting the string across lines and concatenating them using the + operator. There is no performance gain by using StringBuilder. The compiler will see that the strings are all literals and, at compile time, will splice the strings together into one string object.

Knowledge of this fact comes in handy when creating a long string in your code by wrapping a string across several lines but you don’t want that string to violate line-length coding restrictions you might need to (and should) follow.

I have seen many developers solve this super-long string problem by building up the string using StringBuilder because they think the resulting code will be more efficient. For example, this code is unneeded:

// Bad code. I should have just used + to build bigString.
StringBuilder sb = new StringBuilder(200); // See Tip 4!
sb.append("This is a long literal string I am splitting across");
sb.append(" many lines because I don't want to create a string");
sb.append(" that is so long it wraps in my IDE and makes my code");
sb.append(" really hard to read");
String bigString = sb.toString();

The bigString string could have been built more effectively using the + operator, like so:

// Much better way.
String bigString = "This is a long literal string I am splitting across" +
    " many lines because I don't want to create a string" +
    " that is so long it wraps in my IDE and makes my code" +
    " really hard to read";

To prove that the JVM really does create only one string object when you use literals, the following JUnit test passes:

public void testStringLiteralConcatenation() {
    String s1 = "Four score and seven years ago," +
      " our fathers brought forth on this continent a new nation";
    assertTrue(s1 == "Four score and seven years ago, our fathers brought forth on this continent a new nation");

The above test passes because the string s1 is concatenated at compile time and is the same string instance (object in memory) as the full string in the assertTrue method. (See Tip 2 for why the strings are equal.)

If your code needs to build a large string composed of non-literal strings, such as those involving variable values or runtime calculations, you’ll still want to resort to building up the string with StringBuilder. That’s what it’s for. But for string literals, the above approach is easier to code, maintain, and understand.

Tip 7: Don’t use the no-arg super()

I wrote an entire blog post on this topic, but here’s the gist.

If a constructor does not invoke super(<some param list>) as its first statement, the JVM implicitly invokes a call to the no-argument super() to ensure the super class constructor initialize its instance. Thus, constructors you write never need to explicitly invoke the no-argument super().

You need to invoke super only if you want to use a constructor in the superclass other than the no-argument constructor. As a corollary, you cannot avoid having at least one of the superclass’s constructors called when instantiating a class.

The example below shows a developer’s naive belief that super() is required if you want to properly construct the parent object. The developer wrote the Subclass2 constructor believing that by not invoking super(), the superclass’s constructor would not be called.

class ParentClass {
    ParentClass() {
        System.out.println("ParentClass no-argument constructor here");

class Subclass1 extends ParentClass {
    Subclass1() {
        super(); // Needless call to super(), but the developer didn't know this
        System.out.println("Subclass1: Successfully called super()");
class Subclass2 extends ParentClass {
    Subclass2() {
        System.out.println("Subclass2: Nya, nya, I'm not going to call super() here so it won't(?) be called!");

Here’s a unit test to prove the point.

public void testConstructorCalls() {
    new Subclass1();
    new Subclass2();

When the above unit test method is run, the following is printed to the console.

ParentClass no-argument constructor here
Subclass1: Successfully called super()
ParentClass no-argument constructor here
Subclass2: Nya, nya, I'm not going to call super() here so it won't(?) be called!

As you can see from line 3, The ParentClass constructor got called when Subclass2 was instantiated even though the Subclass2 constructor did not call super(). The programmer could not stop the call to ParentClass’s constructor – which is a good thing because Java wouldn’t work if parent classes weren’t properly constructed by either the no-arg constructor always being run or an explicitly invoked constructor that takes arguments.

Tip 8: Design methods for unit-testability

To help ensure your code is doing what it is supposed to be doing, you will want to write unit tests. How you design your code will have a large impact on how testable it is.

A good design practice is to write small methods that perform a unit of useful work that return a testable value. That way, each method can be separately tested to ensure its business logic functions as expected.

For example, consider the method below. The developer was told to write a method that accepts a parameter, use the parameter to invoke a web service that returns a value, use both pieces of data to perform (what we will pretend to be) complex business logic to calculate an answer, then write that answer to the database.

Here is the developer’s first (bad) version of that method.

 * Processes the parameter by calling a web service to get the data, manipulate the data
 * according to business rules, then write the calculated value to a database table.
public void processDataTestUnfriendly(HelperObject info) {
    // Get answer from web service based on the info passed in
    int webServiceAnswer = invokeWebService(info);

    // Perform business rules based on the info and the answer to find our solution
    String businessSolution;
    if (info.someValue() == 86 && webServiceAnswer < 99) {
        businessSolution = "Maxwell";
    } else if (info.someValue() == 99 && webServiceAnswer > 0) {
        businessSolution = "Ninetynine";
    } else if (info.someValue() == 13 && webServiceAnswer == 9) {
        businessSolution = "Fang";
    } else {
        businessSolution = "theChief";

    // Write the solution to the database

The processDataTestUnfriendly method performs all of the required work in a single method that returns void. The only way to test the business logic of this method would be to mock out the database, which requires a lot more testing setup code, or by querying the database using an integration test to verify whether the method calculated the correct result.

A simpler testing approach would be to refactor the method to pull the business logic into a separate method. That new method contains just the business logic and no code that invokes external web services or databases. The refactored method now just calculates and returns the business solution and can be tested with a unit test to validate its correctness.

Here is a refactored version of the above code:

 * Processes the parameter by calling a web service to get the data, manipulate the data
 * according to business rules, then write the calculated value to a database table.
public void processData(HelperObject info) {
    // Get answer from web service based on the info passed in
    int webServiceAnswer = invokeWebService(info);

    // Perform business rules based on the info and the answer to find our solution
    String businessSolution = calculateBusinessSolution(info.someValue(), webServiceAnswer);

    // Write the solution to the database

 * Calculates answer using complex business logic defined in User Story 1010.
 * @param infoValue the info value derived from the HelperObject blah blah blah
 * @param webServiceAnswer the calculated answer from the external web service
 * @return the solution string as calculated using the complex business rules defined in the
 *         user story.
String calculateBusinessSolution(int infoValue, int webServiceAnswer) {
    if (infoValue == 86 && webServiceAnswer < 99) {
        return "Maxwell";
    } else if (infoValue == 99 && webServiceAnswer > 0) {
        return "Ninetynine";
    } else if (infoValue == 13 && webServiceAnswer == 9) {
        return "Fang";
    } else {
        return "theChief";

The new code still calculates and writes the required value to the database, but the business logic has been pulled into a separate, testable method called calculateBusinessSolution. Note that the visibility of the new method has been declared as package-private to allow unit tests in the same package to invoke it. Unit tests can now be written to test the business logic with no need for a database-dependent integration test.

Another bonus to structuring your code for unit-testability is your code will become more modular and less dependent on other parts of your application. Your methods will become smaller and easier to understand because they focus on one small piece of work that you want to test.

Exceptions coding and handling tips

Tips on throwing and catching exceptions.

Tip 9: Catch only application-specific exceptions

Catching generic exceptions, like Exception, RuntimeException, or Throwable, risks obscuring and ignoring serious problems in the application. If your code catches exceptions too high in the inheritance hierarchy, your catch block might be invoked at runtime to handle problems it wasn’t designed to handle. Worse, your code likely will make false assumptions about what went wrong because it was written to deal with a more-expected exception.

For example, the following code assumes the called method will throw an exception only if the JSON data is bad:

 * Converts the JSON object to XML. If the JSON string is illegally formatted, the method
 * returns an XML error document.
 * @param data a JSON document containing important data that needs to be converted to XML
 * @return an XML version of the important JSON data, or an XML error document if the JSON is
 *         badly formatted
public String convertJsonToXml(String data) {
    try {
        // This library throws 3 different checked parsing exceptions and an
        // IllegalArgumentException if you pass it an empty string, so I'm going
        // to catch them all here with Exception. [BAD IDEA!]
        ThirdPartyJsonLibrary thirdPartyJsonLibrary = ThirdPartyJsonLibrary.newInstance();
        return thirdPartyJsonLibrary.convertToXml(data);
    } catch (Exception e) {
        return "<MyJsonObject><error>Invalid JSON document</error></MyJsonObject>";

Why is trying to catch all parsing problems using Exception a bad idea? Consider what happens if the call to thirdPartyJsonLibrary.convertToXml throws a NullPointerException if the parameter is null? Or maybe there is a subtle bug in the library where it throws an IndexOutOfBoundsException if the string contains a JSON array with a tab character before the closing bracket, like ["a", "b", "c" \t ]? (Assume there is a hidden tab character at the end there.)

Both of those problems will be caught in that catch block by catching Exception. The code in the catch block assumes only that the JSON string was bad and obscures other errors that might have occurred.

The developer who wrote this code might argue that passing in a null does mean the JSON is bad, and so the code works remarkably well! But a developer calling the code with a parameter he or she believes contains a valid JSON string could spend hours trying to determine how their JSON document being passed in is badly formatted until realizing their code actually passed in the wrong variable (which was null at the time). A bug that would have been immediately obvious had the code not hidden the NullPointerException now took hours to track down.

Typically, well-designed applications will catch high-level exceptions, like Exception, only at the topmost level of the call stack where the application is bootstrapped. Your application likely will want to catch Exception at some level because, if not caught, the exception would cause an untimely death of the JVM and an unpleasant user experience.

Catching exceptions like Exception or Throwable at the top level of the application usually results in an “unexpected problem occurred in the application” message to the user (or service caller) and an urgent alert to system administrators to diagnose this major, unexpected application error. You probably thus want to deal with these major problems only at one place in the code, not in your JSON-to-XML parsing method.

If your code truly needs to catch a high-level exception lke Exception, the reason should be clearly documented in comments and/or Javadoc. In general, catch only specific exceptions your code can handle.

Tip 10: Define methods to throw specific exceptions

Methods should be defined to throw application-specific exceptions appropriate to that layer of the application. This tip goes hand-in-hand with Tip 9.

If you define a method to throw a generic, high-level exception like Exception, RuntimeException, or – even worse – Throwable, you force all users of that method to catch that high level exception. When you force the callers of your code to catch high-level exceptions, they must violate the good practice in Tip 9, creating all the resulting risks in the application discussed in that tip.

Methods you write should throw an appropriate application exception or a specific predefined Java exception that makes sense to correctly describe the problem, such as IllegalArgumentException.

Tip 11: Favor runtime exceptions

Define methods to throw checked exceptions (non runtime exceptions that must be caught by the caller) only if the caller likely can fix and recover from the problem.

When choosing what exceptions a method will throw, consider how you and other developers will invoke the method. If the exception is something developers will want to catch because they can write the appropriate code to correct the problem (such as retrying a call to a remote service that might temporarily be busy), use a checked exception (an application-appropriate subclass of Exception).

If the exception is for a condition the developer likely cannot recover from, such as a database or other critical resource being inaccessible, use a runtime exception (an application-appropriate subclass of RuntimeException).

That way, callers of your method will not need to write redundant catch blocks for problems they can’t do anything about. The only thing the caller of your method can do for these fatal problems is log the exception and re-throw it. But fatal problems like these should be caught only once, high in the application hierarchy, and not dealt with at every place in the code where your method is invoked.

See Tip 9 for related information.

Tip 12: Chain original exception when translating and re-throwing an exception

When throwing a new exception type as a result of another exception, chain the original exception object to avoid losing the context of the original problem. Chaining the exception means passing the original exception to the new exception’s constructor so the entire sequence of exceptions can be logged for diagnosis.

Ignore this rule only when the original exception is irrelevant to systems administrators who will be diagnosing the problem.

Here is an example of a rare case when chaining the exception is NOT needed:

 * Returns the port number as defined in the given configuration file.
 * @param configFile a configuration file containing a line that says "port = <port number>"
 * @return the port number found in the configuration file
 * @throws MySystemException if the port number is not a valid integer
public int returnPortNumber(File configFile) throws MySystemException {
    String portNumberString = getConfigValue("portNumber", configFile);
    // Convert string to an integer.
    try {
        return Integer.parseInt(portNumberString);
    } catch (NumberFormatException nfe) {
        throw new MySystemException(
            "portNumber in configuration file '" + configFile.getAbsolutePath()
            + "' is not a valid integer: '" + portNumberString + "'");

The above code catches the NumberFormatException when Integer.parseInt discovers that someone accidentally set the port number to a non-integer value and throws an application-specific MySystemException to report this fatal configuration error. (Let’s assume there is no fall-back default value for the port number.)

The exception message includes the file name and the offending string that should have been an integer so system administrators can diagnose the problem and make the needed corrections. System administrators or maintainers diagnosing this exception unlikely will benefit from the knowledge that our code caught this problem when the NumberFormatException occurred 2 lines previously in the code, so adding the nfe as a parameter to the MySystemException constructor will not aid diagnoses or correction.

Still, unless you are pretty certain that the base cause of the exception will not aid in diagnosing the problem, go ahead and include the exception in the new exception’s constructor, like:

    throw new MySystemException(
        "Port number number found in configuration file '" + configFile.getAbsolutePath()
        + "' is not a valid integer: '" + portNumberString + "'", nfe);

Tip 13: Don’t catch an exception just to log and re-throw it

I’m sure you have seen exception stack traces in your logs and on your console that duplicate the stack trace two, three, or more times. The repetitive clutter makes diagnosing the problem more difficult.

You can help solve this log-clutter problem by never catching an exception merely to log and re-throw it because your code can’t resolve the problem.

Exceptions that the calling code can’t handle, such as required resources being unavailable or coding problems that caused a NullPointerException, are best caught and logged at the top level of the calling stack where the problem can be logged once and an appropriate error given to the user or external system calling the application.

See Tip 9 for related exception-handling information.

If you are writing code at a lower level of the stack, don’t catch exceptions that your code can’t resolve. Instead, let the exception bubble up the call stack and be caught by a higher level. Your API might need to translate (and chain) an exception into a different exception type appropriate to your layer’s abstraction (see Tip 12), but there should be no need to log that exception before re-throwing it. Let the original caller catch the fatal exceptions and log them. Once.

Effective Javadoc

You can write the best code in the world, but if developers don’t understand how to use it correctly, its usefulness will be limited and your fellow developers will not extol your skill when they find themselves using your API. Writing Javadoc that clearly defines how to use your API will not only help other developers, it will help you when you come back to your code months later to make modifications.

Tip 14: Document preconditions

If a method accepts parameters, define in the Javadoc what preconditions your code assumes about parameter values. Clearly define what the method will do when those expectations about the parameters are unmet.

For example, what happens if the caller passes in a null? Will the method throw a NullPointerException? An IllegalArgumentException?

There is no single “correct” place in the Javadoc block to define a method’s preconditions. Use the method’s description, @param, @return, @throws, or a combination of them all to tell developers the parameter values your code expects.

As an example, the method below uses the Javadoc description and @throws tag to alert developers of the expected constraint on the parameter and what happens if the constraint is not met.

 * Returns the meaning of life based on the supplied universal truth parameter. The universal
 * truth must be in a <em>final</em> state.
 * @param truth the base assumptions about universal truths to help compute what life is about.
 * @return the meaning of life, as an integer, or 42 if the {@code truth} param is null
 * @throws IllegalArgumentException if {@code truth} is not null but {@code truth.isFinal()} is false
public int getMeaningOfLife(UniversalTruth truth) {
    if (truth == null) {
        return 42;
    if (truth.isFinal()) {
        return internalAlgorithmToDivineLifeMeaning(truth);
    } else {
        throw new IllegalArgumentException(
            "The truth parameter must be in a final state to determine meaning of life."

Tip 15: Document what your method returns

Describe in your Javadoc comments under what circumstances a method returns different values. Always clearly describe under what conditions the method returns null. If the method returns an object that will never be null, clearly document this fact so callers don’t wastefully write a null check on the returned value.

As an example, the Javadoc description in the following method says it returns a User object or throws an exception. The @return tag also explicitly points out that the method never returns null.

 * Returns the currently logged in user. This method assumes a user session is active via an
 * earlier call to {@code createSession()}. If a valid User object cannot be returned, throws an
 * appropriate exception.
 * @return the user retrieved from the data layer, never null
 * @throws IllegalStateException if this method was called before a call to
 *         {@code createSession}
 * @throws InvalidLoginStateException if the current user can't be found in the data layer,
 *         which should only happen if the user was deleted after the user logged into the
 *         application.
public User getCurrentlyLoggedInUser() {
    if (sessionId == null) {
        throw new IllegalStateException(
            "Programming error: No active user session. Call createSession() first.");
    User user = userDao.findUserFromSessionId(sessionId);
    if (user != null) {
        return user;

    // User is null, which generally shouldn't happen. Throw an exception.
    log.warn("Current user from session ID '{}' not found by UserDao."
            + " This should be rare error. Was user just deleted?", sessionId);
    throw new InvalidLoginStateException(
        "Current user disappeared from data layer. Was user just deleted?");

Tip 16: Document side-effects

Developers should be able to discern what a method does by its name and return type. Sometimes, however, a method might also produce hidden side-effects. Whenever a method makes changes to the instance’s state, to a parameter, to another object, to an external system, etc., that are not obvious from the method’s signature, be sure to describe those changes in the method’s Javadoc.

For example, the following getCurrentlyLoggedInUser() method not only returns a User object, it also caches the looked-up object in the instance to avoid an expensive database lookup on subsequent calls. That caching is not obvious from the method’s signature, which says only that the User value will be returned. The code’s Javadoc informs callers of this caching side-effect.

 * Returns the currently logged in user. This method assumes a user session is active via an
 * earlier call to {@code createSession()}. Caches the current user for subsequent calls. If a
 * valid User object cannot be returned, throws an appropriate exception. Thread safe.
 * @return the cached logged-in user or the user retrieved from the data layer, never null
 * @throws IllegalStateException if this method was called before a call to
 *         {@code createSession}
 * @throws InvalidLoginStateException if the current user can't be found in the data layer,
 *         which should only happen if the user was deleted after the user logged into the
 *         application.
public synchronized User getCurrentlyLoggedInUser() {
    if (currentUser == null) {
        if (sessionId == null) {
            throw new IllegalStateException(
                    "Programming error: No active user session. Call createSession() first.");
        User user = userDao.findUserFromSessionId(sessionId);
        if (user == null) {
            // Would happen only if user not found, which means user must have disappeared since
            // application started.
            log.warn("Current user from session ID '{}' not found by UserDao."
                    + " This should be rare error. Was user just deleted?", sessionId);
            throw new InvalidLoginStateException(
                    "Current user disappeared from data layer. Was user just deleted?");
        currentUser = user; // cache result.
    return currentUser;

Tip 17: Document expected exceptions

Javadoc should describe all exceptions a method (intentionally) might throw, even runtime exceptions.

Use @throws to document runtime exceptions even if the exception is not declared in the method’s throws clause. The description in the @throws tag should describe under what conditions the method will throw the given exception.

As an example, look at the getCurrentlyLoggedInUser method in Tip 16. The method can throw two runtime exceptions that are not declared in the method signature, IllegalStateException and InvalidLoginStateException. Even though callers do not have to write a catch block for these exceptions, it is good to inform them of the possibility of these exceptions in case they want to catch and handle them. Even if callers don’t want to catch these runtime exceptions, they might want to document their methods to indicate that these runtime exceptions are possible outcomes from calling their code.


These rules assume you will be using SLF4J or a similiar logging package with efficient logging methods. If you are still using Apache Log4j 1.x, it is time to think about integrating a better logging framework into your project.

Tip 18: Use SLF4J Logger API to avoid wasteful string concatenation

The SLF4J logging API provides several useful improvements over earlier logging APIs and frameworks. One of the major improvements is parameterized logging messages in the Logger interface. E.g.

// *So* old school:
log.trace("At loop count " + i + ", the Fibonacci sequence is: " + sum);

// Better, but still old school and unnecessary code:
if (log.isTraceEnabled()) {
    log.trace("At loop count " + i + ", the Fibonacci sequence is: " + sum);

// The enlightened, more efficient way of logging slf4j-style
log.trace("At loop count {}, the Fibonacci sequence is: {}", i, sum);

The first logging statement uses string concatenation to create the log message. Even if the logging framework were configured not to log at the trace level, at runtime the JVM would still perform three string concatenation operations.

The second logging statement surrounded by the if block prevents the JVM from concatenating the strings if trace level is not being logged, but it requires three lines of code to achieve the efficiency.

The third logging statement uses SLF4J’s parameterized logging. Not only is the logging statement easier to write, the JVM also will not concatenate the strings if logging is turned off at the trace level. The third logging statement also avoids the need for the if block.

The SLF4J Logger interface provides three parameterized method styles to try to avoid string concatenation if the particular the log level is disabled at runtime.

debug(String format, Object arg) // replaces a single {} in format string with arg's string value
debug(String format, Object arg1, Object arg2) // same as above, but replaces two {}
debug(String format, Object... arguments) // replaces an arbitrary number of {} in format string

Note: The parameterized versions of the overloaded debug method above are shown as examples. Logger methods also exist for the other logging levels, such as info, warn, error, etc.

The varargs version of the logging statement on line 3 requires the JVM to create an implicit Object array at runtime, thus SLF4J provides the one-arg and two-arg versions on lines 1 and 2 above to avoid array creation for the common situations of including only one or two variables in the log message.

All three of the parameterized logging method styles can handle exceptions as the final Object parameter. E.g. the following code is valid and understood by SLF4J:

String s = "Hello world";
try {
    Integer i = Integer.parseInt(s);
} catch (NumberFormatException nfe) {
    log.error("Failed to parse '{}' as an integer.", s, nfe);

Even though the call to Logger is invoking a method with two Object parameters (s and nfe), the logging framework correctly interprets the unmatched nfe argument (since there is only one placeholder {} in the format string) as an exception, and will log its stack trace.

Tip 19: Use the appropriate log level

Choosing which log level to use for a given log message is not always obvious. Some levels, like error, are clearer when to use. But the choice between using trace versus debug is less obvious.

The following guidelines are meant as suggestions to help determine what log level to use for the type of message you want to log. No matter what level you choose for a message, aim to be consistent with the type of message you log and the log level you choose.

Log level guidelines

trace Detailed messages to log during development that almost never would be enabled in Production. You might use trace messages to shake out an algorithm when strange things are occurring in the code. Developers often will remove trace-level log statements once problems are solved and the information is no longer needed. One difference between trace and debug is that trace often is used when you need to log a lot of detail that probably is relevant only to you during development and likely would be irrelevant to other developers if they saw the messages in the log.
debug Developer-level information used while writing and debugging code. The information logged likely will be interesting to other developers as well as yourself during development and could help debug the application in Production if strange things start occurring in the code. Log messages at the debug level often remain in the code permanently but are disabled in Production.
info Information that would be useful to system administrators or advanced business users to indicate the status of the system and its business-level processing steps. Log messages at this level often indicate important business functionality has been completed, such as a user logging in, submitting a benefit application, etc. Log messages at the info level sometimes are enabled in Production.
warn Warning messages indicate something unusual has been detected in the state of the data, the application, or its environment that warrants special attention by system maintainers but the application will continue processing. Use the warn level when the message needs to be noted by system maintainers for possible action. For example, a log message indicating the system is running in a special development mode where no passwords or encryption are enabled should be a warn message so maintainers will note this fact if in indeed the system was running in Production mode. Messages at this level should always be enabled in Production.
error Use this level when a problem has occurred in the application that requires immediate attention. For example, the application cannot connect to a database, a file cannot be written because the file system is full or in read-only mode, an unexpected runtime error has occurred and the user is seeing a "whoops" type of message, etc. You should expect that, in Production, system maintainers will receive Slack and SMS alerts when the logging framework sees error-level messages, so use this level appropriately.

Tip 20: Include helpful information in logging statements

Log messages you write should provide context so readers of the log will understand what the message is trying to say. That is, the system should not die with the log reporting only that a FileNotFoundException occurred. The log message should say what file name was trying to be read and from what directory it was expected to be found.

As appropriate, the log messages should include:

  • The value of data that has caused the situation being logged
  • An indication of the application’s state
  • What resource was being accessed that caused the problem
  • Anything else pertinent that explains what was occurring in the application and why

Keep in mind that log messages often will be read by others – sometimes late at night after they have been awoken by angry bosses to deal with a system that has crashed – so try to be as helpful as possible by explaining what was going on in the application at the time. They might not love you for it, but at least they won’t be cursing your name when they realize whose obscure log message kept the system down an extra 2 hours because they had to call and wake up several more people (including perhaps you) to figure out what the log message meant.

When you log the value of variables, it is often helpful to quote the data fields so readers of the log message can clearly determine where the data starts/stops and where the logging text begins/ends. E.g.

log.error("Failed to parse '{}' as an integer.", s);

By quoting the data with ', if the variable s was an empty string, the log message would say “Failed to parse ‘’ as an integer” instead of “Failed to parse as an integer,” which might not be clear to readers what didn’t get parsed.

Tip 21: Log to a Logger

When your application needs to log a message, use your logging framework, not System.out or System.err.

This rule also means do not use an exception’s printStackTrace() method, which logs to System.err.

Even when you are creating new code, go ahead and add the logging code to it right away. That way, you don’t have to go back and change all the System.out statements, and you can set the log output to go to the console for immediate feedback during development.

Tip 22: Take advantage of Logger’s knowledge of null

SLF4J’s Logger, like System.out.println, handles null values gracefully by outputting null references as the string “null” rather than throwing a NullPointerException.

For example:

// This null check is unneeded!
log.error("Form processor invoked with invalid parms. No FormSubmission ID or multiple"
    + " FormSubmission IDs were passed: {}",
    formSubmissionIds == null ? "null" : formSubmissionIds.toString());

// A simpler way that does the same thing:
log.error("Form processor invoked with invalid parms. No FormSubmission ID or multiple"
    + " FormSubmission IDs were passed: {}", formSubmissionIds);

Both statements above do the same exact thing. If the formSubmissionsIds variable in the above code is null, both logging statements will print the value as “null”. Logger will invoke toString() on the given Object parameter only if it is not null. No null check is needed and no call to toString() is needed. The logging framework does the grunt work for you.

Further reading

If you are interested in learning more-advanced Java programming tips, I highly recommend Joshua Bloch’s Effective Java. His 3rd edition covers tips for effectively using the latest Java features like functional interfaces, try-with-resource statements that reduce code that accesses external resources, the @SafeVarargs annotation, as well as more more tips on general Java programming techniques that will help advance you from intermediate to advanced Java programmer.