Utility Classes of the JDK: Collections and Arrays
The JDK is evolving with every new release, adding more and improving existing features. Beneath the surface are hidden gems that make our lives as developers much easier.
Missing pieces of functionality are often complemented by third-party libraries, like the different Apache Commons libraries, or Google Guava. But over time, some of their functionality and ideas become incorporated into the JDK.
The first article highlights the utility classes with their static methods for dealing with collections and arrays.
Table of Contents
At least Java 8 is assumed.
Some methods might not be available in earlier versions. All listed methods omit thestatic
keyword and generic type definitions to reduce visual clutter.
Working with Collections
The utility class java.util.Collections
, introduced with Java 2, and improved with generics thanks to Java 5, provides a lot of static methods to operate on or return collections.
Immutable empty collections
When we need to return an empty collection or a collection-related type, we could create a new empty instance. If our code design allows this instance to be immutable, we can use Collections
instead for an empty, static
instance:
Singletons
Instead of an empty collection, we can also return an immutable collection with exactly one element:
Immutability
Creating an unmodifiable view backed by an existing collection. This way we can make a pre-existing collection immutable:
Sorting and (re)ordering
Sorting a given collection by either natural order, or uses the provided Comparator
:
Non-sort-related (re-)ordering is also supported, like reversing the order, rotating or moving all elements around, and randomly shuffling the content:
Binary search
Instead of iterating over collections, the index of a specific element can be located using the binary search algorithm:
int binarySearch(List<? extends Comparable<? super T>> list, T key)
int binarySearch(List<? extends T> list, T key, Comparator<? super T> c)
Thread-safety
Creating a synchronized (thread-safe) view of an existing collection:
To remain thread-safe, all access to the backing collection must be by the returned synchronized collection.
Another restriction is the need for manual synchronization if the collection is traversed by an Iterator
, Spliterator
, or Stream
.
The creation of the traversal-object must be inside a synchronized
block:
Type-safe non-generic collections
Before the introduction of generics, collection types had to deal with the lack of type-safety and everything was an Object
. If we have legacy code with such a non-generic collection, we can create a type-safe view of that collection:
Collection<E> checkedCollection(Collection<E> c, Class<E> type)
Map<K,V> checkedMap(Map<K,V> m, Class<K> keyType, Class<V> valueType)
SortedSet<E> checkedSortedSet(SortedSet<E> s, Class<E> type)
SortedMap<K,V> checkedSortedMap(SortedMap<K,V> m, Class<K> keyType, Class<V> valueType)
NavigableSet<E> checkedNavigableSet(NavigableSet<E> s, Class<E> type)
NavigableMap<K,V> checkedNavigableMap(NavigableMap<K,V> m, Class<K> keyType, Class<V> valueType)
Here’s a quick example of how to use make a List
` type-safe:
Working with Arrays
The utility class java.util.Arrays
is kind of the array-based counterpart to java.util.Collections
, providing static
methods for operating on, sorting, copying, searching arrays, etc.
Due to value-types, and the generality of Object
, most methods are available for value-types, specifically Object
, and generics.
To not list every single method, the placeholder type[]
will be used as a stand-in for the different types.
The type boolean
is a special case and won’t support a method unless it’s mentioned.
List creation
The easiest way to create a list from an arbitrary amount of elements:
Thanks to the varargs
, we can either provide multiple elements or an array:
Be aware that the List
instance will be immutable.
To create a mutable view again, we can wrap it in another ArrayList
:
Sorting
Just like collections before, arrays can be sorted. In the case of value types, this means in numerical order, for Object
and <T>
, in the natural order.
Sorting only a specific range of an array is also supported:
If natural order doesn’t suffice, a custom [Comparator](https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html)
can be provided:
Streams
I have written multiple times about how much I love Streams.
With the help of java.util.Arrays
we can easily create Stream<T>
and value-type Streams, at least for the supported types:
Spliterator
Java 8 introduced the java.util.Spliterator
interface, a base utility for Streams. It’s used for traversing and partitioning sequences.
Spliterators are also subclassed according to their value-type, and as before, only three numerical types are supported:
Spliterator<T> spliterator(T[] arr)
(T[] array)Spliterator<T> spliterator(T[] arr, int startInclusive, int endExclusive)
String representations
For better debugging or logging output, we can create a string representation of arrays. In addition to the listed types above, boolean
is supported. Object[]
might contain other arrays, so a method for a deep string representation is also available:
We should never use the string representation generated by a toString()
method for anything other than debugging or simple display purposes. The actual implementation might change over time, or different JDKs might handle it not quite exactly the same.
Equality
Like string representation, boolean
is supported, and we can check Object[]
for deep equality:
Hashcodes
Calculating a hashcode also supports boolean
and deep calculation:
Binary search
Finds the index a specific element in an array using the binary search algorithm:
Copying
We can copy any array to a specific length, either truncating it, or padding the new space with the appropriate uninitialized value, or null
. The value type boolean
is supported:
Filling and setting
Fill an array, or specified range, with a provided value. The value type boolean
is supported:
If we need more control over the value, a generator function can be provided:
Parallel prefixing
The parallelPrefixing
methods cumulate each element of an array in parallel. For larger arrays, this kind of operation is usually more efficient than sequential processing:
void parallelPrefix(type[] arr, int fromIndex, int toIndex, V op)
void parallelPrefix(T[] arr, int fromIndex, int toIndex, BinaryOperator<T> op)
If you haven’t used this method before, it can be unclear what exactly it does, at least that has been my experience Here’s an example to explain it:
Conclusion
Many common tasks that we might either code ourselves, or use a third-party-library, can be safely done directly with the JDK. This way we depend on fewer externalities, don’t introduce bugs by writing additional code, and handle common tasks in a concise, consistent, and reproducible way.
The next part will present utility classes for handling I/O.