Jim H
Jim H
  • Home
  • Resume
  • Code Review-BestPractices
  • Java Best Practices
  • Kotlin Best Practices
  • Homophones in English
  • Personal
  • Blog

Best Practices in Java


This is a collection, in no particular order, of best practices learned and shared over a period of years.  Some are specific to Core Java; others are good programming practice independent of language.

Do you have corrections or suggestions? Send me an email.


Copyright © 2019-2025 Jim Hamilton. All rights reserved.

Contents

General Best Practices 

Operator Precedence

Avoid Null Pointer Exception

Immutability and "final"

Constant Definition and Readability

Avoid Unnecessary Autoboxing

Strings and StringBuilders

Collections

Enumerations

Throwables

Integer Arithmetic

Floating-point Arithmetic

Date & Time

Stateless Objects

Encapsulation

Spring & Spring Boot

Serialization & Deserialization

Remote Debugging

Testing

Other General Tips

Don't Do This

Appendix: Code Snippets

General Best Practices

Back to Contents


These are strongly suggested, to make sure that when multiple engineers edit the same code, the only changes are to the code, not wholesale changes to files.  This makes code reviews consistent.

  • Consistency is good.  It helps the reviewer to know what to expect.  It helps the developer who looks at the code a year from now–who might be you.  (Pro tip: Always assume that the next developer who looks at your code has trouble managing anger and knows where you live. 🙃)
    • Always be consistent within a project.
    • It’s best to be consistent for all projects owned by your team.
  • There is no extra credit for making your code difficult to understand (unless you are writing an entry to something like the International Obfuscated C Code Contest; I’m not aware of anything similar for Java, but whatever).  On the other hand, your reviewer and successor will love you if you make your code easier to understand.  (Please don’t waste too much time on that link to ioccc.org; the contest has been around for decades, and there are a lot of “winners” to read through and try.)
  • On the same note: Comments are a Good Thing™.  Your IDE will most likely be able to generate “empty” Javadoc from a template.  Just write a quick comment about the element:
    • For a class, the Javadoc should say what the class is, not what it does.
    • For a method, the comment should say what the method does, what parameters it takes, what it returns, and what (checked) exceptions might be thrown.  (This includes public, protected, and package-private methods; there is no need to document private methods with Javadoc.)
    • For a field, the comment should say how the field is used. (Again, no Javadoc on a private field.)
  • Other than Javadoc, comments are useful any time reading the code is not “easy.”  But see the previous point about “difficult to understand” code.  If you can’t understand your own code, it’s a contest entry.  You should also be able to understand code your teammates write when you are reviewing their code.  If in doubt, ask!
  • Remember that your code is yours until it has been merged.  Once the code is merged, it’s ours, not merely yours.  That means anyone who is on duty may have to read, understand, and possibly change the code. The implication is that reviewing the code means accepting responsibility for the code.
  • Use meaningful identifier names, and in general, don't use single-letter names. It might feel appropriate to name your loop indices i, j, and k, but "outer", "middle", and "inner" will make more sense–especially a year from now, or to your successor. As an aside, I once saw a function with eight variables, named abc, abC, aBc, aBC, Abc, AbC, ABc, and ABC. Yes, it was for a contest entry…
  • On the same subject: the rules for Java identifiers are similar to other languages' rules: names may contain letters, numbers, underscores, and dollar signs; names must begin with a letter, _ or $; they are case-sensitive, and they may not contain whitespace. (And of course, they must not match reserved keywords.) The Java style guide specifies further guidelines:
    • Local variables, method names, and field names should begin with a lower-case letter, and words should break with camelCase.
    • Class names, interface names, and annotations should begin with an upper-case letter, and words should break with UpperCamelCase.
    • Constant names should be upper-case and break words with SCREAMING_SNAKE_CASE.
  • All that said, Java uses Unicode throughout. Even though identifiers are restricted to letters, numbers, _, and $, there are thousands more letters than the 52 upper- and lower-case letters in ASCII. It is perfectly legal to name a variable "señor" or "façade," or even "पहचानकर्ता," but please don't give into temptation. (A good IDE will issue a warning about non-ASCII characters in an identifier.)


