What if Java had no if?
What would you do?
Where art thou Control Flow?
Programming in Java would be pretty hard in Java if we didn’t have if
statements, for
loops, while
statements. These are convenient language artifacts that help us determine if, how, and when the code in our programs execute. This is the essence of control flow in a program. All programming languages have mechanisms that enable control flow to occur. Most programming languages provide control flow mechanisms in the language itself, via built-in statements.
The following code shows some basic control flow statements in Java.
public static void main(String[] args)
{
if (args.length > 1)
{
int i = Integer.parseInt(args[1]);
for (int j = 0; j < i; j++)
{
System.out.println(args[0]);
}
}
else if (args.length > 0)
{
System.out.println(args[0]);
}
else
{
System.out.println("Hello World!");
}
}
This code checks the args
String
array length
to see if zero, one, or two arguments are supplied to the program. If zero arguments are passed, the program outputs “Hello World!”
. If one argument is passed, the program outputs the argument which is arg[0]
. If two arguments are passed, the program outputs the first argument (arg[0]
) the number of times specified in the second argument (arg[1]
). There is no safety check in this code to make sure the second parameter is actually a number.
What would you do if you didn’t have an
if
statement in Java?
Modeling Control Flow with Objects and Methods
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. When I was first learning Smalltalk, I had to learn where these control flow methods were and how to use them with lambdas. I believe this enabled me to become familiar with lambdas very quickly, as I had to use them everywhere I needed control structures in my Smalltalk programs.
The place I first learned to look for control structures in Smalltalk was the class hierarchy for Boolean
. The following is a UML class diagram showing how the Boolean
type is modeled in Smalltalk.
The Boolean class in Smalltalk defines methods and:
, or:
, ifTrue:
, ifFalse:
, ifTrue:ifFalse:
, and ifFalse:ifTrue
. Each of these methods take one or two Block
parameters. A Block
in Smalltalk is a type that can be represented with a literal lambda syntax. The basic syntax for a Block
is square brackets with a pipe that separates parameters on the left, with expression on the right. If there are zero parameters in the Block
, there will be no pipe. The following are examples of literal blocks, also known as lambdas or closures.
[]
— An emptyBlock
which returnsnil
when evaluated.[true]
— Zero argumentBlock
which returnstrue
when evaluated[:a | a]
— One argumentBlock
which returnsa
when evaluated[:a :b | a + b]
— Two argumentBlock
which returnsa + b
[:a :b :c | a + b + c]
— Three argumentBlock
which returnsa + b + c
when evaluated.
Evaluating Conditionals in Smalltalk
Without an if
statement in Smalltalk, you learn to use the instances (true
and false
) of the two subclasses of Boolean
(True
and False
) with lambdas (via Block
) to perform conditional logic.
The following tests in Smalltalk show how conditional logic can be accomplished using the methods on the True
and False
classes that I documented in the diagram above.
Here is a test demonstrating various results for True
.
testTrue
self assert: (true and: [ true ]).
self assert: (true or: [ true ]).
self deny: (true and: [ false ]).
self assert: (true or: [ false ]).
self assert: (true ifTrue: [ true ]).
self assert: (true ifTrue: [ true ] ifFalse: [ false ]).
self deny: (true ifFalse: [ true ] ifTrue: [ false ]).
self assert: (6 > 5 ifTrue: [ true ]).
self assert: (4 > 5 ifTrue: [ true ]) equals: nil.
Here is a test demonstrating various results for False
.
testFalse
self deny: (false and: [ true ]).
self assert: (false or: [ true ]).
self deny: (false and: [ false ]).
self deny: (false or: [ false ]).
self assert: (false ifFalse: [ true ]).
self assert: (false ifTrue: [ false ] ifFalse: [ true ]).
self deny: (false ifFalse: [ false ] ifTrue: [ true ]).
self assert: (6 > 5 ifFalse: [ true ]) equals: nil.
self assert: (4 > 5 ifFalse: [ true ]).
Passive vs. Active Boolean Class
Java has both a primitive and Object
form of boolean
. The primitive version is named boolean
. The object version is named Boolean
. The Boolean
class acts as a wrapper for the primitive boolean
type in Java so that the primitive values can be used in generic collections like List
, Set
, and Map
. The Boolean
class only defines six instance methods as of Java 21. The methods are toString
, hashCode
, equals
, compareTo
, describeConstable
, and booleanValue
. This class contains no active methods that do anything. Most of the methods return a different type representation of the booleanValue
contained in the wrapper. The current Boolean
class in Java is what I would refer to as a passive class. It is merely an object data holder for primitive boolean
.
It is possible to make Boolean
an active class in Java. As an experiment I defined a new Boolean
sealed interface and defined True
and False
implementations.
public sealed interface Boolean permits Boolean.True, Boolean.False
{
Boolean TRUE = new True();
Boolean FALSE = new False();
static Boolean valueOf(boolean value)
{
return value ? TRUE : FALSE;
}
default Boolean and(Supplier<Boolean> block)
{
return null;
}
default Boolean or(Supplier<Boolean> block)
{
return null;
}
default <R> R ifTrue(Supplier<R> trueBlock)
{
return null;
}
default <R> R ifFalse(Supplier<R> falseBlock)
{
return null;
}
default <R> R ifTrueIfFalse(
Supplier<R> trueBlock,
Supplier<R> falseBlock)
{
return null;
}
default <R> R ifFalseIfTrue(
Supplier<R> falseBlock,
Supplier<R> trueBlock)
{
return null;
}
final class True implements Boolean {}
final class False implements Boolean {}
}
I had to provide a static method to convert a primitive boolean
to the Boolean
interface for this to work. I will leave it to your imagination how I overrode the default implementations of the parent Boolean
interface in the True
and False
classes.
When I had completed the implementation, I rewrote the Control Flow Java example in the first section of this blog using the new Boolean
interface. This is what the code looks like with Supplier
and Boolean
instances in variables to provide clarity.
public static void main(final String[] args)
{
Supplier<Object> moreThanOneSupplier = () ->
{
IntInterval.oneTo(Integer.parseInt(args[1]))
.forEach(j -> System.out.println(args[0]));
return null;
};
Supplier<Object> moreThanZeroSupplier = () ->
{
System.out.println(args[0]);
return null;
};
Supplier<Object> noArgumentSupplier = () ->
{
System.out.println("Hello World!");
return null;
};
Boolean argsGreaterThanOne = Boolean.valueOf(args.length > 1);
Boolean argsGreaterThanZero = Boolean.valueOf(args.length > 0);
argsGreaterThanOne.ifTrueIfFalse(
moreThanOneSupplier,
() -> argsGreaterThanZero.ifTrueIfFalse(
moreThanZeroSupplier,
noArgumentSupplier));
}
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. The active Boolean
version of the code is composable and easier to move logic around with everything clearly compartmentalized. If I inline the Supplier
instances, the code looks as follows.
public static void main(final String[] args)
{
Boolean argsGreaterThanOne = Boolean.valueOf(args.length > 1);
Boolean argsGreaterThanZero = Boolean.valueOf(args.length > 0);
argsGreaterThanOne.ifTrueIfFalse(
() ->
{
IntInterval.oneTo(Integer.parseInt(args[1]))
.forEach(j -> System.out.println(args[0]));
return null;
},
() -> argsGreaterThanZero.ifTrueIfFalse(
() ->
{
System.out.println(args[0]);
return null;
},
() ->
{
System.out.println("Hello World!");
return null;
}));
}
For less scrolling and easier comparison, this was the original Java code from above with if
statements and the for
loop.
public static void main(String[] args)
{
if (args.length > 1)
{
int i = Integer.parseInt(args[1]);
for (int j = 0; j < i; j++)
{
System.out.println(args[0]);
}
}
else if (args.length > 0)
{
System.out.println(args[0]);
}
else
{
System.out.println("Hello World!");
}
}
The code in the primitive boolean
example with if
statements is less verbose, but would require more copying and pasting to move logic around.
The verbosity using the active Boolean interface is caused partially because I chose to use Supplier
, which more closely models how Smalltalk uses its Block
with Boolean
. Smalltalk Boolean
methods with Block
allow for Boolean
expressions to be formed as results from methods. If I don’t care about returning a value from a Boolean
expression, I could model the methods using Runnable
.
The following code shows how code would look if I used Runnable
instead.
public static void main(final String[] args)
{
Boolean argsGreaterThanOne = Boolean.valueOf(args.length > 1);
Boolean argsGreaterThanZero = Boolean.valueOf(args.length > 0);
argsGreaterThanOne.ifTrueIfFalse(
() -> IntInterval.oneTo(Integer.parseInt(args[1]))
.forEach(j -> System.out.println(args[0])),
() -> argsGreaterThanZero.ifTrueIfFalse(
() -> System.out.println(args[0]),
() -> System.out.println("Hello World!")));
}
The curly braces all disappear because each branch of the if
statement can be covered by a single line of code. Lambdas in Java allow me to remove the curly braces for single line lambda expressions. This removes a bunch of unnecessary lines of code, at the cost of some potential readability due to text compression and loss of natural whitespace.
If I extract the Runnable
instances into their own variables, the code will look as follows.
public static void main(final String[] args)
{
Boolean argsGreaterThanOne = Boolean.valueOf(args.length > 1);
Boolean argsGreaterThanZero = Boolean.valueOf(args.length > 0);
Runnable moreThanOneRunnable = () ->
IntInterval.oneTo(Integer.parseInt(args[1]))
.forEach(j -> System.out.println(args[0]));
Runnable moreThanZeroRunnable = () -> System.out.println(args[0]);
Runnable noArgumentRunnable = () -> System.out.println("Hello World!");
argsGreaterThanOne.ifTrueIfFalse(
moreThanOneRunnable,
() -> argsGreaterThanZero.ifTrueIfFalse(
moreThanZeroRunnable,
noArgumentRunnable));
}
Final thoughts
This blog was intended to explain in simple terms the differences between true
and false
in Java, and true
and false
in Smalltalk. Java uses statements provided by the language for user management of program control flow. Smalltalk uses Objects, Methods and Lambdas to accomplish the same. Both approaches have pros and cons. Composability and verbosity are sometimes at odds with each other. If we extract methods in the branches of logic, we can achieve better composability and less verbosity with both approaches.
The active Boolean
approach I demonstrated and described here could be added to the Boolean
class in Java to make it an active Boolean
object. This would enable the Boolean
class to manage control flow through methods. The benefit of this approach would be to enable more complex if
expressions which may be hard to emulate and make readable with the current Java mechanism of ternary expressions.
Update: One downside that makes the active Boolean
object approach impractical in Java is if you need access to any local variables outside of the lambdas used in the conditionals. This would require messy tricks using final arrays to potentially allow for updates to local variables outside of the lambda scopes. The if
statement approach has access to any variables defined outside of its conditional scopes. Another downside is that Java lambdas do not have support for non-local returns, which would limit returning out of the method from the conditionals. Thanks to Oleg Pliss for pointing out these important difference between Java Lambdas and Smalltalk blocks on LinkedIn.
Learning multiple languages that use different strategies to address the same problems is useful. Learning a whole new language takes time. My hope is that this bitesize comparison of basic control flow in Java and Smalltalk is useful for you to be able to understand the pros and cons different approaches.
Thanks 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.