Skip to content

Guide to Java Collections

Hello! A good understanding of collections is a valuable skill for any Java developer. Java brings a number of data structures and algorithms (for example, for searching and sorting) that you can use out of the box and do not need to implement every time from zero. From the other hand, it is true, that Java Collections Framework is quite extensive and it is not something you can learn over a lunch time. It requires time and practice. But even more – it requires a roadmap.
This tutorial serves as a summary of Java Collections Framework and common data structures like Lists, Sets, Maps, Queues in Java.

Table of Contents

Introduction to the Java Collections Framework

Before we will go to concrete data structures, let have a helicopter view on Java Collections Framework. It provides for developers a unified architecture for representing and manipulating collections, enabling collections to be manipulated independently of implementation details. Java defines a collection as an object that represents a group of objects. So, Java Collections Framework (JCF) includes a number of interfaces and implementations that facilitate data operations like searching, sorting, insertion, manipulation, or deletion of elements. Below you can see the graph that represents an overall structure of JCF:

Graph 1. Java Collections Framework structure

One of the common interview questions sounds like “why do we use JCF”? Well, there is a number of reasons, why JCF is so useful and important:

  1. JCF reduces programming efforts, because you don’t need to reinvent these data structures and algorithms yourself
  2. JCF offers high-performance implementations of data structures and algorithms, therefore it increases performance
  3. JCF establishes interoperability between unrelated APIs
  4. JCF makes it easier to learn collections!

The root classes of JCF are java.util.Collection and java.util.Map. That is very important to remember, as it is very common to think that Map is a collection. While maps contain collection-view operations, which enable them to be manipulated as collections, from a technical point of view, maps are not collections in Java. Let explore now the Collection interface deeper as it is base class for a number of other data structures.

java.util.Collection in details

As it was already mentioned, Java collection is a group of objects, that are known as its elements. Note, that elements in some collections have to be unique, while other types permit duplicates. java.util.Collection is not implemented directly, rather Java has implementations of its subinterfaces. The Collection interface is typically used to pass collections around and manipulate them with max degree of generality. Let explore what operations are defined in this interface.

Inserting elements

There are two ways to add new elements to collection: add a single element and add all elements from the another collection. Note, that both of these methods are marked optional. That means, that implementations are permitted to not perform one or more of these operations (so they throw UnsupportedOperationException when such methods are called). Take a look on the code snippet below:

String anna = "Anna";

// add a single element

List<String> femaleNames = new ArrayList<String>();
femaleNames.add(anna);

// add all elements from another collection

List<String> names = new ArrayList<String>();
names.addAll(femaleNames);

In this example we use an ArrayList that is one of the Java collections and which implements both add() and addAll() methods. NB that ArrayList has two add() methods, but we concentrate here on the Collection’s one. So, java.util.Collection permits us to insert elements in these ways:

  • boolean add(Element e) = this method adds a new element and ensures collection contains the specified element. So it returns either true or false depending if this collection changed as a result of the call.
  • boolean addAll(Collection c) = this method inserts all elements from the argument to the collection. Note, that this method also returns boolean value that is true if this collection changed as a result of the call

Some collection implementations have restrictions on the elements that they may contain, and as the result they restricts certain insertions. For example, if collection does not allow duplicates, so you can’t add an element that already exists. Same goes for null values.

Removing elements

Compare to two inserting methods, there are more methods to delete elements from the collection. Take a look on them:

  • void clear() = removes all of the elements from the collection
  • boolean remove(Element e) = removes a single instance of the specified element e from the collection (if that element is presented)
  • boolean removeAll(Collection c) = deletes all of the collection’s elements that are also contained in the argument’s collection
  • boolean removeIf(Predicate filter) = deletes all of the elements of this collection that satisfy the given predicate.

Let have a look on the example below:

// delete a single element

String name = "Maria";
boolean result = femaleNames.remove(name);
System.out.println(result); // false

// delete several elements

boolean result = names.removeAll(femaleNames);
System.out.println(result); // true

// remove using predicate

boolean result = femaleNames.removeIf(name->name.equalsIgnoreCase("Anna"));
System.out.println(result); // true

// clear collection

names.clear();

It is important to remember that not all of these methods are optional. removeIf method is not optional, while remove, removeAll and clear are optional operations.

Create a stream

We already talked about Java streams. You may remember, that the stream stands for a sequence of elements supporting sequential and parallel aggregate operations. java.util.Collection has two methods to create streams (both are not optional):

  • Stream<E> stream() = creates a sequential stream with this collection as its source
  • Stream<E> parallelStream() = creates a possibly parallel stream with this collection as its source.

Let have a look on the code snippet below:

List<Person> people;

Stream<Person> stream = people.stream();