Now, some specifics:

  • Always use { braces } around single-instruction if, else, and loop blocks.  (35 years’ experience tells me this one rule makes code much easier to review and debug.)
 Why? Because we've all occasionally had an extra semicolon sneak in. This would not matter if the code is inside braces, but what if comes before your single statement? Consider:

                       if (foo == 1);          // ← note semicolon

                            callDangerousMethod();  // called even when foo != 1

  • Turn on your IDE’s helpers:
    • Code generation for accessors and constructors – they will be correct
    • Automatic imports, and import cleanup
    • Static code analysis can detect bugs with compile-time warnings (or, when your IDE does continuous compilation, at edit time).
    • Some IDEs (IntelliJ IDEA, for one) can simplify complex expressions, automatically turn loops into lambdas and vice/versa, swap expressions on either side of a comma, invert condition tests.
    • Share a preferences package, so that everyone is using the same helpers.
  • Never use  "import package.*;" Instead, always import classes explicitly.  If your IDE tries to do this, stop it!  (IntelliJ IDEA: in Preferences, filter on “java import”; Under “Code Style / Java” there is a tab for Imports.  Set “Class count to use import with ‘*’” to some huge number–I like “9999”.  Do the same thing for static imports.)
  • Always use spaces, and not tabs, at the beginning of lines.  (For IntelliJ IDEA: the same preference panel has a “Tabs and Indents” section.  Make sure “Use tab character” is turned off.)  Why? Because different tools may use different widths for tab characters, so a line beginning with 4 spaces might have a different indentation from a line beginning with a tab.
  • Everyone should use the same tab size and indent values (default for Java is 4, with a continuation value of 8).
  • I don’t care if you use Windows line endings (CRLF) or Unix line endings (LF or newline); but once a file is in source code control, stick with whatever line ending was first used for that file.  Most IDEs (including IntelliJ IDEA) can handle either or both.  Changing the line ending will, perforce, change every line in the file.  If you do this, you will destroy the history of who changed which lines. The 'git blame' command exists for a reason!
  • Teams should agree on layout of classes.  Suggestion:
            

access-modifier [abstract] class ClassName 

                [extends … implements …] {

       constants

       fields

       constructors

       abstract methods

       public/protected methods

       private methods

       accessors 

}

  • Another suggestion is to group methods that refer to each other, or "go together" logically, regardless of their access levels. (This is spelled out explicitly in the Kotlin style guide, as referenced here.)
  • Some teams like to alphabetize methods, rather than sort by access level or logical placement. Don't do that; the IDE may be able to show a method index in alphabetical order, no matter where they appear in the file.
  • Trivial, ad hoc, classes do not necessarily need to make their fields private.  For example, a geometric Point class could have public fields x and y, and no logic. (By the way: this is an intentional exception to the guideline about single-letter names. When the concept has universal consensus, such as x, y, z for 3-dimensional coordinates, it's fine to go with the crowd. But if you're showing polar coordinates, name them "rho," "theta," and "phi,"  not "ρ,"  "θ," and "φ.")
  • Package-private access (no access modifier) should usually have a comment stating that the field/method is intended to be package-private; that is, that the developer did not “forget” to add “private”. 

public class Foo {

   public long field1;

   long field2; // package-private

   private long field3; 

}

Operator Precedence

Back to Contents


We all remember PEMDAS from elementary school, right? Parentheses, Exponents, Multiplication/Division, Addition/Subtraction. (I’ve seen some remember a different mnemonic, BEDMAS, where parentheses are called “braces,” and equal-precedence operations got switched, but that’s the same thing. Sounds like British Commonwealth; “countries separated by a common language” and all that.) Well, Java (like its cousins derived from parent C and grandparents B and BCPL) has its own operator precedence.  It’s defined here. It’s important that you know this, or at the very least, know where to look it up.

Also, the IDE can help you.  Consider this Boolean expression: 

a || b && c

In IntelliJ Idea, if you type Alt-Enter while your cursor is on that expression, one of the options you will see is “Add clarifying parentheses”. Selecting that command will yield: 

a || (b && c)

When I tried a real-world expression:

foo.equals(code) || container != null && container.contains(code)

the clarified code became:

foo.equals(code) || ((container != null) && container.contains(code))

This is because != is an operator with higher precedence than &&, which has higher precedence than ||.


