3
\$\begingroup\$

Intro

My idea this time was to be able to write something like this:

List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = List.of(4, 5);
        
for (Integer i : new CompoundListIterable<>(list1, list2)) {
    System.out.println(i);
}

prints

1
2
3
4
5

Code

io.github.coderodde.util.CompoundListIterable.java:

package io.github.coderodde.util;

import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;

/**
 * This class combines several lists and exposes them as an iterable over them.
 * 
 * @param <E> the list element type.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.0.0 (Dec 23, 2025)
 * @since 1.0.0 (Dec 23, 2025)
 */
public class CompoundListIterable<E> implements Iterable<E> {

    /**
     * The actual lists to iterate.
     */
    private final List<List<E>> lists;
    
    /**
     * The sum of all list sizes.
     */
    private final int compoundLength;
    
    public CompoundListIterable(List<E>... lists) {
        checkConstructorInput(lists);
        this.lists = List.of(lists);
        this.compoundLength = computeCompoundLength(lists);
    }

    @Override
    public Iterator<E> iterator() {
        return new CompoundListIterator();
    }
    
    /**
     * This inner class implements the actual iterator over compound lists.
     */
    private class CompoundListIterator implements Iterator<E> {

        /**
         * The local index into the current list under iteration.
         */
        private int localListIndex = 0;
        
        /**
         * The index of the current list under iteration.
         */
        private int currentListIndex = 0;
        
        /**
         * Caches the number of iterated elements.
         */
        private int iterated = 0;
        
        @Override
        public boolean hasNext() {
            return iterated < compoundLength;
        }

        @Override
        public E next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            
            List<E> currentList = lists.get(currentListIndex);
            
            // Omit empty lists:
            while (currentList.isEmpty()) {
                currentList = lists.get(++currentListIndex);
                localListIndex = 0;
            }
            
            int myLocalListIndex = localListIndex;
            ++localListIndex;
            ++iterated;
            
            if (localListIndex == currentList.size()) {
                // Once here, move to the next list:
                localListIndex = 0;
                currentListIndex++;
            }
            
            return currentList.get(myLocalListIndex);
        }
    }
    
    /**
     * Checks that none of the input lists are {@code null}.
     * 
     * @param <E> the list element type.
     * @param lists the lists to check.
     */
    private static <E> void checkConstructorInput(List<E>[] lists) {
        int index = 0;
        
        for (List<E> list : lists) {
            Objects.requireNonNull(
                    list, 
                    String.format("Input list at index %d is null", 
                                  index));
            ++index;
        }
    }
    
    /**
     * Computes the sum of all the lists' sizes.
     * 
     * @param <E> the list element type.
     * @param lists the lists whose sizes to sum.
     * @return the total size of all the input lists.
     */
    private static <E> int computeCompoundLength(List<E>[] lists) {
        int length = 0;
        
        for (List<E> list : lists) {
            length += list.size();
        }
        
        return length;
    }
}

io.github.coderodde.util.CompoundListIterableTest.java:

package io.github.coderodde.util;

import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import org.junit.Test;
import static org.junit.Assert.*;

public class CompoundListIterableTest {
    
    @Test
    public void emptyListList() {
        Iterator<Integer> it = new CompoundListIterable<Integer>().iterator();
        assertFalse(it.hasNext());
    }
    
    @Test
    public void emptyTwoLists() {
        Iterator<Integer> it = 
            new CompoundListIterable<Integer>(List.of(),
                                              List.of()).iterator();
        assertFalse(it.hasNext());
    }
    
    @Test
    public void twoLists() {
        List<Integer> list1 = List.of(1, 2, 3);
        List<Integer> list2 = List.of(4, 5);
        
        Iterator<Integer> it = 
                new CompoundListIterable<>(list1, list2).iterator();
        
        for (int i = 1; i < 6; ++i) {
            assertEquals(Integer.valueOf(i), it.next());
        }
    }
    
    @Test
    public void threeListsMiddleIsEmpty() {
        List<Integer> list1 = List.of(1, 2, 3);
        List<Integer> list2 = List.of();
        List<Integer> list3 = List.of(4, 5);
        
        Iterator<Integer> it = 
                new CompoundListIterable<>(list1, list2, list3).iterator();
        
        for (int i = 1; i < 6; ++i) {
            assertEquals(Integer.valueOf(i), it.next());
        }
    }
    
