JavaRush /Java Blog /Random EN /Coffee break #56. A Quick Guide to Best Practices in Java...

Coffee break #56. A Quick Guide to Best Practices in Java

Published in the Random EN group
Source: DZone This guide includes the best Java practices and references to improve the readability and reliability of your code. Developers have a big responsibility to make the right decisions every day, and the best thing that can help them make the right decisions is experience. And although not all of them have extensive experience in software development, everyone can use the experience of others. I have prepared some recommendations for you that I have gained from my experience with Java. I hope they help you improve the readability and reliability of your Java code.Coffee break #56.  A Quick Guide to Best Practices in Java - 1

Programming Principles

Don't write code that just works . Strive to write code that is maintainable —not just by you, but by anyone else who may end up working on the software in the future. A developer spends 80% of his time reading code, and 20% writing and testing code. So, focus on writing readable code. Your code shouldn't need comments for anyone to understand what it does. To write good code, there are many programming principles that we can use as guidelines. Below I will list the most important ones.
  • • KISS – Stands for “Keep It Simple, Stupid.” You may notice that developers at the beginning of their journey try to implement complex, ambiguous designs.
  • • DRY - “Don't Repeat Yourself.” Try to avoid any duplicates, instead putting them into a single part of the system or method.
  • YAGNI - “You Ain't Gonna Need It.” If you suddenly start asking yourself, “What about adding more (features, code, etc.)?”, then you probably need to think about whether it's actually worth adding them.
  • Clean code instead of smart code - Simply put, leave your ego at the door and forget about writing smart code. You want clean code, not smart code.
  • Avoid Premature Optimization - The problem with premature optimization is that you never know where the bottlenecks will be in the program until they appear.
  • Single responsibility - Each class or module in a program should only care about providing one bit of a specific functionality.
  • Composition rather than implementation inheritance - Objects with complex behavior should contain instances of objects with individual behavior, rather than inheriting a class and adding new behaviors.
  • Object gymnastics are programming exercises designed as a set of 9 rules .
  • Fail fast, stop fast - This principle means stopping the current operation when any unexpected error occurs. Compliance with this principle leads to more stable operation.

Packages

  1. Prioritize structuring packages by subject area rather than by technical level.
  2. Favor layouts that promote encapsulation and information hiding to protect against misuse rather than organizing classes for technical reasons.
  3. Treat packages as if they had an immutable API - do not expose internal mechanisms (classes) intended only for internal processing.
  4. Do not expose classes that are intended to be used only within the package.

Classes

Static

  1. Do not allow creation of a static class. Always create a private constructor.
  2. Static classes should remain immutable, do not allow subclassing or multi-threaded classes.
  3. Static classes should be protected from orientation changes and should be provided as utilities such as list filtering.

Inheritance

  1. Choose composition over inheritance.
  2. Do not set protected fields. Instead, specify a secure access method.
  3. If a class variable can be marked as final , do so.
  4. If inheritance is not expected, make the class final .
  5. Mark a method as final if it is not expected that subclasses will be allowed to override it.
  6. If a constructor is not required, do not create a default constructor without implementation logic. Java will automatically provide a default constructor if one is not specified.

Interfaces

  1. Don't use the an interface of constants pattern because it allows classes to implement and pollute the API. Use a static class instead. This has the added benefit of allowing you to do more complex object initialization in a static block (such as populating a collection).
  2. Avoid overusing the interface .
  3. Having one and only one class that implements an interface will likely lead to overuse of interfaces and do more harm than good.
  4. "Program for the interface, not the implementation" does not mean that you should bundle each of your domain classes with a more or less identical interface, by doing this you are breaking YAGNI .
  5. Always keep interfaces small and specific so that clients only know about the methods that interest them. Check out the ISP from SOLID.

Finalizers

  1. The #finalize() object should be used judiciously and only as a means of protecting against failures when cleaning up resources (such as closing a file). Always provide an explicit cleanup method (such as close() ).
  2. In an inheritance hierarchy, always call the parent's finalize() in a try block . Class cleanup should be in a finally block .
  3. If an explicit cleanup method was not called and the finalizer closed the resources, log this error.
  4. If a logger is not available, use the thread's exception handler (which ends up passing a standard error that is captured in the logs).