To see where operator precedence can lay a pitfall, consider a common pattern in C, with bitmasks defined.

#define BIT_0 1    /* (1 << 0) -- don't do a useless operation */

#define BIT_1 1 << 1

#define BIT_2 1 << 2

#define BIT_3 1 << 3 

/* check that bit 2 is set in foo */

if (foo & BIT_2) ...

This works fine, because the & (bitwise and) operator has a lower precedence than the << (shift left) operator. But consider a slightly different operation:

/* check that foo has only bit 3 set (i.e., foo == 8) */ 

if (foo == BIT_3) ...

Remember that #define just substitutes the text after the symbol for each occurrence of the symbol; there is no evaluation. The equality operator has a higher precedence than the shift operator, so the expression tested in the if is 

(foo == 1) << 3

When foo has the value 8, that expression’s value is 0, or false. If foo happened to be 1, the expression evaluates to 8, which evaluates to true. So in those two cases, the expression gives the wrong result. However, if foo’s value is anything else, the expression is false (which is correct). Therefore, we get the right answer, most of the time! Not exactly what we wanted… At least the “clarifying parentheses” would hint that something’s wrong. And, yes, I know that we could fix the defines:

#define BIT_0 1    /* (1 << 0) */

#define BIT_1 (1 << 1)

#define BIT_2 (1 << 2)

#define BIT_3 (1 << 3 )

Or, do something completely better, such as using actual constants instead of text substitution:

const int BIT_0 = 1;

const int BIT_1 = 1 << 1;

const int BIT_2 = 1 << 2;

const int BIT_3 = 1 << 3;

Avoid NullPointerException

Back to Contents


  • When receiving an object from another method, make sure it isn’t null before dereferencing it.  (Usually, if the method documentation promises that the return value is not null, that’s good enough.) If the method has a @NotNull or @NonNull annotation (see below), then static analysis will ensure that the method won’t return null, and the caller does not need to worry about it.
  • In Java 8 and higher, you can return Optional<T>.  If a method returns an Optional, the user method can call isPresent() or isEmpty() to check whether the object exists. (isEmpty() exists in Java 11 and higher.) Or, you can define a lambda with ifPresent():

  Optional<Foo> optionalFoo = maybeGetFoo();

  optionalFoo.ifPresent(foo -> operateOnFoo(foo));

  • As the implementer of maybeGetFoo(), you can return the value returned by the static methods Optional.of(value) or Optional.ofNullable(value). If you know you want to return the null value, return Optional.empty().
  • When comparing an object reference with a constant via .equals(), dereference the constant (which is an object) instead of the variable:

      if (“constant”.equals(variable)) { /* do something */ }


  • Or, if you have defined a symbol for your constant, use that:

  private static final String CONSTANT = “constant”;

  public boolean method() {

    if (CONSTANT.equals(variable)) { return something; }

  }

  • Apache Commons includes the StringUtils class. This class includes many static methods for String comparisons and other utilities, all of which are null-safe. There are many other useful classes in Apache Commons–take a look.
  • Chained dereferences are dangerous, unless you have confirmed that the references are not null.

  class Foo {

    Object bar; 

  }

  Foo foo = new Foo(); 

  System.err.println(foo.bar.toString()); //crash--bar is null


  • It’s not always necessary to call .toString() explicitly. Let’s rewrite the example above:

  class Foo {

    Field bar; 

  }

  Foo foo = new Foo();

  System.err.println(foo.bar); // outputs "null" to stderr


  • JetBrains IntelliJ Idea provides the @Nullable and @NotNull annotations.  (See also Spring 5 annotations @NonNull, @NonNullFields, @NonNullApi and @Nullable. Pick one and use it.)  These provide some inexpensive static analysis to explicitly declare that a method can, or cannot, return null, or that parameters may or may not be null. Warnings will be raised if you dereference a @Nullable value without checking for null. Assertions will be raised if a @NotNull is actually null.  The contracts are inherited by derived classes; they can be strengthened, but not relaxed.  Note:  annotations with those names are available elsewhere; make sure you add the right dependency element to your project.


Maven:


<dependency>

  <groupId>org.jetbrains</groupId>

  <artifactId>annotations</artifactId>

  <version>20.1.0</version>

 </dependency>


