Java Collection Framework Best Practices


In this article, we will discuss the Java collection framework best practices. As we widely use collections framework in a day to day project work so I thought I should create a productive post on it. We can follow these best practices in the day to day project work and this post belongs to Java Best Practices Series category.

I found below collections best practices very useful and we will discuss same in this article. Here are a list of Java collections best practices:
  1. Choosing the right collection/map
  2. Code for Interface, not for Implementation
  3. Use generic type and diamond operator
  4. Prefer isEmpty() over a size()
  5. Return empty collections or arrays, not nulls
  6. Do not use the classic for loop
  7. Favor using forEach() with Lambda expressions
  8. Overriding equals() and hashCode() properly
  9. Implementing the Comparable interface properly
  10. Using Arrays and Collections utility classes
  11. Prefer concurrent collections over synchronized wrappers
  12. Use EnumSet instead of bit fields
  13. Use EnumMap instead of ordinal indexing
  14. Eliminate unchecked warnings

1. Choosing the right collection/map

Refer to below diagram for choosing the right collection.

You may refer these questions before choosing the right collection/map. 
  • Do I need the ordering to remain? 
  • Will I have null key/values? Dups?
  • Will it be accessed by multiple threads 
  • Do I need a key/value pair 
  • Will I need random access? 
  • Does it allow accessing elements by index? 
  • Does it offer fast adding and fast removing elements?

2. Code for Interface, not for Implementation

1. Always use interface type as a reference type.

For example, use List, Set, Map etc interfaces as a reference type.
// Better
List<String> list = new ArrayList<>(); 

// Avoid
ArrayList<String> list = new ArrayList<>();

// Better
Set<String> set = new HashSet<>();

//Avoid
HashSet<String> employees = new HashSet<>();

// Better
Map<String,String> map = new HashMap<>();

//Avoid
HashMap<String,String> map = new HashMap<>();
By declaring a collection using an interface type, the code would be more flexible as you can change the concrete implementation easily when needed, for example:
List<String> list = new LinkedList<>(); 
When your code is designed to depend on the List interface, then you can swap among List’s implementations with ease, without modifying the code that uses it.

2. Always use interface type as a return type

For example,
public Collection listEmployees() {
    List<Employee> employees = new ArrayList<>();
    // add Employees to the list
    return employees;
}

3. Always use Interface Types as a method argument

public void foo(Set<Integer> numbers) {
}
The flexibility of using interface type for a collection is more visible in case of method’s parameters.

3. Use generic type and diamond operator

You can declare a collection of a generic type like this:
List<Employee> listEmployees = new ArrayList<Employee>();
Since Java 7, the compiler can infer the generic type on the right side from the generic type declared on the left side, so you can write:
List<Employee> listEmployees = new ArrayList<>();
The <> is informally called the diamond operator. This operator is quite useful. Imagine if you have to declare a collection like this:
Map<Integer, Map<String, Employee>> map = new HashMap<Integer, Map<String, Employee>>();
You see, without the diamond operator, you have to repeat the same declaration twice, which make the code unnecessarily verbose. So the diamond operator saves you:
Map<Integer, Map<String, Employee>> map = new HashMap<>();

4. Prefer isEmpty() over a size()

Avoid checking the emptiness of a collection like this:
if (listOfEmployees.size() > 0) {
    // dos something if the list is not empty  
}
Instead, you should use the isEmpty() method:
if (!listOfEmployees.isEmpty()) {
    // dos something if the list is not empty
}
There’s no performance difference between isEmpty() and size(). The reason is for the readability of the code.

5. Return empty collections or arrays, not nulls

Some APIs intentionally return a null reference to indicate that instances are unavailable. This practice can lead to denial-of-service vulnerabilities when the client code fails to explicitly handle the null return value case.

If a method is designed to return a collection, it should not return null in case there’s no element in the collection. Consider the following method:
public List<Student> findStudents(String className) {
    List<Student> listStudents = null;
 
    if (//students are found//) {
        // add students to the lsit
    }
 
    return listStudents;
}
Here, the method returns null if no student is found. The key point here is, a null value should not be used to indicate no result. The best practice is, returning an empty collection to indicate no result. The above code can be easily corrected by initializing the collection:
List<Student> listStudents = new ArrayList<>;
Or
Collections.empty();
In summary, never return null in place of an empty array or collection. It makes your API more difficult to use and more prone to error, and it has no performance advantages.

6. Do not use the classic for loop

There’s nothing wrong if you write code to iterate a list collection like this:
for (int i = 0; i < listStudents.size(); i++) {
    Student aStudent = listStudents.get(i);
 
    // do something with aStudent
}
However, this is considered as the bad practice because using the counter variable may lead to potential bugs if it is altered somewhere inside the loop. Also, this kind of loop is not object-oriented since every collection has its own iterator. So it’s recommended to use an iterator like the following code:
Iterator<Student> iterator = listStudents.iterator();
 
