DEV Community

Alexsandro Souza
Alexsandro Souza

Posted on • Updated on

Java Best Practices Quick Reference

Developers have a big responsibility to make the right decision every day and the best thing to help make good decisions is the experience. Not everyone has long experience in software development, but everyone can leverage others' experience. These are some tips that I have acquired with my experience in working with Java and I hope it can help you to improve the readability and reliability of your Java code.

Programming Principles

Don’t write code that the only works. Aim to write code that can be maintained — not only by yourself but by anyone else who may end up working on the software at some point in the future.

80% of the time a developer is reading code and 20% writing and testing the code. So, please focus on writing readable code!

Your code should not need comments to understand what it is doing!

To help us to develop good code, there are many programming principles that we can use as guidelines. Below I will list the most important ones.

  • KISS — It stands for “Keep It Simple, Stupid”. You may notice that developers at the beginning of their journey try to implement complicated, ambiguous design.
  • DRY — “Don’t Repeat Yourself”. Try to avoid any duplicates, instead, you put them into a single part of the system or a method.
  • YAGNI — “You Ain’t Gonna Need It”. If you run into a situation where you are asking yourself, “What about adding extra (feature, code, …etc.) ?”, you probably need to re-think it.
  • Clean code over clever code — Speaking of clean code, leave your ego at the door, and forget about writing clever code.
  • Avoid premature optimization — The problem with premature optimization is that you can never really know where a program’s bottlenecks will be until after the fact.
  • Single responsibility — Every class or module in a program should only concern itself with providing one bit of specific functionality.
  • Composition over Inheritance — Objects with complex behaviors should do so by containing instances of objects with individual behaviors rather than inheriting a class and adding new behaviors.
  • Object calisthenics — Object Calisthenics are programming exercises, formalized as a set of 9 rules
  • Fail fast, fail hard The fail-fast principle stands for stopping the current operation as soon as any unexpected error occurs. Adhering to this principle generally results in a more stable solution

Packages

  1. Favor structuring packages by domain concerns rather than technical layers.
  2. Favor layouts that promote encapsulation and information hiding to protect against improper usage over organizing classes by technical concerns.
  3. View packages as providing a strict API — do not expose the inner workings (classes) that are meant for internal processing only.
  4. Not public access scope for classes that are supposed to be only used inside the package.

Classes

Static

  1. Do not allow instantiation of a static class. Always create a private constructor.
  2. Static classes should be stateless, immutable, not allow subclassing, and thread-safe.
  3. Static classes should be side-effect free and provided as utilities, such as filtering a list.

Inheritance

  1. Prefer composition over inheritance.
  2. Do not expose protected fields. Provide a protected accessor instead.
  3. If a class variable can be marked final, make it.
  4. If inheritance is not expected, make the class final.
  5. Mark a method final unless subclasses are expected to be allowed to override it.
  6. If no constructor is required do not create a default one with no implementation logic. Java will automatically provide a default constructor if none is specified.

Interfaces

  1. Do not use the constant interface pattern (an interface of constants) as it allows classes to implement and dirty up the API. Use a static class instead. This has the added benefit of allowing you to perform more complex object initialization in a static block (such as populating a Collection).
  2. Avoid Interface overuse
  3. Having one and only one class implement an interface is likely to overuse interfaces and it does more harm than good. More
  4. “Program to an interface, not to an implementation” doesn’t mean you should pair each and every one of your domain classes with a more or less identical interface, doing so, you are violating YAGNI
  5. Always keep interfaces small and specific so that clients will only have to know about the methods that are of interest to them. Check out the ISP from SOLID.

General

Assertions

An assertion, usually in the form of a precondition check, enforce the type’s contract in a fail-fast, fail-hard manner. They should be used liberally to catch programming errors as close to the source as possible.

Object state:

  • An object should never be constructed or transition into an invalid state.
  • On constructors and methods, always describe and enforce the contract through validations.
  • The Java keyword assert should be avoided as it can be disabled and is generally a brittle construct.
  • Use the Assertions utility class to avoid verbose if-else conditions for precondition checks.

Generics

A full, extremely detailed explanation is available in the Java Generics FAQ. Below are the common scenarios that developers should be aware of.

  1. When possible, prefer using type inference rather than returning a base class/interface:
// MySpecialObject o = MyObjectFactory.getMyObject();
public <T extends MyObject> T getMyObject(int type) { 
 return (T) factory.create(type);
}
Enter fullscreen mode Exit fullscreen mode
  1. When a type cannot automatically be inferred, inline it.
public class MySpecialObject extends MyObject<SpecialType> {
 public MySpecialObject() {
 super(Collections.emptyList());   // This is ugly, as we loose type
 super(Collections.EMPTY_LIST();    // This is just dumb
 // But this is beauty
 super(new ArrayList<SpecialType>()); 
 super(Collections.<SpecialType>emptyList());
 }
}
Enter fullscreen mode Exit fullscreen mode

Singletons

A singleton should not be written in the classic Design Patterns style, which is quite valid in C++ (as it was written with) but inappropriate in Java.