Here we create a new stream from the collection of elements Person. If you are not familiar with streams, I encourage you to follow my post on streams in Java and Vavr

Iteration

From a technical point of view, iterations stands for a technique used to sequence through a block of code repeatedly until a specific condition either exists or no longer exists. Java provides us several approaches to iterate over a collection. NB that not all collections provide us way to access an element on a base of index (for instance, sets do not). Therefore in this section we will not explore popular iteration approaches, that will not work for all collections. Rather we will concentrate on common methods. As Java collections are also Iterable let explore how it permits us to go through elements of collection:

  • Using iterators
  • Using streams
  • using forEach

Iteration using streams is covered in details in my streams tutorial, so we will not talk about it here. Instead, let have a look on the iterator:

List<String> names = Arrays.asList("Anna", "Bob", "Carol", "David", "Egor", "Francesca", "Gabriel", "Hanka", "Ivan", "Jana");

// create iterator

Iterator iterator = names.iterator();

// go through collection

while(iterator.hasNext()){
	//access an element
	String name = iterator.next();
	System.out.println(name);
}

Basically, iterator pattern permits all elements of the collection to be accessed sequentially, with some operation being performed on each element. You can note that iterator has two core methods:

  • hasNext() – this method returns true if the iteration has more elements and we use it in the while loop (like next() in ResultSet)
  • next() – returns an element and we use it to access the current element of iteration

Note, that iterator() method is not optional. Another approach from Iterable is forEach method:

List<String> names = Arrays.asList("Anna", "Bob", "Carol", "David", "Egor", "Francesca", "Gabriel", "Hanka", "Ivan", "Jana");

names.forEach(System.out::println);

This method accepts Consumer function that specifies an action to apply on each element. NB is accessible in Java 8+.

Access individual element of the collection

I have to say here that java.util.Collection does not contain methods to access individual elements. Each implementation provides its own ways, for instance list elements can be accessed by index, while sets do not support this approach. It is very important to remember that there is no way to access Collection’s elements, as it depends on its subsclasses.

Other Collection methods

We did not provided detailed explanations to these methods, however they are still important to know. I group them under this section:

  • contains(Element e) = returns true if this collection contains the specified element. Uses equals() of object in order to check an equality of the element.
  • containsAll(Collection c) = returns true if this collection contains all of the elements in the specified collection.
  • isEmpty() = returns true if the collection is empty
  • size() = gets an integer value with a number of elements in the collection
  • toArray() = creates an array Element[] from the elements of the collection

Note, that all of these methods are not optional. After we explored java.util.Collection in details let have a look on concrete Java data structures that are available in JCF.

Lists

This is one of most common Java data structure. In theory, list is an ordered collection (also known as a sequence). Java lists are subclasses of java.util.List interface. Take a look on the graph below that represents an hierarchy of Java lists:

Graph 2. Java Lists

Lists allow duplicate elements and permit to access an element by its index. They also add some overloaded versions of common Collection’s methods. In this section we will observe common operations for lists in JCF.

Add elements to list

We already observed two methods in java.util.Collection. Lists also have two another methods to insert elements:

  • void add(int index, E element) = this method adds the specified element at the specified position in this list. NB this method is optional.
  • boolean addAll(int index, Collection c) = this method inserts all of the elements in the specified collection into this list at the specified position (also is optional)

Here is an example:

List<Integer> numbers = new ArrayList<>();

numbers.add(1);
numbers.add(3);
numbers.add(4); 
System.out.println(numbers);

// use add (position, element)

numbers.add(1,2);
System.out.println(numbers);

Lists have methods to search element and returns its numeric position: indexOf() and lastIndexOf. What is a difference between them?

  • indexOf(Element e) = returns the first occurrence of given element or -1 if element is not present in list
  • lastIndexOf(Element e) = returns the last occurrence of given element or -1 if element is not present in list

This code snippet demonstrates search in Java lists:

List<String> names = Arrays.asList("Anna", "Francesca", "Carol", "David", "Egor", "Francesca", "Gabriel", "Jana");

int firstFrancesca = names.indexOf("Francesca");
System.out.println(firstFrancesca); // 1

int lastFrancesca = names.lastIndexOf("Francesca");
System.out.println(lastFrancesca); // 5

Access an individual element

Lists permit to get an individual element using its index (NB index starts from zero):

List<String> names = Arrays.asList("Anna", "Francesca", "Carol", "David", "Egor", "Francesca", "Gabriel", "Jana");

String name = names.get(2);
System.out.println(name); // Carol

Create a sub list from a range

This method allows to create a sublist from elements of the parent list in the specified range from the starting index (inclusive) to the final index (exclusive):

