What if Java had no for?
These loops look like objects to me!
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 anIntInterval
from Eclipse Collections to represent an OO version of thefor
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 anIterator
.
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.
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.
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 ^
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.
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:
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.
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.
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.
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:
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.
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.
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.
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.
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.
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
.
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
.
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.