What if Java had no for?

Donald Raab
19 min readJan 15, 2024

--

These loops look like objects to me!

Photo by Etienne Girardet on Unsplash

Where for art thou?

This blog is intended to make you think about and discover alternatives to modeling for loops in an object-oriented (OO) programming language. It is convenient to have a versatile and performant eager for statement built in the Java language. It is also convenient to have lazy versions of numeric range based for loops in the form of IntStream and LongStream.

In my previous blog, I introduced a new lazy abstraction for an int range based for loop, without giving much of an explanation. This is the quote in the previous blog where I introduced the concept.

In the active Boolean version of the the code, I use an IntInterval from Eclipse Collections to represent an OO version of the for loop.

I will explain what an IntInterval from Eclipse Collections is later in the blog.

The following are the topics I will cover in this blog.

In this blog, I explain some of the versatility of for statements in Java with examples. I explain how some of the language (lambdas) and library (Streams) work including in Java 8 release have improved the level of abstraction of looping constructs for Java developers. Java does not go as far as Smalltalk does on the OO abstraction level for looping constructs. I show and explain how Smalltalk makes some things amazingly simple in its pure object-oriented approach to looping. Finally, I explain some features that Eclipse Collections provides that enables additional levels of abstraction for Java developers to enhance their productivity.

This blog does not include anything about parallelism.

1. For Loops in Java

Three parts of the for

Looping is part of control flow in a programming language. For loops are used to do things until a condition is met, or infinitely if no condition is specified. There is a for statement in Java that is well structured and extremely useful for executing a block of code a number of times. The example I used in the previous blog was to output a String parameter a specified number of times.

public static void main(String[] args)
{
int numberOfTimes = Integer.parseInt(args[0]);
for (int i = 0; i < numberOfTimes; i++)
{
System.out.println(args[1]);
}
}

The code will throw an exception if less than two arguments are passed in. This code will output the String at args[1] the number of times specifed at args[0]. The String at args[0] is converted to an int and stored in the variable numberOfTimes.

The structure of a for loop in Java includes three statements inside of parentheses after the for keyword, each separated by a semi-colon. The statements are as follows.

  • Initialization— executed once to initialize one or more variables
  • Continue Condition — a boolean expression that when true will continue to loop and when false will cause the loop to exit
  • Advancement Expression— an expression that may cause a change in the result of the Continue Condition, like incrementing or decrementing a counter or calling next on an Iterator.

For example — Summing numbers 1 to 10

A very simple example of a for loop in Java is summing the numbers from 1 to 10. The following test is an example of this.

    @Test
public void sumNumbersFromOneToTen()
{
int sum = 0;
for (
int i = 1; // Intialization
i <= 10; // Continue Condition
i++) // Advancement Expression
{
sum += i;
}
Assertions.assertEquals(55, sum);
}

For example — Summing numbers 10 to 1

This code could also be written as summing the numbers from 10 to 1. The following test is an example of this.

@Test
public void sumNumbersFromTenToOne()
{
int sum = 0;
for (
int i = 10; // Intialization
i > 0; // Continue Condition
i--) // Advancement Expression
{
sum += i;
}
Assertions.assertEquals(55, sum);
}

Inlining the three statements

I previously broke the three expressions over multiple lines so they are easy to parse and read. Normally, the expressions will all be on the same line, as follows:

// For loop from 1 to 10 incrementing by 1
for (int i = 1; i <= 10; i++)

// For loop from 10 to 1 decrementing by 1
for (int i = 10; i > 0; i--)

The for loop in Java is very versatile. Before the Java language had lambdas, the for loop was the preferred mechanism for iterating over an array or Collection.

Sum Array of ints —Indexed Access

The following for loop uses indices for summing up elements of an int array.

@Test
public void sumArrayOfIntsUsingIndexedAccess()
{
int sum = 0;
int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (
int i = 0; // Intialization
i < array.length; // Continue Condition
i++) // Advancement Expression
{
sum += array[i];
}
Assertions.assertEquals(55, sum);
}

Sum Array of ints — Java 5 for loop

The following for loop uses the simplified version of the for loop introduced in Java 5 for iterating over each element of a an int array.

