1. Overview

List is a pretty commonly used data structure in Java. Sometimes, we may need a nested List structure for some requirements, such as List<List>.

In this tutorial, we’ll take a closer look at this “List of Lists” data structure and explore some everyday operations.

2. List Array vs. List of Lists

We can look at the “List of Lists” data structure as a two-dimensional matrix. So, if we want to group a number of List objects, we have two options:

  • Array-based: List[]
  • List-based: List<List>

Next, let’s have a look at when to choose which one.

Array is fast for “get” and “set” operations, which run in O(1) time. However, since the array’s length is fixed, it’s costly to resize an array for inserting or deleting elements.

On the other hand, List is more flexible on insertion and deletion operations, which run in O(1) time. Generally speaking, List is slower than Array on “get/set” operations. But some List implementations, such as ArrayList, are internally based on arrays. So, usually, the difference between the performance of Array and ArrayList on “get/set” operations is not noticeable.

Therefore, we would pick the List<List> data structure in most cases to gain better flexibility.

Of course, if we’re working on a performance-critical application, and we don’t change the size of the first dimension – for instance, we never add or remove inner Lists – we can consider using the List[] structure.

We’ll mainly discuss List<List> in this tutorial.

3. Common Operations on List of Lists

Now, let’s explore some everyday operations on List<List>.

For simplicity, we’ll manipulate the List<List> object and verify the result in unit test methods.

Further, to see the change straightforwardly, let’s also create a method to print the content of the List of Lists:

private void printListOfLists(List<List<String>> listOfLists) {
    System.out.println("\n           List of Lists          ");
    System.out.println("-------------------------------------");
    listOfLists.forEach(innerList -> {
        String line = String.join(", ", innerList);
        System.out.println(line);
    });
}

Next, let’s first initialize a list of lists.

3.1. Initializing a List of Lists

We’ll import data from a CSV file into a List<List> object. Let’s first look at the CSV file’s content:

Linux, Microsoft Windows, Mac OS, Delete Me
Kotlin, Delete Me, Java, Python
Delete Me, Mercurial, Git, Subversion

Let’s say we name the file as example.csv and put it under the resources/listoflists directory.

Next, let’s create a method to read the file and store the data in a List<List> object:

private List<List<String>> getListOfListsFromCsv() throws URISyntaxException, IOException {
    List<String> lines = Files.readAllLines(Paths.get(getClass().getResource("/listoflists/example.csv")
        .toURI()));

    List<List<String>> listOfLists = new ArrayList<>();
    lines.forEach(line -> {
        List<String> innerList = new ArrayList<>(Arrays.asList(line.split(", ")));
        listOfLists.add(innerList);
    });
    return listOfLists;
}

In the getListOfListsFromCsv method, we first read all lines from the CSV file into a List object. Then, we walk through the lines List and convert each line (String) into List.

Finally, we add every converted List object to listOfLists. Thus, we’ve initialized a list of lists.

Curious eyes may have detected that we wrap Arrays.asList(..) in a new ArrayList<>(). This is because the Arrays.asList method will create an immutable List. However, we’ll make some changes to the inner lists later. Therefore, we wrap it in a new ArrayList object.

If the list of lists object is created correctly, we should have three elements, which is the number of lines in the CSV file, in the outer list.

Moreover, each element is an inner list, and each of those should contain four elements. Next, let’s write a unit test method to verify this. Also, we’ll print the initialized list of lists:

List<List<String>> listOfLists = getListOfListsFromCsv();

assertThat(listOfLists).hasSize(3);
assertThat(listOfLists.stream()
  .map(List::size)
  .collect(Collectors.toSet())).hasSize(1)
  .containsExactly(4);

printListOfLists(listOfLists);

If we execute the method, the test passes and produces the output:

           List of Lists           
-------------------------------------
Linux, Microsoft Windows, Mac OS, Delete Me
Kotlin, Delete Me, Java, Python
Delete Me, Mercurial, Git, Subversion

Next, let’s so make some changes to the listOfLists object. But, first, let’s see how to apply changes to the outer list.

3.2. Applying Changes to the Outer List

If we focus on the outer list, we can ignore the inner list at first. In other words, we can look at List<List> as the regular List.

Thus, it’s not a challenge to change a regular List object. We can call List‘s methods, such as add and remove, to manipulate the data.

Next, let’s add a new element to the outer list:

List<List<String>> listOfLists = getListOfListsFromCsv();
List<String> newList = new ArrayList<>(Arrays.asList("Slack", "Zoom", "Microsoft Teams", "Telegram"));
listOfLists.add(2, newList);

assertThat(listOfLists).hasSize(4);
assertThat(listOfLists.get(2)).containsExactly("Slack", "Zoom", "Microsoft Teams", "Telegram");

printListOfLists(listOfLists);

An element of the outer list is actually a List object. As the method above shows, we create a list of popular online communication utilities. Then, we add the new list to listOfLists in the position with index=2.

Again, after the assertions, we print the content of listOfLists:

           List of Lists           
-------------------------------------
Linux, Microsoft Windows, Mac OS, Delete Me
Kotlin, Delete Me, Java, Python
Slack, Zoom, Microsoft Teams, Telegram
Delete Me, Mercurial, Git, Subversion

3.3. Applying Changes to Inner Lists

Finally, let’s see how to manipulate the inner lists.

Since listOfList is a nested List structure, we need to first navigate to the inner list object we want to change. If we know the index exactly, we can simply use the get method:

List<String> innerList = listOfLists.get(x);
// innerList.add(), remove() ....

However, if we would like to apply a change to all inner lists, we can pass through the list of lists object via a loop or the Stream API.

Next, let’s see an example that removes all “Delete Me” entries from the listOfLists object:

List<List<String>> listOfLists = getListOfListsFromCsv();

listOfLists.forEach(innerList -> innerList.remove("Delete Me"));

assertThat(listOfLists.stream()
    .map(List::size)
    .collect(Collectors.toSet())).hasSize(1)
    .containsExactly(3);

printListOfLists(listOfLists);

As we’ve seen in the method above, we iterate each inner list via listOfLists.forEach{ … } and use a lambda expression to remove “Delete Me” entries from innerList.

If we execute the test, it passes and produces the following output:

           List of Lists           
-------------------------------------
Linux, Microsoft Windows, Mac OS
Kotlin, Java, Python
Mercurial, Git, Subversion

3.4. The Size of the List of Lists

We know the standard List.size() method returns the count of the elements in the list. However, when talking about the size of a list of lists, depending on the requirement, there can be two scenarios:

  • The number of inner lists
  • The sum of the number of elements in all inner lists

Next, let’s see how to calculate the two different sizes through examples.

Since inner lists are elements of the outer list, we can simply call the listOfLists.size() method to get the number of inner lists:

List<List<String>> listOfLists = getListOfListsFromCsv();
// the number of inner lists
assertThat(listOfLists).hasSize(3);

However, to get all elements in inner lists, we must sum up each inner list’s size. We can write a loop to access each inner list and sum their size. A better way is to use Stream API to get the result:

// size of all elements in inner lists
int totalElements = listOfLists.stream().mapToInt(List::size).sum();
assertThat(totalElements).isEqualTo(12);

As the example above shows, we transform each inner list into an integer using the mapToInt() method. The integer is the size of each inner list. Further, as mapToInt() returns an IntStream, we can get the sum of the IntStream by calling the sum() method.

4. Conclusion

In this article, we’ve discussed the list of lists data structure.

Further, we’ve addressed the common operations on the list of lists through examples.

As usual, the complete code of this article can be found over on GitHub.