Understand Java Collections

When we talk about Java collections, we consider two things. First, more wide concept, means implementations of common data structures. An another, narrow understanding, corresponds to concrete interfaces and their implementations, that are contained in Java Collections Framework, supplied out-of-the box. In that sense, we need to remember that such collections inherit to the root interface – java.util.Collection, which – in its regard – defines all common functionality, that sub classes should implement.

Such common manipulations include various operations with elements, such as insertion and deletion, as well iterations or connection with Java Streams API. That is why I tried to put all these important concepts of java.util.Collection in one post, as they are crucial to know for any Java developer.

Do you want to increase your Java collections skills?

This topic is really huge, and a single post is not enough to cover all. That is why I wrote this Practical Guide. Everything you need in the one book. Do you want to become Java ninja?

Which data structures are collections

This section provides a helicopter view on Java Collections Framework. It offers 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.

Take a look on the graph below:

Graph 1. Java Collections Framework hierarchy

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

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

The root classes here 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.

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 (for example, sets), while other types permit duplicates (for example, array-based lists). java.util.Collection is not implemented directly, rather Java has implementations of its subinterfaces. This interface is typically used to pass collections around and manipulate them with max degree of generality.

Add elements to collections

There are two ways to insert new elements to collection:

  • Add a single element
  • Add all elements from the another collection.

Note, that both of these methods are marked optional. That meands, that concrete implementations are permitted to not perform one or more of these operations (in such cases they throw UnsupportedOperationException when insertion is performed).

Take a look on the code snippet below:

@Test
void insertTest(){
    // get mock posts
    List<Post> posts = getPosts();

    // Add single element
    Post post = new Post(6, "Phasellus scelerisque", "Phasellus scelerisque eros id lacus auctor");
    posts.add(post);
    assertThat(posts).contains(post).hasSize(6);

    // add multiple elements
    List<Post> newPosts = new ArrayList<>();
    newPosts.addAll(posts);
    assertThat(newPosts).containsAll(posts);
}

In examples I use an ArrayList implementation that is one of the Java collections and which implements both add() and addAll() methods. ArrayList has two add() methods, but we concentrate here on the Collection’s one.

To sum up, 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 c to the collection. Note, that this method also returns boolean value, which stands true if this collection changed as a result of the call

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

Delete an element

Compare to two inserting methods, there are more methods to remove 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:

@Test
void removeTest(){
    // get mock posts
    List<Post> posts = getPosts();

    Post post = posts.get(2);
    assertThat(posts).contains(post);

    // remove object
    posts.remove(post);
    assertThat(posts).doesNotContain(post);

    // clear
    posts.clear();
    assertThat(posts).isEmpty();

    // delete with predicate
    posts = getPosts();
    // remove posts with ID 2 and 4
    posts.removeIf(p -> p.getId() % 2 == 0);
    assertThat(posts).hasSize(3);
}

It is important to remember that not all of these methods are optional (means, can be skipped in particular implementations). removeIf method is not optional operation, while remove, removeAll and clear are optional operations.

Work with Streams API

In Java stream stands for a sequence of elements supporting sequential and parallel aggregate operations. java.util.Collection has two methods to initialize 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:

@Test
void streamTest(){
    List<Post> posts = getPosts();
    Stream<Post> stream = posts.stream();
    assertThat(stream).isInstanceOf(Stream.class);
}

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. Note, 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, which will not work for each collection. Rather we will concentrate on approaches, that can be used with any collection.

As Java collections are also Iterable let explore how it permits us to go through elements of collection:

  • Using iterators
  • Using streams
  • using forEach
@Test
void iterationTest(){
    List<Post> posts = getPosts();

    // create iterator
    Iterator<Post> iterator = posts.iterator();
    // Option 1 with hasNext
    System.out.println("Iteration using iterator hasNext");
    while(iterator.hasNext()){
        Post post = iterator.next();
        System.out.println(post);
    }

    Iterator<Post> iterator2 = posts.iterator();
    // Option 2 using forEachRemaining
    System.out.println("Iteration using iterator forEachRemaining");
    iterator2.forEachRemaining(p -> System.out.println(p));

    // using forEach
    System.out.println("Iteration using forEach");
    posts.forEach(System.out::println);

    // using stream
    System.out.println("Iteration using stream");
    posts.stream().forEach(p -> System.out.println(p));
}

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

You can also use forEachRemaining method. It accepts a Consumer function that is executed for each remaining element until all elements have been processed or the action throws an exception.

Note, that iterator() method is not optional.

Another approaches to mention here could be forEach method and using Streams API.

Access individual element of the collection

I have to say here that java.util.Collection does not contain methods to access individual elements. That means, that each particular implementation has its own ways. For instance elements of array-based lists can be accessed by their index, while sets do not support that. It is very important to remember that there is no way to access Collection’s elements, as it depends on its subsclasses.

Other non-optional 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

That what stands for all collections. Of course, each particular type has different underlying implementation of these methods, due to concrete data structure’s logic. But all of them inherit non-optional functionality from the root interface, that as we said already, ensures better learning curve for developers and promotes interoperability.

I tried to accumulate all important concepts in this post. If you think that something is missed or you have questions regarding java.util.Collection and concrete types, don’t hesitate to leave a comment below or contact me directly using these channels.

Share via
Copy link
Powered by Social Snap