  1. While correctly thread-safe, never implement as follows. (This has been a performance bottleneck!)
public final class MySingleton {
 private static MySingleton instance;
 private MySingleton() {
   // singleton
 }

 public static synchronized MySingleton getInstance() {
 if (instance == null) {
   instance = new MySingleton();
 }
   return instance;
 }
}
Enter fullscreen mode Exit fullscreen mode
  1. If lazy initialization is truly desirable, then a combination of the two approaches will do the job!
public final class MySingleton {
 private MySingleton() {
   // singleton
 }
 private static final class MySingletonHolder {
   static final MySingleton instance = new MySingleton();
 } 

 public static MySingleton getInstance() {
   return MySingletonHolder.instance;
 }
}
Enter fullscreen mode Exit fullscreen mode
  1. Spring: By default, a bean is registered with a singleton scope, meaning that only one instance will be created by the container and be wired to all consumers. This provides the same semantics as a normal Singleton, without the performance or coupling limitations.

Exceptions

  1. Use checked exceptions for recoverable conditions and run-time exceptions for programming errors. Example: Getting an Integer from a String.
  • Bad: NumberFormatException extends RuntimeException, so it is meant to indicate programming errors.
  • Do not do the following:
// String str = input string
Integer value = null;
try {
 value = Integer.valueOf(str);
} catch (NumberFormatException e) {
  // non-numeric string
}
if (value == null) {
  // handle bad string
} else {
   // business logic
}
Enter fullscreen mode Exit fullscreen mode
  • Correct usage:
// String str = input string

// Numeric string with at least one digit and optional leading negative sign
if ( (str != null) && str.matches("-?\\d++") ) { 
 Integer value = Integer.valueOf(str);
 // business logic
} else {
  // handle bad string
}
Enter fullscreen mode Exit fullscreen mode
  1. You should handle exceptions in the right place, the right place is at the domain level.
  • WRONG WAY — The data object layer doesn’t know what to do when there is a database exception.
class UserDAO{
 public List<User> getUsers(){
 try{
   ps = conn.prepareStatement("SELECT * from users");
   rs = ps.executeQuery();
   //return result
 }catch(Exception e){
   log.error("exception")
   return null
   }finally{
     //release resources
   }
  }
 }
Enter fullscreen mode Exit fullscreen mode
  • RECOMMENDED WAY — The data layer should just rethrow the exception and transfer the responsibility to handle the exception or not to the right layer.
class UserDAO{
 public List<User> getUsers(){

 try{
   ps = conn.prepareStatement("SELECT * from users");
   rs = ps.executeQuery();
   //return result
   }catch(Exception e){
         throw new DataLayerException(e);
     }finally{
       //release resources
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Exceptions should in general NOT be logged at the point they are thrown, but rather at the point they are actually handled. Logging exceptions when they are thrown or rethrown tends to fill the log files with noise. Also, note that the exception stack trace captures where the exception was generated anyway.

  2. Favour the use of standard exceptions

  3. Use Exceptions rather than Return codes.

Equals and HashCode

There are a number of concerns to be aware of for writing proper object equivalence and hash code methods. To simplify usage, use java.util.Objects’ equals and hash.

public final class User {
 private final String firstName;
 private final String lastName;
 private final int age;
 ...
 public boolean equals(Object o) {

 if (this == o) {
   return true;
 } else if (!(o instanceof User)) {
   return false;
 }
 User user = (User) o;
 return Objects.equals(getFirstName(), user.getFirstName()) && 
 Objects.equals(getLastName(),user.getLastName()) &&
 Objects.equals(getAge(), user.getAge());
 }

 public int hashCode() {
   return Objects.hash(getFirstName(),getLastName(),getAge());
 }
}
Enter fullscreen mode Exit fullscreen mode

Resource Management

  1. Methods for safely releasing resources:
  2. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource.
private doSomething() {

try (BufferedReader br = new BufferedReader(new FileReader(path))) {

 try {
   // business logic
 }
}
Enter fullscreen mode Exit fullscreen mode

Provide Shutdown Hooks

Provide a shutdown hook to be called if the JVM is gracefully terminated. (This will not handle abrupt terminations, such as due to a power outage)

This is the recommended alternative instead of declaring a finalize() method, which will only be run if System.runFinalizersOnExit() is true (by default it is false).


public final class SomeObject {
 var distributedLock = new ExpiringGeneralLock ("SomeObject", "shared");
 public SomeObject() {

 Runtime
  .getRuntime()
 .addShutdownHook(new Thread(new LockShutdown(distributedLock)));
 }

 /** Code may have acquired lock across servers */
 ...

 /** Safely releases the distributed lock. */

 private static final class LockShutdown implements Runnable {

 private final ExpiringGeneralLock distributedLock;

 public LockShutdown(ExpiringGeneralLock distributedLock) {
 if (distributedLock == null) {
   throw new IllegalArgumentException("ExpiringGeneralLock is null");
 }
   this.distributedLock = distributedLock;
 }
 public void run() {
  if (isLockAlive()) {
   distributedLock.release();
  }
 }
 /** @return True if the lock is acquired and has not expired yet. */

 private boolean isLockAlive() {
   return distributedLock.getExpirationTimeMillis() > System.currentTimeMillis();
 }
 }
}
Enter fullscreen mode Exit fullscreen mode

Allow resources to expire (and also be renewable) is shared between servers. (This allows recovery from abrupt termination, such as power outages).

See code sample above, which uses an ExpiringGeneralLock (a lock that is shared across systems).

Date-Time

Java 8 introduces a new date-time API under the package java.time. With Java 8, a new Date-Time API is introduced to cover the following drawbacks of old date-time API: Not thread-safe, Poor design, Difficult time zone handling and etc.

Concurrency

General

  1. Beware of the following libraries, which are surprisingly not thread-safe. Always synchronize against the objects if shared between multiple threads.
  2. Date (not immutable) — Use the new Date-time API that is thread-safe;
  3. SimpleDateFormat — Use the new Date-time API that is thread-safe;
  4. Prefer using java.util.concurrent.atomic classes over making variables volatile.
  5. The behavior of the atomic classes is more obvious to the average developer, whereas volatile requires understanding the Java Memory Model.
  6. The atomic classes wrap volatile variables in a more user-friendly interface.
  7. Understand the use-cases where volatile is appropriate. (see article)
  8. Use Callable when requiring a checked exception but there is no return type. As Void cannot be instantiated, this communicates the intent and can return null safely.

Threads

  1. java.lang.Thread should be considered depreciated. While it is not, officially, in almost all instances the java.util.concurrent package provides a cleaner solution to the problem.
  2. It is considered poor practice to extend java.lang.Thread — instead implement Runnable and create a new thread with the instance in the constructor (rule of composition over inheritance).
  3. Prefer executors and streams when required concurrent processing
  4. It is always a good idea to specify your own custom thread factory to control the configuration of the threads being created. More
  5. Use DaemonThreadFactory in Executors for non-critical threads so that the thread pool can be shut down immediately on server shutdown. more

this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {

 Thread thread = Executors.defaultThreadFactory().newThread(runnable);
 thread.setDaemon(true);
 return thread;
});
Enter fullscreen mode Exit fullscreen mode
  1. Java synchronization is no longer slow (55–110ns). Do not avoid it by using broken tricks like double-checked locking.
  2. Prefer synchronizing against an internal object, rather than the class, as users may synchronize against your class/instance.
  3. Always synchronize multiple objects in the same order to avoid deadlocks.
  4. Synchronizing against the class does not inherently block access to its internal objects. Always use the same locks when accessing a resource.
  5. Beware that the synchronized keyword is not considered part of a method’s signature and will thus not be inherited.
  6. Avoid excessive synchronization, it can cause reduced performance and deadlock. Use the synchronized keyword strictly to the piece of code that requires synchronization.

Collections

  1. Use Java-5 concurrent collections when possible in multi-threaded code. These are safe and have superior performance.
  2. Use CopyOnWriteArrayList over synchronizedList when suitable
  3. Use Collections.unmodifiable list(…) or copy the collection when receiving it as a parameter new ArrayList(list). Avoid having local Collections changed from outside your class.
  4. Always return a copy of your collection, avoiding your list be changed from outside the new ArrayList(list)
  5. Each collection should get wrapped in its own class, so now behaviors related to the collection have a home (e.g. filter methods, applying a rule to each element).

Miscellaneous

  1. Prefer lambdas to anonymous classes
  2. Prefer method references to lambdas
  3. Use enums instead of int constants.
  4. Avoid use float and double if exact answers are required, use BigDecimal instead, ex Money
  5. Prefer primitive types to boxed primitives
  6. The use of magic numbers in the code should be avoided. Use constants
  7. Don’t return Null. Communicate with your method client with Optional. The same for Collections — Return empty arrays or collections, not nulls
  8. Avoid creating unnecessary objects, reuse objects, and avoid unnecessary GC clean up

Lazy Initialization

Lazy initialization is a performance optimization. It’s used when data is deemed to be “expensive” for some reason. With Java 8 we should use the Supplier functional interface for that.


== Thread safe Lazy initialization ===

public final class Lazy<T> {
 private volatile T value;
 public T getOrCompute(Supplier<T> supplier) {
   final T result = value; // Just one volatile read
   return result == null ? maybeCompute(supplier) : result;
 }

 private synchronized T maybeCompute(Supplier<T> supplier) {
 if (value == null) {
   value = supplier.get();
 }
   return value;
 }
}
Lazy<String> lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
Enter fullscreen mode Exit fullscreen mode

That's it for now, hope it was useful!

Top comments (0)