Tuesday, 13 April 2021

Java Collections Framework Best Practices

       In this article, we will discuss the best practices for using the Java Collections Framework, along with practical examples for each practice.

1) Choose the right collection

        This is one of the most important considerations before using any collection. The choice of collection should depend on the functionality and the problem you are trying to solve. Choosing the right collection can improve both the readability and performance of your application.

When selecting a collection, consider the following criteria:

  1. Does the collection allow duplicate elements?

  2. Does it allow null values?

  3. Does it support index-based access?

  4. Is it thread-safe, or does it support concurrent access?

  5. What are the performance characteristics of operations such as insertion, deletion, searching, and iteration?

A developer should understand the features and performance implications of each collection type before deciding which one to use.


2) Use interface type when declaring any collection.

When declaring a collection, prefer using the interface type instead of a concrete implementation class.

Example:

List<String> listOfItems = new ArrayList<>();

Instead of:

ArrayList<String> listOfItems = new ArrayList<>();

Using the interface type makes your code more flexible and maintainable because you can easily change the underlying implementation without modifying the rest of the code.

For example, if your requirements change, you can simply replace the implementation with:

List<String> listOfItems = new LinkedList<>();

This follows the programming principle of "Program to an interface, not an implementation," which promotes loose coupling and makes the code easier to maintain and extend.


3) Method return type should be interface instead of collection class.

       As a best practice, the return type of a method should be an interface rather than a concrete collection class. This makes the code more flexible because changes to the underlying collection implementation inside the method do not affect the calling code.

For example, if a method initially returns an ArrayList but later needs to return a LinkedList, the method signature can remain unchanged if the return type is declared as List.

 public List<String> getAllItems() {
      
      List<String> listOfItems = new ArrayList<>();
      
      //in future if we change arrayList to linkedList it doesn't impact on caller
      // get all items - fetch items from database

      return listOfItems;
 }


4) Use generic type and diamond operator

 Always use generic types when declaring a collection. Otherwise, there is a risk of encountering a ClassCastException at runtime because the collection can hold objects of different types.

Example:

List listOfItems = new ArrayList<>();

listOfItems.add("item");

listOfItems.add(1);

so avoid using above declaration, apply generics,

List<String> listOfItems = new ArrayList<>();

listOfItems.add("company");

listOfItems.add(12);     //compile time error

 Use diamond operator(<>),

This operator <> is called the diamond operator. Without diamond operator we have to write declaration twice so using this operator no need to write declaration twice as follows,

List<String> listOfItems = new ArrayList<String>();

With <> operator,

List<String> listOfItems = new ArrayList<>();


5) Prefer to use Collection isEmpty() or CollectionUtils.isEmpty()  methods instead of size() method

Checking the emptiness of collection, avoid using size method like,

if  (listOfItems.size > 0)  {
   //write logic if list is not empty
}

Prefer to use collection or collection util methods,

if  (!listOfItems.isEmpty())  {
    //write logic if list is not empty
}

or use collection util(apache) method,

if (CollectionUtils.isNotEmpty(listOfItems)) {
      //write logic if list is not empty
}


6) Specify initial capacity of a collection if possible

       When processing a large batch of records, it is a good practice to specify the initial capacity of the collection based on the expected batch size. This minimizes the need for the collection to resize its internal storage as elements are added, which can improve performance.
List<String> listOfItems = new ArrayList<>(500);

The above statement creates an ArrayList with an initial capacity of 500 elements. This helps reduce the overhead of repeated resizing when you know the approximate number of elements in advance.


7)  Return an empty array or collection instead of a null value for methods that return an array or collection

      If a method returns a collection, always return an empty collection instead of null. This eliminates the need for the caller to perform null checks, resulting in cleaner, safer, and more readable code.

Returning an empty collection also reduces the risk of NullPointerException and simplifies client-side code.

//Avoid this
public List<String> getAllPreSaleItems(String itemType) {
		
	List<String> preSaleItems = null;
	if (itemType.equalsIgnoreCase("preSale")) {
		//preSaleItems = fetch all presale items from database
	}
	return preSaleItems;
}

Best practice to return empty list as, 

//best practice to return empty list
private List<String> getAllPreSaleItems(String itemType) {
		
	List<String> preSaleItems = null;
	if (itemType.equalsIgnoreCase("preSale")) {
		//preSaleItems = fetch all presale items from database
	}
	if (preSaleItems == null) {
		return Collections.EMPTY_LIST;
	}
	return preSaleItems;
}

8) Do not use the classic for loop

Classic for loop,

List<String> listOfItems = Arrays.asList("a", "b", "c");
for (int i=0; i<listOfItems.size(); i++) {
   System.out.println(listOfItems(i));
}

Better to use for-each loop,

List<String> listOfItems = Arrays.asList("a", "b", "c");
for (String item : listOfItems) {
   System.out.println(item);
}

The enhanced for-each loop improves code readability and reduces the likelihood of bugs. It iterates through each element in the collection one by one without requiring explicit index management or an iterator.


9) Prefer to use forEach() with Lambda expressions(Java 8 onwards)

       When using the forEach() method, prefer Java 8 lambda expressions. Lambda expressions make the code more concise, readable, and expressive by eliminating unnecessary boilerplate code.

Example:

List<String> skuList = Arrays.asList("MTCCC", "MTAAA", "PT1111");
 
skuList.forEach(sku -> System.out.println(sku));


10) Prefer concurrent collections over synchronized wrappers

         When developing multithreaded applications, prefer using the concurrent collections provided by the java.util.concurrent package instead of the synchronized collections created by the Collections.synchronizedXXX() methods.

The concurrent collections are designed to deliver better performance and scalability in multithreaded environments. They achieve this by using advanced synchronization techniques such as copy-on-write, compare-and-swap (CAS), and specialized locking mechanisms, rather than synchronizing the entire collection.

Some commonly used classes in the java.util.concurrent package include:

  • ConcurrentHashMap

  • CopyOnWriteArrayList

  • ConcurrentSkipListMap

  • PriorityBlockingQueue



Related Posts:-

No comments:

Post a Comment