List<String> names = Arrays.asList("Anna", "Francesca", "Carol", "David", "Egor", "Francesca", "Gabriel", "Jana");
List<String> sublist = names.subList(1,4);
System.out.println(sublist); // elements 1-3 from names

Replace elements

There are two methods to replace elements in the list:

  • set(int index, E element) = this method replaces the element at the specified position in this list with the specified element (NB is an optional operation)
  • replaceAll(UnaryOperator operator) = this method replaces each element of this list with the result of applying the operator to that element (NB not an optional operation)
List<Integer> numbers = Arrays.asList(1,2,4,4,5,6);
numbers.set(2, 3);

Set

Sets are collections that contain no duplicate elements. Java Sets use equals method of its element to check an equality with elements of the set that are presented already. Take a look on the hierarchy of Java Set:

Graph 3. Java Sets

Due to the set’s nature of unique elements, all constructors of sets must create a set that contains no duplicate elements. Some set implementations have restrictions on the elements that they may contain (for example null). That means that attempt to insert a null value will lead to NullPointerException. This means you need to check that element is not null. Also there are limitations to insert elements of different type – it will lead to the ClassCastException. Let observe most common methods for sets.

Add elements to set

Generally, java.util.Set interface does not have unique methods that differ from java.util.Collection to insert new elements. However, you have to remember that set has to contain non-duplicate elements. That means that call of add(Element e) method first will check for an equality of the element to elements of the set and will leave the set unchanged if element is already in set:

Set<Integer> numbers = new HashSet<Integer>();

boolean result = set.add(5); // true

boolean result2 = set.add(5); //false

Note on order of elements in set

It is important to remember about an order of elements in Java set. It depends on a concrete implementation:

  • HashSet and LinkedHashSet while do not allow duplicates, process elements like lists – in the order they were inserted.
  • TreeSet keeps all its elements in the sorted order

You can learn more about sets in Java in the dedicated post

Queue

Finally, we will review Queue – the last JCF class before we will go to Map. Queue is the collection that first-in first-out (FIFO) sequence. That means that addition of elements takes place only at the tail, and removal takes place only at the head. Take a look on this diagram:

Graph 4. Java Queues

Notice, that LinkedList implements java.util.Queue alongside with java.util.Queue. Additionally to common Collection’s methods, Queue has own methods for insertion, extraction, and inspection operations. Let go through them.

Insert element to queue

There are two ways to insert an element to queue – either by using add() method that is inherited from Collection, either by using queue-specific offer() method. The later inserts the specified element into this queue if it is possible to do so immediately without violating capacity restrictions. When using a capacity-restricted queue, this method is generally preferable to add(). Both of these methods notify developer when queue fails to insert an element:

  • add() throws IllegalStateException when fails to insert an element
  • offer() returns false in case of failure

The later method takes its importance on queues with restriction, for example with size restrictions. To get a difference let look on the code below:

Queue<Integer> numbers = new ArrayDeque<Integer>(5);

// we can insert 5 elements
numbers.add(43);
numbers.add(14);
numbers.add(9);
numbers.add(85);
numbers.add(90);

// throw an exception:
numbers.add(11);

//return false
boolean result = numbers.offer(10);

Remove element from queue

Due to the nature of this data structure, you can’t just remove the specific element. Rather you can remove head – first element. There are two methods remove() and poll(). Let get the difference:

// create a new queue

Queue<Integer> numbers = new ArrayDeque<>(10);

// remove head of EMPTY queue

int poll = numbers.poll(); // null

int remove = numbers.remove(); // throws NoSuchElementException 

So, a distinction between methods happens on empty queues. Special poll() methods either removes a head of queue or return null if no elements are presented. Collection’s remove() also removes a head element, but in the case of absence of elements throws NoSuchElementException.

Get head of queue

Finally, there are two methods to access the head element: element() and peek(). Similar to previous ones, they are also differ on a basis of behavior on an empty queue:

  • element() = retrieves the head of queue or throws NoSuchElementException if queue is empty
  • peek() = also retrieves the head, but in the case of an empty queue it returns null.

Have a look on this example:

Queue<Integer> numbers = new ArrayDeque<>(10);

numbers.add(10);

// head element is 10

int element = numbers.element();
int peek = numbers.peek();

Assertions.assertEquals(element, peek);

// remove head

numbers.poll();

int peek2 = numbers.peek();
Assertions.assertNull(peek);

int element2 = numbers.element(); // throws NoSuchElementException

To quickly learn the contrast between these methods, I advice you to review this table, originally from Java SE documentation:

Throws Exception on empty QueueReturns false on empty Queue
Inserting elementsadd()offer()
Remove elementsremove()poll()
Access a head elementelement()peek()

