I need an index with this List iteration method

Donald Raab
Javarevisited
Published in
6 min readJun 28, 2021

Iterating over Lists in Java with indices using external and internal iterators

Photo by Maksym Kaharlytskyi on Unsplash

How to iterate over a List in Java with indices

In Java, there are a few ways to to iterate over a List with indices. I will cover a few of the most common external iteration approaches, and how they can be combined with providing an index. I will explain how to use the RandomAccess interface to safely iterate over the List interface.

Iterate using an Iterator

This is the most basic form of collection iteration that works with any Collection (prior to Java 5), and any Iterable type in Java (since Java 5).

List<Integer> list = Lists.mutable.with(1, 2, 3);
int index = 0;
for (Iterator<Integer> it = list.iterator(); it.hasNext(); index++)
{
Integer integer = it.next();
System.out.println(integer + ":" + index);
}

// Outputs:
// 1:0
// 2:1
// 3:2

Use this form of iteration if you need to directly manipulate the Iterator. An example of this would be if you had a flattened list of pairs which you needed to call next() twice to re-construct their pairs.

Iterator using the Java 5 for-each loop

The for-each loop is just syntactic sugar for a for-loop with an Iterator.

List<Integer> list = List.of(1, 2, 3);
int index = 0;
for (Integer integer : list)
{
System.out.println(integer + ":" + index++);
}

// Outputs:
// 1:0
// 2:1
// 3:2

Use this form of iteration in most cases, except when are concerned with raw iteration performance and creating an Iterator instance.

Iterate using an indexed for-loop

This is the fastest external iteration for-loop for a random access List.

List<Integer> list = Lists.mutable.with(1, 2, 3);
for (int i = 0; i < list.size(); i++)
{
Integer integer = list.get(i);
System.out.println(integer + ":" + i);
}

// Outputs:
// 1:0
// 2:1
// 3:2

Use this form of iteration when you are optimizing iteration code for performance. This is mostly applicable in library or framework code, but may also apply to some performance critical or garbage sensitive areas in application code. Only use this code in places where you can guarantee that a List is RandomAccess.

But how do you know if a List supports random access?

RandomAccess Interface

In Java 1.4, a marker interface was introduced named RandomAccess. I don’t know why it was added only as a marker interface, and didn’t have methods for size and get defined on it. This interface is used by many library algorithms to optimize for RandomAccess Lists, and to provide performance safe iteration for non-RandomAccess List instances. You will find references to this interface in instanceof checks in the standard Java library (look in the Collections class for some examples). The following is an example of using the RandomAccess interface to iterate over a List that may or may not be RandomAccess.

List<Integer> list = Stream.of(1, 2, 3)
.collect(Collectors.toCollection(LinkedList::new));
if (list instanceof RandomAccess)
{
for (int i = 0; i < list.size(); i++)
{
Integer integer = list.get(i);
System.out.println(integer + ":" + i);
}
}
else
{
int index = 0;
for (Integer integer : list)
{
System.out.println(integer + ":" + index++);
}
}

// Outputs:
// 1:0
// 2:1
// 3:2

In this example, the else part of the if statement will execute, since LinkedList is not a RandomAccess List.

IntStream.range() and subList()

Since Java 8, it is possible to use IntStream.range() to iterate over a set of indices and use List look ups using get(). IntStream.range() can be used an object-oriented version of an indexed for-loop.

List<Integer> list = List.of(1, 2, 3, 4, 5);
IntStream.range(1, 4)
.forEach(index ->
System.out.println(list.get(index) + ":" + index));

// Outputs:
// 2:1
// 3:2
// 4:3

With this approach, a developer can iterate over a specific range of indexes in the List. The same can be accomplished using subList.

List<Integer> list = List.of(1, 2, 3, 4, 5);
list.subList(1, 4).forEach(System.out::println);

// Outputs:
// 2
// 3
// 4

This code will output all but the first and last element. Both InStream.range() and subList() are inclusive for the from index, and exclusive for the to index.

Notice however, that with subList(), we lost access to the index in the call to forEach. There is no way to get access to this index back using subList.

Internal Eclipse Collections iterators with an index

Eclipse Collections has a few internal iterators which are passed the element and index to the functional interface that is provided.

forEachWithIndex (OrderedIterable)

This method will iterate from first element to last element passing each element and its index to the ObjectIntProcedure provided.

MutableList<Integer> list = Lists.mutable.with(1, 2, 3);
list.forEachWithIndex((each, index) ->
System.out.println(each + ": " + index));

// Outputs:
// 1: 0
// 2: 1
// 3: 2

reverseForEachWithIndex (ReversibleIterable)

This method will iterate from last element to first element passing each element and its index to the ObjectIntProcedure provided.

MutableList<Integer> list = Lists.mutable.with(1, 2, 3);
list.reverseForEachWithIndex((each, index) ->
System.out.println(each + ": " + index));

// Outputs:
// 3: 2
// 2: 1
// 1: 0

forEachWithIndex with a range (OrderedIterable)

There is an overload of forEachWithIndex which takes an inclusive from and to index range in addition to the ObjectIntProcedure.

MutableList<ObjectIntPair<String>> result =
Lists.mutable.empty();
MutableList<String> list =
Lists.mutable.with("1", "2", "3", "4", "5");
list.forEachWithIndex(1, 3,
(each, index) -> result.add(
PrimitiveTuples.pair(each, index)));

var expected = List.of(
PrimitiveTuples.pair("2", 1),
PrimitiveTuples.pair("3", 2),
PrimitiveTuples.pair("4", 3));
Assertions.assertEquals(expected, result);

This method can be used to iterate both forward and reverse, based on the range that is specified. The following code will execute in reverse because the from index is greater than the to index.

MutableList<ObjectIntPair<String>> result =
Lists.mutable.empty();
MutableList<String> list =
Lists.mutable.with("1", "2", "3", "4", "5");
list.forEachWithIndex(3, 1,
(each, index) -> result.add(
PrimitiveTuples.pair(each, index)));

var expected = List.of(
PrimitiveTuples.pair("4", 3),
PrimitiveTuples.pair("3", 2),
PrimitiveTuples.pair("2", 1));
Assertions.assertEquals(expected, result);

zipWithIndex

This method takes all of the elements of the collection and “zips” them together with their indices into Pair instances.

MutableList<String> list = Lists.mutable.with("1", "2", "3");
MutableList<Pair<String, Integer>> zipped = list.zipWithIndex();

var expected = List.of(
Tuples.pair("1", 0),
Tuples.pair("2", 1),
Tuples.pair("3", 2));
Assertions.assertEquals(expected, zipped);

collectWithIndex (OrderedIterable)

This method allows a developer to collect up all of the elements of a collection with their indices to a new collection. This is a more general, and potentially useful form of zipWithIndex, where the result can be any type the developer wants, not just a Pair<Integer, Integer>. In the following example, the collection is collected into a new collection of StringIntPair record instances.

record StringIntPair(String value, int index){};
MutableList<String> list = Lists.mutable.with("1", "2", "3");
MutableList<StringIntPair> collected =
list.collectWithIndex(StringIntPair::new);

var expected = List.of(
new StringIntPair("1", 0),
new StringIntPair("2", 1),
new StringIntPair("3", 2));
Assertions.assertEquals(expected, collected);

The collectWithIndex method takes an ObjectIntToObjectFunction as a parameter.

Arriving in Eclipse Collections 11.0

After writing this blog, I decided it was time to add selectWithIndex and rejectWithIndex methods to the OrderedIterable and ListIterable interfaces. When Eclipse Collections 11.0 is released, developers will be able to filter both inclusively and exclusively using predicates that take both the element and index as parameters (ObjectIntPredicate).

The following example demonstrates how to divide a list into separate lists based on even and odd indexes.

ImmutableList<Integer> list = Lists.immutable.with(1, 2, 3, 4, 5);
ImmutableList<Integer> evenIndexes =
list.selectWithIndex((each, index) -> index % 2 == 0);
ImmutableList<Integer> oddIndexes =
list.rejectWithIndex((each, index) -> index % 2 == 0);

Assert.assertEquals(Lists.immutable.with(1, 3, 5), evenIndexes);
Assert.assertEquals(Lists.immutable.with(2, 4), oddIndexes);

The selectWithIndex and rejectWithIndex methods are available in the Eclipse Collections 11.0.0.M3 milestone release.

Summary

In this blog, I demonstrate different approaches for iterating over List instances with both their elements and indices. In plain Java, your only options today are using external iterators. With Eclipse Collections, developers have the option to use a few specialized internal iterators that pass elements and indexes as parameters to the functional interfaces they are provided.

Enjoy!

I am a Project Lead and Committer for the Eclipse Collections OSS project at the Eclipse Foundation. Eclipse Collections is open for contributions. If you like the library, you can let us know by starring it on GitHub.

--

--

Donald Raab
Javarevisited

Java Champion. Creator of the Eclipse Collections OSS Java library (https://github.com/eclipse/eclipse-collections). Inspired by Smalltalk. Opinions are my own.