while (iterator.hasNext()) {
    Student nextStudent = iterator.next();
 
    // do something with next student
}
Also, the iterator may throw ConcurrentModificationException if the collection is modified by another thread after the iterator is created, which eliminates potential bugs.

Now, it’s better to use the enhanced for loop like this:
for (Student aStudent : listStudents) {
    // do something with aStudent
}
As you can see, the enhanced for loop is more succinct and readable though it uses an iterator behind the scenes.

7. Favor using forEach() with Lambda expressions

Since Java 8, every collection now provides the forEach() method that encapsulates the iteration code inside the collection itself (internal iteration), and you just pass a Lambda expression to this method. This makes the iteration code even more compact, more flexible and more powerful. Here’s an example:
List<String> fruits = Arrays.asList("Banana", "Lemon", "Orange", "Apple");
 
fruits.forEach(fruit -> System.out.println(fruit));
This is equivalent to the following enhanced for loop:
for (String fruit : fruits) {
    System.out.println(fruit);
}
So I encourage you to use the forEach() method for iterating a collection in a way that helps you focus on your code, not on the iteration.

8. Overriding equals() and hashCode() properly

You must override hashCode() in every class that overrides equals(). Failure to do so will result in a violation of the general contract for Object.hashCode(), which will prevent your class from functioning properly in conjunction with all hash-based collections, including HashMap, HashSet, and Hashtable.

9. Implementing the Comparable interface properly

Remember to have your custom types implemented the Comparable interface properly when their elements are added to collections that sort elements by natural ordering, such as TreeSet and TreeMap. It also helps to sort elements in a list collection based on the natural ordering of the elements.

10. Using Arrays and Collections utility classes

Be aware that the Java Collections Framework provides two utility classes named Arrays and Collections which give us many useful functionalities. For example, the Arrays.asList() method returns a list collection containing the given elements, as you can see I used this method in many examples:
List<String> listFruits = Arrays.asList("Apple", "Banana", "Orange");
List<Integer> listIntegers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Double> listDoubles = Arrays.asList(0.1, 1.2, 2.3, 3.4);
And the Collections class provides various useful methods for searching, sorting, modifying elements in a collection (almost on lists). Therefore, remember to look at these two utility classes for reusable methods, before looking for other libraries or writing your own code.

You should check out these productive posts:

11. Prefer concurrent collections over synchronized wrappers

When you have to use collections in multi-threaded applications, consider using concurrent collections in the java.util.concurrent package instead of using the synchronized collections generated by the Collections.synchronizedXXX() methods.

It’s because the concurrent collections are designed to provide maximum performance in concurrent applications, by implementing different synchronization mechanisms like copy-on-write, compare-and-swap, and special locks. The following list shows you how to choose some concurrent collections (on the right) which are equivalent to the normal ones (on the left):
- HashMap -> ConcurrentHashMap

- ArrayList -> CopyOnWriteArrayList

- TreeMap -> ConcurrentSkipListMap

- PriorityQueue -> PriorityBlockingQueue

12. Use EnumSet instead of bit fields

What is the problem using bit fields?

If the elements of an enumerated type are used primarily in sets, it is traditional to use the int enum pattern, assigning a different power of 2 to each constant. For example:

// Bit field enumeration constants - OBSOLETE!
public static class Text {
    public static final int STYLE_BOLD              = 1 << 0; // 1
    public static final int STYLE_ITALIC            = 1 << 1; // 2
    public static final int STYLE_UNDERLINE         = 1 << 2; // 4
    public static final int STYLE_STRIKE_THROUGH    = 1 << 3; // 8

    public Text() {

    }

    // parameter is bitwise OR of zero or more STYLE_ constants
    public void applyStyles(int styles) {
     // hard to interpret a bit field
     // no easy way to iterate over all of the elements represented by a bit field
    }
}
This representation lets us use the bitwise OR operation to combine several constants into a set, known as a bit field:
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

What is the solution?

We can use EnumSet class to solve this problem. The EnumSet class is used to efficiently represent sets of values drawn from a single enum type. This class implements the Set interface. Each EnumSet is represented as a bit vector. Internally, the EnumSet is represented as a bit vector. If the enum type has 64 or fewer elements, the entire EnumSet is represented with a single long, so its performance is comparable to a bit field
import java.util.EnumSet;

import java.util.Set;

public class ExampleEnumSet {
    /** EnumSet - a modern replacement for bit fields. */
     public static class TextEnumSet {
        public enum Style {
            BOLD, ITALIC, UNDERLINE, STRIKETHROUGH;
        }