@Test
public void sumArrayOfIntsUsingForEachLoop()
{
int sum = 0;
int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (int each : array)
{
sum += each;
}
Assertions.assertEquals(55, sum);
}

The first part of the for loop includes the type and name of a varaible for each element in the array. In this case, I use int each.The second part, separated by a :, is the array to loop over.

Sum List of Integers — Indexed Access

If we have a List of Integer objects, we have a few ways we can write a for loop to calculate the sum. We can loop using indexed access.

@Test
public void sumListOfIntegersUsingIndexedAccess()
{
int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
for (
int i = 0; // Intialization
i < list.size(); // Continue Condition
i++) // Advancement Expression
{
sum += list.get(i).intValue();
}
Assertions.assertEquals(55, sum);
}

Sum List of Integers — Iterator

We can loop using an explicit iterator.

@Test
public void sumListOfIntegersUsingIterator()
{
int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
for (
Iterator<Integer> it = list.iterator(); // Intialization
it.hasNext(); // Continue Condition
// No Advancement Expression
)
{
sum += it.next().intValue(); // Advancement in statement via next()
}
Assertions.assertEquals(55, sum);
}

Sum List of Integers — Java 5 for loop

We can loop using the enhanced for loop available since Java 5, which is really a shorthand for using the iterator approach above.

@Test
public void sumListOfIntegersUsingJava5ForLoop()
{
int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
for (Integer each : list)
{
sum += each.intValue();
}
Assertions.assertEquals(55, sum);
}

All for one, and for for all

The versatile nature of a for loop makes it a tough competitor for looping. Oleg Pliss commented on my post of this article in LinkedIn:

A few important features of ‘for’ loops are missing in the article: access to the locals outside the loop; ‘continue’ and ‘break’ statements (potentially with a label); and ‘return’ (from the method).

This is some of what I was alluding to by the versatility of the for statement, and Oleg did a masterful job of identifying the functionality I did not cover in a single sentence. Here are links to further tutorials on for and the branching statements I didn’t cover here for those interested in learning more.

In the rest of the blog I will demonstrate how for loops are implemented in Smalltalk, and show how in Java 8 and with Eclipse Collections, Java has advanced towards a more object-oriented and functional model of for loops. I have been working on replacing unnecessary for loops in Java applications for the past 20 years as I explain in the following blog. Usually there is a higher level algorithm like filter, map, or reduce that is implemented imperatively with a for loop. It’s more readable if you can hide the implementation details of a for loop behind a higher level abstraction that explains what you are doing with an intention revealing name. It’s also potentially better for performance for different types to optimize for specific iteration patterns, instead of requiring developers to write different for loops for different types because one is faster with indexed access vs. being faster with an iterator.

2. OO For Loops in Smalltalk

The following is a quote from my last blog about Smalltalk and control flow.

Smalltalk is a programming language that models control flow in its class library, instead of the language. There are no if statements, for loops, while statements in Smalltalk. There are instead methods on classes that make control flow possible. Methods are the abstraction that are used to model all control flow, along with code blocks, which are also known as lambdas or closures.

An object-oriented version of a for loop will result in looping behavior distributed across many classes. The behavior of different kinds of for loops are aligned with the object that makes the most sense to be responsible for that behavior.

There are both eager and lazy forms of a for loop in an object-oriented model. The statement version of a for loop in Java is always eager.

Smalltalk Interval — sum

I will start by showing how to sum the numbers from 1 to 10 using Smalltalk’s Interval type. Interval is a lazy for loop. Interval is also a lazy SequqencedCollection.

testSumNumbersFromOneToTen
|sum|

sum := (1 to: 10) sum.

self assert: sum equals: 55.

In my two previous “What if…” blogs, I explained that “everything is an object” in Smalltalk. I will explain one step at a time what all the objects are in the code above, and which are receiving the messages that are accomplishing the task at hand.

The first object instance encountered in the code is the literal 1. The object instance represented by the literal 1 is of type SmallInteger. The message to: is sent to the object 1 with the parameter 10, which is also an instance of SmallInteger. If I inspect the result of the code (1 to: 10) in Pharo Smalltalk, the result is an instance of an Interval. The following is a screen capture of the result.

The Code (1 to: 10) returns an Interval in Smalltalk

Interval is lazy because it knows about the range of data (1 to 10 by 1) but has yet to do anything with that data. The Interval class in Smalltalk is designed as follows.

Interval in the Smalltalk Class Hierarchy

The decision to make Interval a SequencedCollection brings a lot of convenience. The internal iterator support for Interval is extensive. There are class methods on Interval which allow for construction. The convenient shorthand I used about calling to: on SmallInteger can be searched in the Smalltalk class library quickly to show how the Interval is constructed.

The to: method defined on the Number class which is a parent class for SmallInteger

The ^ means return in Smalltalk, so the code 1 to: 10 is going to result in Interval from: 1 to: 10 by: 1. An Interval in Smalltalk is inclusive for both the start and stop.

The final step to understanding how the loop itself is implemented in the sum of the integers from 1 to 10 example is to look at the sum method.

The implementation of sum on Interval

What we discover here is that sum is optimized for Interval. It uses a formula instead of iteration with a loop. This is one of the benefits of having loops represented by objects. They can provide encapsulate and optimized behaviors.

Smalltalk Interval — do:

In the interest of demonstrating the actual loop, I will implement an iterative sum by hand in a test.

testSumNumbersFromOneToTenWithDo
|sum|

sum := 0.
(1 to: 10) do: [ :each | sum := sum + each ].

self assert: sum equals: 55.

I use the do: method here with a Block (lambda) which updates the sum variable with the value of sum plus the value of each SmallInteger. The implementation of do: on Interval looks as follows.

The implementation of do: on Interval class in Pharo Smalltalk

The implementation of do: uses a Block (Condition Continue Block) with whileTrue: followed by Block (Execution Block) to perform the looping. The Condition Continue Block and Execution Block both require access to the index variable which is scoped outside of the blocks, and the Execution Block actually increments the index variable. I will not be implementing whileTrue: for educational purposes in Java as it would require using a final int array or AtomicInteger, LongAdder or equivalent for index to be able to be incremented. There is a tweet reply from Vladimir Zakharov that shows how whileTrue could be implemented on a Functional Interface if someone wanted.

Smalltalk Interval — inject:into:

There is another internal iterator that I can use in Smalltalk that will not require updating a local variable for each element in the Interval. That method is named inject:into:, or as I like to call it, the Continuum Transfunctioner. Here is a sum implemented using inject:into:.

testSumNumbersFromOneToTenWithInjectInto
|sum|

sum := (1 to: 10) inject: 0 into: [ :result :each | result + each ].

self assert: sum equals: 55.

The way the above inject:into: code works can be explained as follows as each element is visited in the Interval.

result := 0
result + each = ?
=================
0 + 1 = 1
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15
15 + 6 = 21
21 + 7 = 28
28 + 8 = 36
36 + 9 = 45
45 + 10 = 55

The result of each iteration is “injected” into the block for the next iteration as the first parameter. The implementation of inject:into: for the Interval class in Pharo Smalltalk is as follows.

The implementation of inject:into: on the Collection class in Pharo Smalltalk

Smalltalk Interval —in reverse order

In order to reverse an Interval, I will need to add a negative step value using the to:by: method on SmallInteger as follows.

testSumNumbersFromTenToOne
|sum|

sum := (10 to: 1 by: -1) sum.

self assert: sum equals: 55.

There are also methods named reverseDo: and reversed on Interval which can take forward Interval and walk through it in reverse. The reversed method will wind up creating an array in reverse, which is why I didn’t demonstrate it here.

Smalltalk Interval — Any Number

The abstraction of Interval and the benefits of dynamic typing really become evident when you learn about the existence of Fraction in Smalltalk. Take the code 1/3 in Smalltalk. If you execute this code in Java for an int, you will expect a result of 0. The following is the result in Pharo Smalltalk.

Dividing Small integer 1 by 3 results in a Fraction of 1/3 in Smalltalk

If we want to represent a range from 1 to 10 by 1/4, we can achieve this simply by writing the following code.

Interval will support any number, so understanding the types of Number that are provided in Smalltalk is helpful. The following class diagram shows the hierarchy for Number in Pharo Smalltalk.

The class hierarchy for Number in Pharo Smalltalk

Smalltalk Interval — More than just a loop

The Interval abstraction provides more than just a lazy for loop. Interval is a lazy Collection. All of the algorithms available to SequencedCollection are also available to Interval. For example, it’s possible to collect all of the numbers in an Interval as a collection of String.

testPrintString
|strings expected|

strings := (1 to: 5) collect: #printString.

expected := Array with: '1' with: '2' with: '3' with: '4' with: '5'.

self assert: strings equals: expected.

The method named collect: is defined on Collection, and can be used to transform from one type to another. Here I am converting SmallInteger to String by applying the method printString to each element of the Interval.

The following code filters the even values of an Interval using the select: method, and then converts the SmallInteger values to their square values and converts them to an array of String.

testEvensPrintString
|strings expected|

strings := ((1 to: 10) select: #even) squared collect: #printString.

expected := Array with: '4' with: '16' with: '36' with: '64' with: '100'.

self assert: strings equals: expected.

Smalltalk — Number meets Collection

Looping with a range of values in Interval is useful but somewhat limited. Looping over a collection of arbitrary values is more useful. There are many methods available on the Collection class that provide internal iterators. I am going to show what some of these methods do, and then explain how they do them. The how is where an eager loop provided by the Number class in a method named to:do: arrives.

The following is the method for do: in OrderedCollection, which is one of the most commonly uses collection types in Smalltalk. OrderedCollection in Smalltalk is the equivalent of ArrayList in Java.

The method do: on OrderedCollections calls to:do: on Number

The method do: is the equivalent of forEach defined on Iterable in Java. In this code, the first part, firstIndex to: lastIndex do: calls the to:do: method on Number. The code for to:do: looks as follows.

The method to:do: on the Number class

This code uses a Block whileTrue: method to execute another Block the number of times covered by the range from self to stop. If we go back to look at the do: method in Interval, it looks somewhat similar, but requires a bit more math due to Interval having a start, stop and step value.

Again, the method do: on the Interval class

The following code looks very similar, but winds up taking two different paths. One goes through Number to:do:, and the other goes through Interval do:. See if you can figure out which is which.

1 to: 10 do: [:each | Transcript show: each].
(1 to: 10) do: [:each | Transcript show: each].

The code Transcript show: is the equivalent of System.out.print() in Java. If you guessed the first line uses Number to:do: and the second uses Interval do:, then you are correct, and I am finished with the Smalltalk part of the Interval, Number, and Collection looping.

3. OO For Loops in Java

Since Java 8, we have IntStream and LongStream, both which can represent lazy for loops over a range of int or long values.

Java IntStream — range sum

The method range on IntStream is inclusive on the from and exclusive on the to.

@Test
public void sumIntStreamRangeOneToElevenExclusive()
{
int sum = IntStream.range(1, 11).sum();
Assertions.assertEquals(55, sum);
}

Java IntStream — rangeClosed sum

The method rangeClosed on IntStream is inclusive on both the from and the to.

@Test
public void sumIntStreamRangeClosedOneToTenInclusive()
{
int sum = IntStream.rangeClosed(1, 10).sum();
Assertions.assertEquals(55, sum);
}

Both calls to sum on IntStream have a potential silent overflow issue to be aware of. It would have been better if IntStream sum returned a long value. So long as your sum result is less than Integer.MAX_VALUE you will be ok. If it is greater than Integer.MAX_VALUE, the int value sum could wind up negative or some other unexpected positive value.

Java IntStream — forEach sum

IntStream is lazy, so you have to call a terminal method like sum or forEach to force iteration to happen. If we want to calculate a sum by hand, we can use the forEach method. With this version of sum, we can widen the result ourselves to long by using LongAdder.

@Test
public void sumIntStreamRangeClosedForEach()
{
LongAdder sum = new LongAdder();
IntStream.rangeClosed(1, 10).forEach(sum::add);
Assertions.assertEquals(55, sum.intValue());
}

I use the LongAdder class to create an instance of an object that will be effectively final and can be used as a method reference in the forEach. LongAdder internally keeps a long value. To illustrate how LongAdder handles larger numbers and avoids int overflow, where sum does not, I will create a small range in the billions.

@Test
public void sumIntStreamRangeClosedInBillions()
{
LongAdder sum = new LongAdder();
IntStream.rangeClosed(2_000_000_000, 2_000_000_001).forEach(sum::add);
Assertions.assertEquals(4_000_000_001L, sum.longValue());

// Overflow happened silently here with IntStream.sum
int intSum = IntStream.rangeClosed(2_000_000_000, 2_000_000_001).sum();
Assertions.assertEquals(-294_967_295, intSum);
}

Java LongStream — sum

Another alternative to IntStream.sum that is almost always safe from overflow is LongStream.sum. The following is an example of sum on LongStream.

@Test
public void sumLongStreamRangeClosedOneToTenInclusive()
{
long sum = LongStream.rangeClosed(1L, 10L).sum();
Assertions.assertEquals(55L, sum);

long bigSum = LongStream.rangeClosed(2_000_000_001L, 2_000_000_010L).sum();
Assertions.assertEquals(20_000_000_055L, bigSum);
}

Java IntStream — toList

If we want the elements of an IntStream to be represented in a List, we have to use the IntStream API to box the stream and convert it to a List<Integer>.

@Test
public void filterIntStreamRangeClosedEvensToList()
{
List<Integer> list = IntStream.rangeClosed(1, 10)
.filter(i -> i % 2 == 0)
.boxed()
.toList();

List<Integer> expected = List.of(2, 4, 6, 8, 10);

Assertions.assertEquals(expected, list);
}

Java Iterable — forEach

In Java 8, we got support for concise lambda expressions and the Java Stream API. We also got a forEach method on the Iterable interface, which allows all Collection types in Java to provide internal iterators that are optimized for each type.

The following code can be used to sum the List<Integer> of even numbers from 1 to 10 that I created above.

@Test
public void sumListOfEvensUsingForEach()
{
List<Integer> list = List.of(2, 4, 6, 8, 10);
LongAdder sum = new LongAdder();
list.forEach(sum::add);

Assertions.assertEquals(30L, sum.longValue());
}

4. OO For Loops in Eclipse Collections

One of the first custom Collection types I created in Eclipse Collections was the Interval class. I though it would be very useful to have a List<Integer> that you could create simply by specifying a range. I also thought it would be useful to have a full complement of rich internal iterators on Interval, so I had it also implement LazyIterable<Integer>. We have used Interval extensively in unit tests in Eclipse Collections. It is often the fastest way to create a List, Set, Bag, Stack or any other type where having some Collection of Integer is all we need. The following image shows the number of usages on Interval in the Eclipse Collections project.

Interval usages in Eclipse Collections Project

There are ~1,900 usages in the tests module alone. The Interval class has proven itself very useful in Eclipse Collections unit tests.

Eclipse Collections Interval — sum

The following code will create an Interval from 1 to 10 and return a sum using sumOfInt. The method sumOfInt knows to widen the sum to a long.

@Test
public void sumIntervalOneToTen()
{
long sum = Interval.oneTo(10).sumOfInt(Integer::intValue);

Assertions.assertEquals(55L, sum);
}

Eclipse Collections Interval — sum evens

The following code will include only the even numbers from 1 to 10 and sum them.

@Test
public void sumIntervalEvensOneToTen()
{
long sum = Interval.evensFromTo(1, 10).sumOfInt(Integer::intValue);

Assertions.assertEquals(30L, sum);
}

Eclipse Collections Interval — as a List and as a LazyIterable

There are three possible types that Interval can be used as — Interval, List, LazyIterable.

@Test
public void intervalIsListAndLazyIterable()
{
Interval interval = Interval.oneTo(5);
List<Integer> list = interval;
LazyIterable<Integer> lazyIterable = interval;

Assertions.assertEquals(List.of(1, 2, 3, 4, 5), list);
Assertions.assertEquals(Set.of(1, 2, 3, 4, 5), interval.toSet());
Assertions.assertEquals(
Interval.oneTo(10),
lazyIterable.concatenate(Interval.fromTo(6, 10)).toList());
}

The following diagram shows the design of the Interval class in Eclipse Collections.

Class diagram for Interval class in Eclipse Collections

Eclipse Collections IntInterval — sum

Interval has proved itself extremely useful for quickly creating List<Integer> instances, especially in test code. For production use cases, where memory and performance matter, IntInterval may be a better alternative. IntInterval is an ImmutableIntList. The internal iterators on IntInterval are not lazy by default like Interval, but IntInterval does support lazy iteration via an explicit call to asLazy.

The following code shows how to calculate a sum using IntInterval using both eager and lazy approaches.

@Test
public void sumIntIntervalOneToTen()
{
IntInterval intInterval = IntInterval.oneTo(10);

long eagerSum = intInterval.sum();

LazyIntIterable lazy = intInterval.asLazy();
long lazySum = lazy.sum();

Assertions.assertEquals(55L, eagerSum);
Assertions.assertEquals(55L, lazySum);
}

I reuse the instance of IntInterval to create the LazyIntIterable, after already calculating the eager sum. I did this to illustrate that an IntInterval can be reused, unlike an IntStream from Java, which may only be used once.

Eclipse Collections IntInterval — usage

The usage of IntInterval in Eclipse Collections is more modest than Interval, but still quite good.

Usages of IntInterval in Eclipse Collections

Eclipse Collections LongInterval — sum

Eclipse Collections provides primitive Interval support for both int and long. The long support is provided by LongInterval.

The following code shows how to calculate a sum using LongInterval using both eager and lazy approaches.

@Test
public void sumLongIntervalBillionOneToBillionTen()
{
LongInterval longInterval =
LongInterval.fromTo(1_000_000_001L, 1_000_000_010L);

long eagerSum = longInterval.sum();

LazyLongIterable lazy = longInterval.asLazy();
long lazySum = lazy.sum();

Assertions.assertEquals(10_000_000_055L, eagerSum);
Assertions.assertEquals(10_000_000_055L, lazySum);
}

I reuse the instance of LongInterval to create the LazyLongIterable, after already calculating the eager sum. I did this to illustrate that a LongInterval can be reused, unlike a LongStream from Java, which may only be used once.

Eclipse Collections LongInterval — usage

The usage of LongInterval in Eclipse Collections is more modest than both Interval and IntInterval.

Usages of LongInterval in Eclipse Collections

Eclipse Collections IntInterval and LongInterval Class Hierarchy

I have included both hierarchies for IntInterval and LongInterval in this diagram to show that they do ultimately share a root parent interface named PrimitiveIterable. The following is the UML class hierarchy for both IntInterval and LongInterval.

Class hierarchies for IntInterval and LongInterval in Eclipse Collections

There was an evolution in design approach from Interval to IntInterval and LongInterval. Interval was one of the earliest containers in Eclipse Collections, and was created before immutable and primitive types were added to the framework. Interval is heavily used already so it is too late to revisit its design as it would cause too much pain to convert it to an ImmutableList<Integer> just for consistency sake. This ship has sailed, and this is now just a historical implementation decision. There are things I continue to like about both design approaches.

Final Thoughts

I hope this blog helped you learn about different approaches and levels of abstraction for looping in an object-oriented language. I believe it is useful to have both language and library constructs available in Java to enhance the productivity of developers when it comes to looping.

Smalltalk takes a novel approach to implementing control flow that I found extremely insightful when I first learned about it in 1994. I hope the Smalltalk examples and explanations helped you learn something insightful about this venerable programming language. I believe there is still so much we can learn from the past in programming languages, and only by learning this storied past can we hope to create a better future for programmers.

I intentionally did not explain either eager parallel or lazy parallel looping in this blog. This can be a natural progression once you have understood how eager serial and lazy serial looping works using lambdas. The inclusion of lambdas in Java 8 has opened up many new possibilities for productivity enhancements for Java developers. I expect that we will continue to see improvements in the language and libraries of Java further leveraging this critical feature.

Thank you for reading!

I am the creator of and committer for the Eclipse Collections OSS project, which is managed at the Eclipse Foundation. Eclipse Collections is open for contributions.

--

--

Donald Raab

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