1. Choosing the right collections
2. Always using interface type when declaring a collection
3. Use generic type and diamond operator
4. Specify initial capacity of a collection if possible
5. Prefer isEmpty() over size()
6. Do not return null in a method that returns a collection
7. Do not use the classic for loop
8. Favor using forEach() with Lambda expressions
9. Overriding equals() and hashCode() properly
10. Implementing the Comparable interface properly
11. Using Arrays and Collections utility classes
12. Using the Stream API on collections
13. Prefer concurrent collections over synchronized wrappers
14. Using third-party collections libraries
15. Eliminate unchecked warnings
18. Using bounded wildcards to increase API flexibility
Now, let’s start with the first one.- Does it allow duplicate elements?
- Does it accept null?
- Does it allow accessing elements by index?
- Does it offer fast adding and fast removing elements?
- Does it support concurrency?
- Etc
List<String> listNames = new ArrayList<String>(); // (1)instead of:
ArrayList<String> listNames = new ArrayList<String>(); // (2)What’s the difference between (1) and (2)?In (1), the type of the variable listNames is List, and in (2) listNames has type of ArrayList. 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> listNames = new LinkedList<String>();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.The flexibility of using interface type for a collection is more visible in case of method’s parameters. Consider the following method:
public void foo(Set<Integer> numbers) { }Here, by declaring the parameter numbers as of type Set, the client code can pass any implementations of Set such as HashSet or TreeSet:
foo(treeSet); foo(hashSet);This makes your code more flexible and more abstract.In contrast, if you declare the parameter numbers as of type HashSet, the method cannot accept anything except HashSet (and its subtypes), which makes the code less flexible.It’s also recommended to use interface as return type of a method that returns a collection, for example:
public Collection listStudents() { List<Student> listStudents = new ArrayList<Student>(); // add students to the list return listStudents; }This definitely increases the flexibility of the code, as you can change the real implementation inside the method without affecting its client code.So this 2nd best practice encourages you to favor abstract types over concrete types.
List<Student> listStudents = new ArrayList<Student>();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<Student> listStudents = 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, Student>> map = new HashMap<Integer, Map<String, Student>>();You see, without the diamond operator, you have to repeat the same declaration twice, which make the code un-necessarily verbose. So the diamond operator saves you:
Map<Integer, Map<String, Student>> map = new HashMap<>();
List<String> listNames = new ArrayList<String>(5000);This creates an array list that can hold 5000 elements initially. If you don’t specify this number, than the array list itself will have to grow its internal array each time the current capacity is exceeded, which is inefficient. Therefore, consult Javadocs of each collection to know its default initial capacity so you can know whether you should explicitly specify the initial capacity or not.
if (listStudents.size() > 0) { // dos something if the list is not empty }Instead, you should use the isEmpty() method:
if (!listStudents.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.
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 are 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<>;Therefore, always check the logic of the code to avoid returning null instead of an empty collection.If you want to learn more in-depth, I recommend you to read this good Java collections book.
for (int i = 0; i < listStudents.size(); i++) { Student aStudent = listStudents.get(i); // do something with aStudent }However, this is considered as bad practice because using the counter variable i 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 nextStudent }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.
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.
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.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); int sum = numbers.stream().reduce(0, (x, y) -> x + y); System.out.println("sum = " + sum);The key point here is, always take advantages of the Stream API on collections to write code that performs aggregate functions quickly and easily.Also check my Java Stream Tutorial for more useful examples using the Stream API.
- HashMap -> ConcurrentHashMap
- ArrayList -> CopyOnWriteArrayList
- TreeMap -> ConcurrentSkipListMap
- PriorityQueue -> PriorityBlockingQueue
See the article Java Collections and Thread Safety to understand in-depth about collections and thread safety.- Fastutil: This library is a great choice for collections of primitive types like int or long. It’s also able to handle big collections with more than 2.1 billion elements (2^31) very well.
- Guava: This is Google core libraries for Java 6+. It contains a magnitude of convenient methods for creating collections, like fluent builders, as well as advanced collection types like HashBiMap, ArrayListMultimap, etc.
- Eclipse Collections: this library includes almost any collection you might need: primitive type collections, multimaps, bidirectional maps and so on.
- JCTools: this library provides Java concurrency tools for the JVM. It offers some concurrent data structures currently missing from the JDK such as advanced concurrent queues.
Having said that, don’t lock yourself to Java Collections Framework provided by the JDK, and always take advantages of the third-party collections libraries.Again, I recommend you to read this Java collections book to learn in depth.List list1 = new ArrayList(); List<String> list2 = new ArrayList<>(list1);The compiler issues the following warning although the code is still compiled:
Note: ClassNam.java uses unchecked or unsafe operationsUnchecked warnings are important, so don’t ignore them. It’s because every unchecked warning represent a potential ClassCastException at runtime. In the above code, if list1 contains an Integer element rather than String, than the code that uses list2 will throw ClassCastException at runtime.Let do your best to eliminate these warnings. The above code can be corrected like this:
List<String> list1 = new ArrayList<>(); List<String> list2 = new ArrayList<>(list1);However, not every warning can be easily eliminated like this. In cases you cannot eliminate unchecked warnings, let prove that the code is typesafe and then suppress the warning with an @SuppressWarnings(“unchecked”) annotation in the narrowest possible scope. Also write comments explaining why you suppress the warning.
public double sum(Collection<Number> col) { double sum = 0; for (Number num : col) { sum += num.doubleValue(); } return sum; }A limitation of this method is that it can accept only List<Number>, Set<Number> but not List<Integer>, List<Long> or Set<Double>. So to maximize the flexibility, update the method to use the bounded wildcard as shown below:
public double sum(Collection<? extends Number> col)Now, this method can accept a collection of any types which are subtypes of Number like Integer, Double, Long, etc.And that's 18 best practices about Java collections and generics. I hope you found this article helpful. And I'd love to recommend you to take this Java course for more hands-on practices.