General rules

Statements

An assertion, usually in the form of a precondition check, enforces a "fail fast, stop fast" contract. They should be used widely to identify programming errors as close to the cause as possible. Object condition:
  • • An object should never be created or put into an invalid state.
  • • In constructors and methods, always describe and enforce the contract using tests.
  • • The Java keyword assert should be avoided as it can be disabled and is usually 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 common scenarios that developers should be aware of.
  1. Whenever possible, it's better to use type inference rather than returning the base class/interface:

    // MySpecialObject o = MyObjectFactory.getMyObject();
    public  T getMyObject(int type) {
    return (T) factory.create(type);
    }

  2. If the type cannot be determined automatically, inline it.

    public class MySpecialObject extends MyObject {
     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());
      super(Collections.emptyList());
     }
    }

  3. Wildcards:

    Use an extended wildcard when you're only getting values ​​from a structure, use a super wildcard when you're putting only values ​​into a structure, and don't use a wildcard when you're doing both.

    1. Everyone loves PECS ! ( Producer-extends, Consumer-super )
    2. Use Foo for producer T.
    3. Use Foo for consumer T.

Singletons

A singleton should never be written in the classic design pattern style , which is fine in C++ but not appropriate in Java. Even though it is properly thread-safe, never implement the following (it would be 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;
  }
}
If lazy initialization is truly desired, then a combination of these two approaches will work.
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;
  }
}
Spring: By default, a bean is registered with singleton scope, which means that only one instance will be created by the container and connected to all consumers. This provides the same semantics as a regular singleton, without any performance or binding limitations.

Exceptions

  1. Use checked exceptions for correctable conditions and runtime exceptions for programming errors. Example: getting an integer from a string.

    Bad: NumberFormatException extends RuntimeException, so it is intended to indicate programming errors.

  2. Don't 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
    }

    Correct Use:

    // 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
    }
  3. You have to handle exceptions in the right place, in the right place at the domain level.

    WRONG WAY - The data object layer does not know what to do when a database exception occurs.

    class UserDAO{
        public List getUsers(){
            try{
                ps = conn.prepareStatement("SELECT * from users");
                rs = ps.executeQuery();
                //return result
            }catch(Exception e){
                log.error("exception")
                return null
            }finally{
                //release resources
            }
        }}
    

    RECOMMENDED WAY - The data layer should simply rethrow the exception and pass the responsibility for handling the exception or not to the correct layer.

    === RECOMMENDED WAY ===
    Data layer should just retrow the exception and transfer the responsability to handle the exception or not to the right layer.
    class UserDAO{
       public List getUsers(){
          try{
             ps = conn.prepareStatement("SELECT * from users");
             rs = ps.executeQuery();
             //return result
          }catch(Exception e){
           throw new DataLayerException(e);
          }finally{
             //release resources
          }
      }
    }

  4. Exceptions generally should NOT be logged at the time they are issued, but rather at the time they are actually processed. Logging exceptions, when they are thrown or re-thrown, tend to fill the log files with noise. Also note that the exception stack trace still records where the exception was thrown.

  5. Support the use of standard exceptions.

  6. Use exceptions rather than return codes.

Equals and HashCode

There are a number of issues to consider when writing proper object and hash code equivalence methods. To make it easier to use, 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());
 }
}

Resource management

Ways to safely release resources: 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 that implement java.io.Closeable , can be used as a resource.
private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path)))
 try {
   // business logic
 }
}

Use Shutdown Hooks

Use a shutdown hook that is called when the JVM shuts down gracefully. (But it will not be able to handle sudden interruptions, such as due to a power outage) This is a recommended alternative instead of declaring a finalize() method that will only run if System.runFinalizersOnExit() is true (default 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();
   }
 }
}
Allow resources to become complete (as well as renewable) by distributing them between servers. (This will allow recovery from a sudden interruption such as a power outage.) See the example code above that uses ExpiringGeneralLock (a lock common to all systems).