Gradle:

dependencies { implementation'org.jetbrains:annotations:20.0.1' }


  • Keep the version up to date, of course.  Note that using these annotations does not protect you from null values–all they do is let you see a warning when you might be misusing an API. 
  • When you’re really, really tired of dealing with NPEs, that’s the time to learn Kotlin. 😊 This is a language developed by JetBrains, that runs in the JVM, and Kotlin classes are interoperable with Java classes.  But methods can be defined never to return a null (you actually need an extra char to allow it to return a null) and never to take a null parameter (same). 


It could be worse. The developers of JavaScript looked at null references, and said, “Hey, that’s cool! Let’s have two of those!”

Immutability and final

Back to Contents


Most classes in the Java library make immutable objects: they cannot be changed after they are instantiated. String, the temporal classes in java.time, the auto boxed subclasses of Number, are all examples of immutable classes. (Collection classes, such as ArrayList, TreeSet, etc. are mutable, in that elements may be added or removed from a collection.)

The keyword final is used to make a local variable or a member immutable. However, if a final object is mutable, only the reference is immutable; the referent object can still change.


“Effectively final”

Starting in Java 8, objects are final by default. If a reference takes an assignment after its initialization, it is, by definition not final.  However, any reference that is not changed after its initialization is “effectively final” and may be treated as final by language structures.


Final Methods and Classes

A class may be declared final. This prevents the class from being extended. An enumeration (keyword enum) is a class that is always final, so the final keyword would be redundant (and an error). An interface may not be final; because interfaces are meant to be implemented elsewhere, they are, by definition, not final.

A method may be declared final. This prevents the method from being overridden in a class that extends the base class. Another look at enumerations shows an example; see the section on Enumerations below.

Constant Definition and Readability

Back to Contents


This applies to any programming language. We often want to avoid using “magic numbers,” so we define constants: 

public static final int THE_ANSWER = 42;

OK, that’s brilliant. But how did we get 42? 

public static final String THE_QUESTION = “What do you get if you multiply six by nine”; 

(Source: The Restaurant at the End of the Universe by Douglas Adams, the second of 5 books in the Hitchhiker’s “trilogy.”) 


Enough whimsy.  There will be times when you want to define a constant that means “one megabyte” (2 to the 20th power). You may simply write: 

public static final long ONE_MEGABYTE = 1048576;

which is correct. But the reader would appreciate knowing that you were correct. Instead, write one of the following: 

public static final long ONE_READABLE_MEG = 1024 * 1024; 

public static final long TWO_TO_THE_TWENTIETH_POWER = 1 << 20; 


Which of the following would you prefer to read in someone’s code? 

public static final long MILLIS_PER_DAY_TRUST_ME = 86400000L; 

public static final long MILLIS_PER_DAY_READABLE = 1000 * 60 * 60 * 24; 


Or better still: 

public static final long SECONDS_PER_HOUR = 60 * 60; 

public static final long SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;

public static final long MILLIS_PER_DAY = 1000 * SECONDS_PER_DAY; 


The value of MILLIS_PER_DAY_TRUST_ME is correct—trust me! (Well, most days, except for the two days each year when many places change from one time zone to another—an example of a "corner case.") But I did double-check it, just to be sure I had not fat-fingered the number. You can check it yourself, or you can make your constants readable, which makes them easy to check at a glance. You really don’t want to have to find an incorrect constant in the middle of your code. (Trust me!)

One more bit of whimsy:

If we work in tridecimal (base 13), 6 × 9 actually is 42.

Avoid Unnecessary Autoboxing

Back to Contents


Containers can only contain objects.  That’s why Odin (or insert the god of your choice) created autoboxing.  But an autoboxed value is an object, that has to be created, and perhaps destroyed.  This always involves overhead.

  • Never (never, never) use an autoboxed (Integer, Short, Character or Long) as a loop counter.  Those objects are immutable; adding 1 creates a new object—every time through the loop.
  • Don’t use Boolean to create a 3-state (true, false, null) variable.  Create an enum instead.
  • Don’t compare two Long, Short, or Integer values with equality comparison operators (== or !=); those operators will compare object references.  Will it work? Depends.  Java caches boxed values from -128 to 127 (DO NOT DEPEND ON THAT!) so if your boxed objects contain that value, you’re good.  For any other case, kaboom!  Consider:

public class Main {

  public static void main(String[] args) {

    Long twoK = 2000L;

    Long dosK = 2000L;

    System.err.println("==: " + (twoK == dosK));

    System.err.println(".equals(): " + twoK.equals(dosK));

  } 

}

 This program produces the following output:

==: false 

.equals(): true

  • Don’t compare Float or Double values with == or !=, for the same reason as for the integral boxed types. (For that matter, don’t compare float or double primitive values with == or !=, either! To understand why not, see the section on floating-point arithmetic.)
  • You may compare a boxed value to a primitive with == or !=, as long as you are sure that one side is a primitive. (Also, don't try to dereference a primitive—but at least that will produce a compiler error.)
  • You may compare a boxed value with a numeric expression with == or !=. The object will be unboxed before the comparison.
  • Inequality comparison operators (>, <, >=, <=) are OK, because the objects are unboxed before this comparison.  (Unlike equality, there’s no such thing as “is this object greater than that object?” for generic objects.) 
  • Prefer to make fields primitives rather than boxed, for the reasons above.  One exception is for a class that can be serialized, especially to XML or JSON, where a null field can be left out of the serialization, or a missing field can be deserialized to null.  (More on serialization will follow. Much more.)

Strings and StringBuilders

Back to Contents


A String object is a sequence of UTF-16 characters or surrogate pairs. (For a description of UTF-16, see this article.) In fact, the class String implements the interface CharSequence. It is fine to think of a String as a sequence of code points; any code points that do not fit into the Basic Multilingual Plane (BMP) are instead represented by surrogate pairs.

  • As mentioned above, a literal string is an object of type String.
  • String objects (like most objects) are immutable.
  • A statement doing String concatenation with the + operator works by creating a StringBuilder behind the scenes. This has implications for your code:  A single statement with one or more + operators making a string is fine.  But if you intend to build up a String, and will need more than one statement to do it, you should instantiate a StringBuilder explicitly. Otherwise, multiple StringBuilder objects would be created.
  • Strings have a natural sorting order. When comparing two String objects, start from the initial character. If the code points at that position are different, the code point with a lower integer value will sort first; repeat until one String ends. If both Strings are the same length, they are equal; otherwise, the shorter one sorts first. (Special handling is needed for surrogate pairs; there are code points in the BMP that sort higher than each part of a surrogate pair, but the code point represented by a surrogate pair is always higher than one in the BMP.)
  • However, String implements the interface Comparable<String>, and the implementation handles this correctly. This makes TreeSet<String> and TreeMap<String, ?> sort correctly, without any additional worry.
  • Be careful when attempting to reverse a String/CharSequence. The naive approach would be to just reverse the char values in the sequence. This would break  surrogate pairs (which would be possible to detect, as they are invalid in reverse order); but it would also break "decomposed" sequences with combining characters, so that diacritical marks would appear on the wrong character. It is possible to do this safely, but the method is beyond the scope of this article.
  • This should not be an issue purely within Java, but it is possible to store UTF-16 text in either byte order: Big Endian (high byte first) or Little Endian (low byte first). The Unicode Standard includes a character called "Zero-Width Non-Breaking Space" (\uFEFF) which is used as the Byte Order Marker (BOM). A Little Endian UTF-16 text sequence would start with FFFE. (If a rendering engine tries to interpret the BOM as a character, that character is invisible.)
  • The endianness issue is just one reason most text is stored as UTF-8. When storing text, you can use the String method getBytes("UTF-8") to get a byte array encoded in UTF-8. If the resulting byte array is to be shared or streamed, make sure the consumers know its encoding. This can be done with an HTTP header or a byte order marker. (In UTF-8, code point \uFEFF is encoded as EF BB BF.)
  • When you need to decode a byte array, String has a constructor that takes a byte array and an encoding type [String(byte[] bytes, String charSetName)]. If there is a header that says the text is UTF-16BE or UTF-16LE, you know that it is Big Endian or Little Endian, respectively. Otherwise, if there is a BOM, respect that. If a header says "UTF-8", or you see the UTF-9 rendering of the BOM, it's easy. If there is no BOM, or the header just says "UTF-16", the "standard" assumption is that the text should be Big Endian. However, because Windows (it's always Windows) is implemented on Intel, which is a Little Endian architecture, the ubiquitous assumption is that it's Little Endian.
  • If you have UTF-16 and don't have another clue as to which end comes first, count bytes with value zero. Since (especially in code, including HTML, XML, Java, C, even Pascal, ADA, Algol, and various assembly languages) there will be a lot of ASCII characters, and these all have values less than 128 decimal or 80 hexadecimal, the "high" byte of these characters is zero. If most "zero" bytes are at even offsets, the text is Big Endian.