We observed data structures that implement java.util.Collection interface and their core methods. Let now move to Map data structure, that technically speaking is not a Java collection.

You can learn more on queues in Java in this post

Map

First thing to remind here is that maps are not collections. This is due to the fact that unlike collections, maps operate two entities – keys and values. Take a look on the hierarchy of Java maps on the graph below:

Graph 5. Java Maps

So, based on this unique nature of maps we usually use not an index of an element, but its key to retrieve it. NB that map can’t contain duplicate keys; each key can map to at most one value. Let explore most important methods from java.util.Map interface

Insert element to map

Unlike collections, where we used add methods in order to insert new elements, maps operates a concept of put. Therefore we have following methods in Maps:

  • V put(K key, V value) = this method associates the specified value V with the specified key K in the map and returns this value V
  • void putAll(Map m) = this method copies all of the mappings from the specified map m
  • V putIfAbsent(K key, V value) = this method, as it comes from its name, puts new element with key K if this key is not already specified. Otherwise returns null

Take a look on the code snippet below:

HashMap<String, Person> phoneBook = new HashMap<String,Person>();

phoneBook.put("433-2682", new Person("Anna"));
phoneBook.put("336-1609", new Person("Beata"));
phoneBook.put("805-0368", new Person("Carolina"));

// not permitted:
phoneBook.put("433-2682", new Person("Jana"));

While there are ways to insert different values with same keys, they are not in a scope of this post.

Remove elements

There are two overloaded methods to delete elements from maps and both of them require to know keys of objects to remove:

  • V remove(Object key) = this method removes the mapping for a key from this map if it is present (NB is an optional method)
  • boolean remove(Object key, Object value) = this method removes the entry for the specified key only if it is currently mapped to the specified value.

Let use the example from the previous section:

HashMap<String, Person> phoneBook = new HashMap<String,Person>();

phoneBook.put("433-2682", new Person("Anna"));
phoneBook.put("336-1609", new Person("Beata"));
phoneBook.put("805-0368", new Person("Carolina"));

// remove by key

Person person = phoneBook.remove("805-0368");

// remove by key + value

Person carolina = new Person("Carolina");
// we assume that equals() and hashCode() were overridden in Person
// e.g. carolina equals with carolina associated with 805-0368

boolean result = phoneBook.remove("805-0368", carolina);

Access element

To access an element in the map we use get() method that accepts a key as argument and returns the value to which this key is mapped, or null if this map does not contain mapping for such key. We can reuse a code from previous sections:

HashMap<String, Person> phoneBook = new HashMap<String,Person>();

phoneBook.put("433-2682", new Person("Anna"));
phoneBook.put("336-1609", new Person("Beata"));
phoneBook.put("805-0368", new Person("Carolina"));

Person beata = phoneBook.get("336-1609");

Another way is to use getOrDefault method. Instead of return null when key does not have a mapping, it returns a default value (similar to Java Optional). Take a look on the code snippet below:

// Beata is already in phone book

Person beata = phoneBook.get("336-1609");

// There is no Jana in phone book

Person jana = phoneBook.getOrDefault("444-7775", new Person("Jana"));

Replace elements

Finally, let have a look on methods that allow us to replace elements in the map:

  • V replace(K key, V value) = this method replaces the entry for the specified key K only if it is currently mapped to some value.
  • boolean replace(K key, V old, V new) = this method also replaces a value associated to K key, but only if it was mapped to old value
HashMap<String, Person> phoneBook = new HashMap<String,Person>();

phoneBook.put("433-2682", new Person("Anna"));
phoneBook.put("336-1609", new Person("Beata"));
phoneBook.put("805-0368", new Person("Carolina"));

// replace

phoneBook.replace("443-2682", new Person("Maria"));

Person person = phoneBook.get("443-2682");
// now it is Maria
Assertions.assertEquals("Maria".person.getName());

boolean result = phoneBook.replace("443-2682", anna, new Person("Zuzana"));
//false because not is Anna anymore

NB, that if key K is already presented in map, there is absolutely no difference in behavior of put() and replace() methods.

You can learn more about maps in this dedicated post

Conclusion

In this post we observed Java Collections Framework (JCF). We started from an understanding of the JCF’s components and core java.util.Collection interface, that provides common functionality for most data strcutures in Java (with exception of maps). We also learned most common methods for concrete collections: Lists, Sets, Queues. Finally, we explored how to work with Maps in Java.

References

  • John I. Moore, Jr Iterating over collections in Java 8 (2014) JavaWorld, read here
  • Herbert Schildt Java: The Complete Reference, Eleventh Edition 11th edn, McGraw-Hill Education, 2018
  • Marcus Biel An Introduction to the Java Collections Framework (2016) DZone, read here
Copy link
Powered by Social Snap