    @Test
    public void threeListsLeftIsEmpty() {
        List<Integer> list1 = List.of();
        List<Integer> list2 = List.of(1, 2, 3);
        List<Integer> list3 = List.of(4, 5);
        
        Iterator<Integer> it = 
                new CompoundListIterable<>(list1, list2, list3).iterator();
        
        for (int i = 1; i < 6; ++i) {
            assertEquals(Integer.valueOf(i), it.next());
        }
    }
    
    @Test
    public void threeListsRightIsEmpty() {
        List<Integer> list1 = List.of();
        List<Integer> list2 = List.of(1, 2, 3);
        List<Integer> list3 = List.of(4, 5);
        
        Iterator<Integer> it = 
                new CompoundListIterable<>(list1, list2, list3).iterator();
        
        for (int i = 1; i < 6; ++i) {
            assertEquals(Integer.valueOf(i), it.next());
        }
    }
    
    @Test
    public void largerIterable() {
        List<Integer> list1 = List.of();
        List<Integer> list2 = List.of(1, 2, 3);
        List<Integer> list3 = List.of();
        List<Integer> list4 = List.of();
        List<Integer> list5 = List.of();
        List<Integer> list6 = List.of(4, 5, 6, 7);
        List<Integer> list7 = List.of();
        List<Integer> list8 = List.of();
        
        Iterator<Integer> it = 
                new CompoundListIterable<>(list1,
                                           list2, 
                                           list3,
                                           list4,
                                           list5,
                                           list6,
                                           list7,
                                           list8).iterator();
        
        for (int i = 1; i <= 7; ++i) {
            assertEquals(Integer.valueOf(i), it.next());
        }
    }
    
    @Test(expected = NoSuchElementException.class) 
    public void throwsOnNextAfterLastElementIterated() {
        Iterator<Integer> it = 
                new CompoundListIterable<>(List.of(1), 
                                           List.of(), 
                                           List.of()).iterator();
        
        it.next();
        it.next();
    }
}

(Jacoco reports 100% test coverage.)


Critique request

As always, I am eager to receive any constructive comments.

\$\endgroup\$
0

2 Answers 2

3
\$\begingroup\$

You compute the total number of list elements in the CompoundListIterable constructor and keep track of the number of elements iterated so far. That has undesired effects if any of the original lists is mutated after the construction of the compound iterator and before the actual iteration:

List<Integer> list1 = new ArrayList<>(List.of(1, 2));
Iterator<Integer> it = new CompoundListIterable<>(list1).iterator();
list1.remove(0);

while (it.hasNext()) {
    System.out.println(it.next());
}

prints 2 and then terminates with an IndexOutOfBoundsException.

This problem is avoided if the “move to the next list” operation is done in the hasNext() method:

@Override
public boolean hasNext() {
    while (currentListIndex < lists.size()
        && localListIndex >= lists.get(currentListIndex).size()) {
        ++currentListIndex;
        localListIndex = 0;
    }
    return currentListIndex < lists.size();
}

@Override
public E next() {
    if (!hasNext()) {
        throw new NoSuchElementException();
    }
    E item = lists.get(currentListIndex).get(localListIndex);
    ++localListIndex;
    return item;
}

This also

  • simplifies the logic,
  • makes a dedicated check for empty lists obsolete,
  • makes the computeCompoundLength() method and the iterated and compoundLength member variables obsolete.

List.of already throws a NullPointerException if an element is null, so your checkConstructorInput() is not needed – unless you want the more specific error message “Input list at index N is null”.

\$\endgroup\$
3
\$\begingroup\$

Scope

I think the scope of this is too limited. To be really useful it should take any set of Iterable<E> elements and chain them together. This would still let you work with List<E> objects, of course, but also with any other class that implements Iterable.

To accomplish this you can't rely on indices, but rather on the iterator features of each Iterable<E> object.

Leveraging the standard library

You should definitely look into streams as a way of implementing this.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.