Date-Time

Java 8 introduces the new Date-Time API in the java.time package. Java 8 introduces a new Date-Time API to address the following shortcomings of the old Date-Time API: non-threading, poor design, complex time zone handling, etc.

Parallelism

General rules

  1. Beware of the following libraries, which are not thread-safe. Always synchronize with objects if they are used by multiple threads.
  2. Date ( not immutable ) - Use the new Date-Time API, which is thread safe.
  3. SimpleDateFormat - Use the new Date-Time API, which is thread safe.
  4. Prefer to use java.util.concurrent.atomic classes rather than making variables volatile .
  5. The behavior of atomic classes is more obvious to the average developer, whereas volatile requires an understanding of the Java memory model.
  6. Atomic classes wrap volatile variables into a more convenient interface.
  7. Understand use cases where volatile is appropriate . (see article )
  8. Use Callable when a checked exception is required but there is no return type. Since Void cannot be instantiated, it communicates the intent and can safely return null .

Streams

  1. java.lang.Thread should be deprecated. Although this is not officially the case, in almost all cases the java.util.concurrent package provides a clearer solution to the problem.
  2. Extending java.lang.Thread is considered bad practice - implement Runnable instead and create a new thread with an instance in the constructor (composition rule over inheritance).
  3. Prefer executors and threads when parallel processing is required.
  4. It is always recommended to specify your own custom thread factory to manage the configuration of created threads ( more details here ).
  5. Use DaemonThreadFactory in Executors for non-critical threads so that the thread pool can be shut down immediately when the server shuts down ( more details here ).
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
   Thread thread = Executors.defaultThreadFactory().newThread(runnable);
   thread.setDaemon(true);
   return thread;
});
  1. Java synchronization is no longer so slow (55–110 ns). Don't avoid it by using tricks like double-checked locking .
  2. Prefer synchronization with an internal object rather than a class, since users can synchronize with your class/instance.
  3. Always sync multiple objects in the same order to avoid deadlocks.
  4. Synchronizing with a class does not inherently block access to its internal objects. Always use the same locks when accessing a resource.
  5. Remember that the synchronized keyword is not considered part of the method signature and therefore will not be inherited.
  6. Avoid excessive synchronization, this can lead to poor performance and deadlock. Use the synchronized keyword strictly for the part of the code that requires synchronization.

Collections

  1. Use Java-5 parallel collections in multi-threaded code whenever possible. They are safe and have excellent characteristics.
  2. If necessary, use CopyOnWriteArrayList instead of synchronizedList.
  3. Use Collections.unmodifiable list(...) or copy the collection when receiving it as a parameter to new ArrayList(list) . Avoid modifying local collections from outside your class.
  4. Always return a copy of your collection, avoiding modifying your list externally with new ArrayList (list) .
  5. Each collection must be wrapped in a separate class, so now the behavior associated with the collection has a home (e.g. filtering methods, applying a rule to each element).

Miscellaneous

  1. Choose lambdas over anonymous classes.
  2. Choose method references rather than lambdas.
  3. Use enums instead of int constants.
  4. Avoid using float and double if precise answers are required, instead use BigDecimal such as Money.
  5. Choose primitive types rather than boxed primitives.
  6. You should avoid using magic numbers in your code. Use constants.
  7. Don't return Null. Communicate with your method client using `Optional`. Same for collections - return empty arrays or collections, not nulls.
  8. Avoid creating unnecessary objects, reuse objects, and avoid unnecessary GC cleanup.

Lazy initialization

Lazy initialization is a performance optimization. It is used when data is considered “expensive” for some reason. In Java 8 we have to use the functional provider interface for this.
== Thread safe Lazy initialization ===
public final class Lazy {
   private volatile T value;
   public T getOrCompute(Supplier supplier) {
       final T result = value; // Just one volatile read
       return result == null ? maybeCompute(supplier) : result;
   }
   private synchronized T maybeCompute(Supplier supplier) {
       if (value == null) {
           value = supplier.get();
       }
       return value;
   }
}
Lazy lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
That's all for now, I hope this was helpful!
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION