Java Collections Utility methods

Introduction

The Collections class in Java has many static utility methods that operate on collections or return collections. In this post, we will learn about the Java Collections Utility methods.

The Collections utility methods

The Collections class offers utility methods that operate on collections. The methods throw NullPointerException if the collection objects passed are null. When calling the methods that modify the passed collection (like sort), it is required for the passed collection to be mutable – else it will throw an UnsupportedOperationException.

In this post, I will cover the following Java Collections Utility methods:

  • AddAll
  • Sort
  • Binary Search
  • Checked* methods
  • Disjoint
  • Enumeration and list
  • Fill and Frequency
  • nCopies
  • Min and Max
  • replaceAll
  • Reverse
  • Rotate and shuffle
  • Synchronized* methods
  • Unmodifiable* methods

Collections#addAll

We use the addAll method of the Collections class to add the specified elements into a collection. We pass the collection and the elements to be added into that collection (as a var-args). It returns a boolean to indicate if the collection was modified as a result of the addAll method call.

List<String> list = new ArrayList<>();
list.add("Java");
list.add("Streams");
boolean wasModified = Collections.addAll(list, "Completable Future", "Threads");
System.out.println(wasModified); //true
System.out.println(list); //[Java, Streams, Completable Future, Threads]

We created a list with two strings in it and called Collections#addAll by passing the list and two strings to be added. It returned true, indicating that the collection (list) was modified and the resultant list has four strings (shown above).

The behaviour of this method is similar to calling the addAll method on the collection and passing the elements to be added as a list. But this method is expected to be faster for most implementations.

Let us use Collections#addAll on a set.

Set<String> set = new HashSet<>(Set.of("Java", "Streams")); //see
System.out.println(Collections.addAll(set, "Threads")); //true
System.out.println(set); //[Threads, Java, Streams]

System.out.println(Collections.addAll(set, "Java")); //false
System.out.println(set);//[Threads, Java, Streams]

Here, we call addAll passing the set and a new string. Hence, the set was modified and the call to addAll returned true. Next, we attempt to add a string that was already there in the set. This would result in the set unmodified and hence it returned false.

Collections#sort

The Collections#sortmethod sorts the passed list. Two scenarios are there:

  1. If the elements in the collection are comparable (have a natural ordering), i.e., they implement the Comparable interface, then they are sorted as per that.
  2. If the elements do not have a natural ordering, then we have to pass a Comparator.

The sort will be stable i.e., equal elements will not be reordered and the list must be modifiable.

List<String> list = new ArrayList<>(List.of("Java", "Streams", "Completable Future", 
                "Threads"));
Collections.sort(list);
System.out.println(list); //[Completable Future, Java, Streams, Threads]

Let us create a custom class and create a list of custom class objects. Shown below is the Student class with two fields, getters, equals, hashCode and toString methods implemented.

public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj.getClass() != this.getClass()) {
            return false;
        }
        Student other = (Student) obj;
        return Objects.equals(other.name, this.name);
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    @Override
    public String toString() {
        return name;
    }
}

Note that the Student does not implement Comparable interface. Hence we cannot call the one argument sort method we used earlier. If we did, it would result in a compilation error as the element type T of the list must extend Comparable.

public static <T extends Comparable<? super T>> void sort(List<T> list)

We have to pass an explicit Comparator to sort the Students list. Let us pass a Comparator to sort the student by their name by using Comparator.comparing.

List<Student> students = new ArrayList<>(List.of( //see - must be immutable
        new Student("Mary", 14),
        new Student("John", 12)
));
Collections.sort(students, Comparator.comparing(Student::getName));
System.out.println(students); //[John, Mary]

The result is that the list is ordered following the Student name’s lexicographical ordering.

Collections#binarySearch

The Collections class has a binarySearch method which uses the binary search algorithm. This allows us to search in a sorted list in O(log n) time. This requires that:

  1. The list is sorted in ascending order and
  2. The list must provide a “random” access. In other words, the list type must implement the RandomAccess interface.

Consequences/Considerations:

  1. If the list is not sorted, the results are undefined.
  2. If the list has duplicate elements, the search can pick any among them.
  3. Finally, if the list does not provide a RandomAccess (O(1) access to the element at an index), then it will do a sequential search. This defeats the purpose of a binary-search (as it will result in O(n) time).

The binarySearch method returns an integer, which means two different things depending on if it is positive or negative.

  1. If the result is positive (including zero):
    1. This means that the element we searched for was present in the list. The returned value is the index at which the element was found.
  2. If the result is negative:
    1. The element we searched did not exist in the list. It returned the value of -insertion_point-1 where insertion_point is the index at which the searched value would be inserted.

Binary Search Example

List<String> list = new ArrayList<>(List.of("Java", "Streams", "Completable Future"));

Collections.sort(list); //Sort the list
System.out.println(list); // [Completable Future, Java, Streams]

System.out.println(Collections.binarySearch(list, "Streams")); //2

//non-existing cases
System.out.println(Collections.binarySearch(list, "C")); //-1
System.out.println(Collections.binarySearch(list, "Concurrency")); //-2
System.out.println(Collections.binarySearch(list, "Threads")); //-4

In the above example, we created a list, sorted it before calling the binarySearch methods.
In the first call, we searched for an existing string value from the list and it returned the index of it. The remaining three examples demonstrate the (value) missing case. 

  • Searching for the string “C” returned -1 as it would be the first string in the list if inserted i.e., the insertion is point is 0 and hence it returned -1 (-insertion_point -1).
  • Similarly, searching for the string “Concurrency” returned -2 as the insertion point is index 1 (after the string “Completable Future”).
  • Finally, “Threads” would be the last element in the list (insertion point is 3 and hence it returned -4).

Binary Search on custom class

Let us use the binarySearch method on a list of Students. Let us create a list of Students and sort them.

List<Student> students = new ArrayList<>(List.of(
        new Student("Mary", 14),
        new Student("John", 12),
        new Student("Kelly", 12)
));

Comparator<Student> compareByName = Comparator.comparing(Student::getName);
students.sort(compareByName); //or Collections.sort(students, compareByName);
System.out.println(students); //[John, Kelly, Mary]
System.out.println(Collections.binarySearch(students, new Student("Kelly", 12), compareByName)); //1

System.out.println(Collections.binarySearch(students, new Student("Kelly", 14), compareByName)); //1
Since, the elements in the list do not implement a Comparable, we have to pass a Comparator (similar to the sort method).

We used a Comparator to compare by the student’s name. Note that passing a different age does not affect the output as the binarySearch method uses the passed comparator to compare the objects. This is true, even if the equals method of the Student class considered the age instance variable in defining the equals and the hashCode method.
System.out.println(Collections.binarySearch(students, new Student("Adam", 16), compareByName)); //-1
System.out.println(Collections.binarySearch(students, new Student("Larry", 12), compareByName));//-3
System.out.println(Collections.binarySearch(students, new Student("Wess", 12), compareByName)); //-4

The above shows the scenarios of calling the binarySearch with the list for missing values.

Collections checked methods

Before generics, it was possible to add any element type to a raw collection as we didn’t have compile time safety checks. In such case, the insertion would be successful, but we would get an exception at runtime when typecasting the element to the expected type.

The Collections class provides utility methods to get a dynamically typesafe view of the specified collection. It basically returns a collection implementation which has a check to verify that the type of element added matches the type of the collection intended. Thus, any attempt to insert an element of the wrong type will throw a ClassCastException during the insertion time itself (rather than when accessing the element).

This is very helpful when working with legacy APIs that still work on raw types. We can also use this during debugging – to drop in the collection from the checkedCollection method and find where the element of wrong type is being added.

Collection<String> list = new ArrayList<>(List.of("Java", "Streams", "Completable Future"));
modifyByAddingWrongType(list);
System.out.println(list);

private void modifyByAddingWrongType(Collection list) {
    list.add(1);
}

In the above code, we added an invalid element type (integer) into the collection, which was meant to hold only strings. This did not produce any exceptions.