        /** Any set can be passed but EnumSet is best. */
        public void applyStyles(Set<Style> styles) {
        }
    }

    public static void main(String[] args) {
        TextEnumSet t2 = new TextEnumSet();
        t2.applyStyles(EnumSet.of(TextEnumSet.Style.BOLD, TextEnumSet.Style.ITALIC));
    }
}
Read more about EnumSet Class APIs with examples - EnumSet Class in Java

13. Use EnumMap instead of ordinal indexing

EnumMap is a specialized Map implementation for use with enum type keys. All of the keys in an enum map must come from a single enum type that is specified, explicitly or implicitly, when the map is created. Enum maps are represented internally as arrays. This representation is extremely compact and efficient.
It is rarely appropriate to use ordinals to index into arrays: use EnumMap instead. If the relationship you are representing is multidimensional, use EnumMap<..., EnumMap<...>>. This is a special case of the general principle that application programmers should rarely if ever, use Enum.ordinal.
Below program demonstrate the usage of EnumMap:
import java.util.EnumMap;
import java.util.Map.Entry;

/**
 * EnumMap Demonstration Example
 * @author Ramesh Fadatare
 *
 */
public class EnumMapExample {
     enum Days {
         SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;
     }

     public static void main(final String[] args) {
         final EnumMap<Days, String> enumMap = new EnumMap<>(Days.class);
         enumMap.put(Days.SUNDAY, "Its Sunday!!");
         enumMap.put(Days.MONDAY, "Its Monday!!");
         enumMap.put(Days.TUESDAY, "Its Tuesday!!");
         enumMap.put(Days.WEDNESDAY, "Its Wednesday!!");
         enumMap.put(Days.THURSDAY, "Its Thursday!!");
         enumMap.put(Days.FRIDAY, "Its Friday!!");
         enumMap.put(Days.SATURDAY, "Its Saturday!!");
  
         for(final Entry<Days, String> entry : enumMap.entrySet()){
             System.out.println(" Key -> " + entry.getKey().SUNDAY);
             System.out.println("Value - >" + entry.getValue());
         }
     }
}
Output:
 Key -> SUNDAY
Value - >Its Sunday!!
 Key -> SUNDAY
Value - >Its Monday!!
 Key -> SUNDAY
Value - >Its Tuesday!!
 Key -> SUNDAY
Value - >Its Wednesday!!
 Key -> SUNDAY
Value - >Its Thursday!!
 Key -> SUNDAY
Value - >Its Friday!!
 Key -> SUNDAY
Value - >Its Saturday!!
Read more about EnumMap on EnumMap Class in Java

14. Eliminate unchecked warnings

When you program with generics, you'll see a lot of compiler warnings:
  • unchecked cast warnings
  • unchecked method invocation warnings
  • unchecked generic array creation warnings
  • unchecked conversion warnings
Eliminate every unchecked warning that you can. If you eliminate all warnings, you are assured that the code is TYPESAFE. Means you won't get ClassCastException at runtime.

SupressWarnings

If you can't eliminate a warning and you can prove that the code that provoked the warning is typesafe, then (and only then) suppress the warning with an @SupressWarnings("unchecked") annotation
Highly recommended to use SupressWarnings annotation on smallest scope possible.
Every time you use a @SuppressWarnings("unchecked") annotation, add a comment saying why it is safe to do so. This will help others understand the code, and more importantly, it will decrease the odds that someone will modify the code so as to make the computation unsafe.
Let's look at few @SuppressWarnings() annotation examples.

SupressWarnings Examples

Suppressing warnings on using unchecked generic types operations:
@SuppressWarnings("unchecked")
void uncheckedGenerics() {
    List words = new ArrayList();
 
    words.add("hello"); // this causes unchecked warning
}
Unchecked warnings are important. Don’t ignore them. Every unchecked warning represents the potential for a ClassCastException at runtime. Do your best to eliminate these warnings. If you can’t eliminate an unchecked warning and you can prove that the code that provoked it is typesafe, suppress the warning with a @SuppressWarnings("unchecked") annotation in the narrowest possible scope.

Related Best Practices Posts

Complete Collection Framework Developer Guide: Java Collections Guide

Comments

  1. This post is very informative and useful of Java Collection Framework best practices.

    ReplyDelete
    Replies
    1. Thanks, You should read this article http://www.javaguides.net/2018/06/guide-to-string-best-practices-in-java.html

      Delete
  2. Is really good and nice. I have questions about 11 position. We should always use concurentHashMap ? I'm not sure, whether in this case we must be aware that some data we can lost. Only synchronized gives us 100% data consistency. What you think about it ?

    ReplyDelete

Post a Comment