Collections

Back to Contents


Unnecessary Object Creation – Empty Collections

The class java.util.Collections contains generic methods to return an empty Set, List, or Map cast to any member type; the method names can be considered “intuitive:” Collections.emptySet(), .emptyList(), .emptyMap().  As long as the empty collection you “create” can be considered immutable, you may use these methods to satisfy your needs.  The major advantage is, you have a collection reference that is not null, so it can be used in a loop or lambda (that will execute zero times).

Singleton collections

Analogous to the empty collections, Collections provides singleton collections: immutable collections that contain exactly one object.  Collections.singleton(item) returns a set with one item; Collections.singletonList(item) returns a list; and there is a singletonMap(key, value).  The actual implementations are very optimized for speed; and are private to the Collections class. These collections are immutable.

Maps

Avoid using .containsKey() when you actually need the value.  Calling .containsKey() is nearly as expensive as .get(); if you’re going to call .get() anyway, just call it once (assign the result to a local variable) and compare it with null.  (Exception: if the value for an existing key can actually be null, and you care about the distinction from a key that doesn’t exist, you need containsKey() to distinguish this case from when the entry does not exist.)

Mutable vs. Immutable Collections

Most of the collections you will create, such as ArrayList, HashSet, LinkedHashMap, BlockingQueue, are mutable; you may add, insert, and remove elements. Never assume that collections you do not create are mutable. If you need to modify a collection that you did not create, it’s best to copy its elements to a new, mutable collection. In the Javadoc for the interfaces that define collections, methods such as .add(), .put(), .remove() are shown as optional operations, and are either not implemented, or throw an Error, for immutable collections. (Well, calling a method that is not implemented will also throw an Error.)

Enumerations

Back to Contents


An enum is (sort of) a class.  Classes (including enums) can have fields and methods.  But it’s only sort of a class.  Mainly, it inherits from Enum (instead of directly from Object); and in that you can’t derive a class from an enum.  Inheritance flows from the enum to the members of the enum; that is, you can define a method for the enum, and override it for a single (or multiple) enumeration(s).  This can make for client code that is much cleaner than a huge switch statement.

Enum Features

  • Enum.toString() can be overridden; Enum.name() is declared final.  Which is the right one to use depends on your use case; but .name() will not vary from one version to the next, as long as the declared name of the enumeration does not change.
  • Enum.equals(Object other) is written as 

public final boolean equals(Object other) {

  return this == other;

}

  • In this case, the overhead of a method call is trivial, but not zero; but as one can see, it’s perfectly safe to compare enumerations with == and !=.
  • Avoid using Enum.ordinal(). If the enum is re-ordered, or a new value is inserted, ordinality changes. (Also, water is wet, and it got dark last night, unless you live in a polar region.) If you must depend on ordinality, leave a comment explaining that, and explaining why.  Be prepared to defend your reasons during code review.  (Code reviewers: be prepared to reject .ordinal()) 
  • Avoid using EnumType.compareTo(). The important part of the implementation is a single line:  return this.ordinal - other.ordinal;  You often won’t care about “less than” or “greater than”, only “equal to”; so use == to compare.
  • Do use EnumType.valueOf(string), especially for deserialization; but be prepared to catch an IllegalArgumentException, in the case where string is not the name of a member in EnumType.

NextHome

Copyright © 2019-2025 Jim Hamilton - All Rights Reserved.

Powered by

This website uses cookies.

We use cookies to analyze website traffic and optimize your website experience. By accepting our use of cookies, your data will be aggregated with all other user data.

DeclineAccept