We would get an exception when accessing the element from the list. Say we want to sort it by calling Collections.sort(list). This will throw a ClassCastException.
 
To catch this earlier (during insertion time itself), we can use the checkedCollection method.

Collection<String> checkedList = Collections.checkedCollection(list, String.class);
modifyByAddingWrongType(checkedList);

Now, it will fail with a ClassCastException when it attempts to add an invalid element (of wrong type) into the list (i.e., in the modifyByAddingWrongType method).

The Collection class provides similar methods for other collection types as well, like checkedListcheckedMapcheckedSet etc.,

Collections#disjoint

We can use the disjoint method to check if two collections have no element in common. In other words, it returns true only if the two specified collections have no elements in common.

It iterates through one of the collection and calls contains method on the other collection.

List<String> list1 = List.of("Java", "Streams");
List<String> list2 = List.of("Completable Future", "Threads");

System.out.println(Collections.disjoint(list1, list2)); //true

We pass two lists with no common element between them, and hence the call to Collections#disjoint returned true.

If we pass a list shown below and compare list1 and list3, it would return false because there is a common element “Java” between list1 and list3.

List<String> list3 = List.of("Completable Future", "Java", "Threads"));
System.out.println(Collections.disjoint(list1, list3)); //false

Using that on list of students,

List<Student> students1 = List.of(
        new Student("Mary", 14),
        new Student("John", 12)
);
List<Student> students2 = List.of(
        new Student("Larry", 16),
        new Student("John", 15)
);

//Student must implement equals, hashCode
System.out.println(Collections.disjoint(students1, students2)); //false

Note that the age of the Student did not matter and it considered the Student John to be common between the two lists. If the Student class used the age field when implementing the equals and the hashCode method, then the above call would have returned true (disjoint).

Enumeration and List methods

The enumeration method accepts a list and returns an Enumeration. The list method of the Collection class accepts an Enumeration and returns an ArrayList. These two methods can be used to provide the interoperability with legacy APIs working on an Enumeration.

List<String> list = List.of("Java", "Streams", "Completable Future", "Threads");
Enumeration<String> enumeration = Collections.enumeration(list);

while (enumeration.hasMoreElements()) {
    System.out.println(enumeration.nextElement());
}

The above code prints,

Java
Streams
Completable Future
Threads
List<String> list = List.of("Java", "Streams", "Completable Future", "Threads");
Enumeration<String> enumeration = Collections.enumeration(list);
ArrayList<String> arrayList = Collections.list(enumeration);
System.out.println(arrayList); //[Java, Streams, Completable Future, Threads]

Fill and Frequency

The Collections fill method replaces all the elements of the list with a specified element.

List<String> list = Arrays.asList("a", "b", "c", "d");
Collections.fill(list, "undefined");
System.out.println(list); //[undefined, undefined, undefined, undefined]

The Collections#frequency method accepts a collection and an element and returns the number of times that element is present. More formally, it returns the number of elements in the collection that are equal to the passed object.

List<String> list = List.of("Java", "Streams", "Java",  "Completable Future");
System.out.println(Collections.frequency(list, "Java")); //2
System.out.println(Collections.frequency(list, "Streams")); //1

With list of students example,

List<Student> students = List.of( //see - must be immutable
        new Student("Mary", 14),
        new Student("John", 12),
        new Student("John", 14)
);

System.out.println(Collections.frequency(students, new Student("John", 11))); //2
System.out.println(Collections.frequency(students, new Student("Mary", 11))); //1

Remember that equals and the hashCode methods are based only on the Student’s name.

Collections#nCopies

The nCopies is a useful method to create an immutable list with n copies of the specified object.

List<String> list = Collections.nCopies(5, "Java");
System.out.println(list); //[Java, Java, Java, Java, Java]

The above creates a list with the specified string (“Java”) present five times in the list.

Collections min and max

Collections#min

When the elements of the list have a natural ordering, the Collections#min returns the minimum element as per their natural ordering. If not (if the class doesn’t implement the Comparable interface), we have to pass a Comparator, and it returns the minimum element according to the ordering determined by the comparator.

List<String> list = List.of("Java", "Streams", "Java",  "Completable Future");
System.out.println(Collections.min(list)); //Completable Future

In the above example, when the elements are sorted lexicographically, the string “Completable Future” would be the first and hence the Collections#min method returned it.

List<Student> students = List.of(
        new Student("Mary", 11),
        new Student("John", 12),
        new Student("Larry", 17)
);
System.out.println(Collections.min(students, Comparator.comparing(Student::getName))); //John
System.out.println(Collections.min(students, Comparator.comparing(Student::getAge))); //Mary

With the list of students, when we pass a comparator based on name, it returned the student whose name would appear first when sorted by name (John). When we pass a comparator to order based on the Student’s age, it returned the student who would appear first when sorted by age (youngest).

Collections#max

The Collections#max returns the maximum element from the list passed. The rules/mechanism specified for Collections#min applies to this as well.

System.out.println(Collections.max(list)); //Streams

System.out.println(Collections.max(students, Comparator.comparing(Student::getName))); //Marry
System.out.println(Collections.max(students, Comparator.comparing(Student::getAge))); //Larry

Collections replaceAll and reverse

Collections replaceAll

The replaceAll replaces all occurrences of the specified old value with the new value in a list. It returns a boolean to denote if the list was modified or not.

List<String> list = new ArrayList<>(List.of("Java", "Streams", "Java", "Completable Future"));
System.out.println(Collections.replaceAll(list, "Streams", "Java Streams")); //true
System.out.println(list); //[Java, Java Streams, Java, Completable Future]

In the above code, we replaced all occurrences of “Streams” with “Java Streams”.

Collections reverse

The reverse method reverses the order of the elements in the list.

Collections.reverse(list);
System.out.println(list); //[Completable Future, Java, Java Streams, Java]

Collections rotate and shuffle

The rotate method rotates the elements in the passed list by the specified distance and the shuffle method randomly shuffles the elements in the passed list.

Collections.rotate(list, 1);
System.out.println(list); //[Completable Future, Java, Streams, Java]

Collections.rotate(list, 3);
System.out.println(list); //[Java, Streams, Java, Completable Future]
List<String> list = List.of("Java", "Streams", "Java",  "Completable Future");
Collections.shuffle(list);

Collections synchronized methods

The Collections class offers many synchronized* methods for different collections types like synchronizedCollectionsynchronizedListsynchronizedMapsynchronizedSet etc., 

The basis of all these methods is to return a synchronized version of the passed collection or the map instance. Basically, it returns a new object of the same collection type and holds the reference to the passed collection. All methods are synchronized using an internal object/lock. Thus, it guarantees that the returned collection is thread-safe.

Note that the thread-safety applies to the individual method access only. Thus, when traversing the collection, the caller has to synchronize manually.

List<String> list = List.of("Java", "Streams", "Java", "Completable Future");
List<String> synchronizedList = Collections.synchronizedList(list);

Collections unmodifiable methods

The Java Collections class has several unmodifiable* methods to return an unmodifiable view of the passed collection. It returns a wrapper class that holds the passed collection object. The write operations like add, remove throw an UnsupportedOperationException while the read operations are delegated to the underlying collection object.

Example: Let us see the unmodifiableList.

List<String> list = new ArrayList<>(List.of("Java", "Streams", "Java", "Completable Future"));
List<String> unmodifiableList = Collections.unmodifiableList(list);
System.out.println(unmodifiableList.get(0)); //Java

//throws java.lang.UnsupportedOperationException
System.out.println(unmodifiableList.remove(0));

Note that unmodifiable is not the same as immutable. If the underlying collection is modified, then the above unmodifiableList would appear to have been changed.

//modify the source collection
list.add("Atomic Integer");
System.out.println(unmodifiableList); //[Java, Streams, Java, Completable Future, Atomic Integer]

If we want to make the list immutable, then we have to create a copy of the source list when creating the unmodifiableList.

unmodifiableList = Collections.unmodifiableList(new ArrayList<>(list));

Conclusion

In this post, we learnt about the various Java Collections utility methods.

Leave a Reply