What if Java had no if?

Donald Raab
8 min readJan 7, 2024

--

What would you do?

Photo by Jon Tyson on Unsplash

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 class hierarchy for Boolean in Smalltalk and literal instances of true and false

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 empty Block which returns nil when evaluated.
  • [true] — Zero argument Block which returns true when evaluated
  • [:a | a] — One argument Block which returns a when evaluated
  • [:a :b | a + b] — Two argument Block which returns a + b
  • [:a :b :c | a + b + c] — Three argument Block which returns a + 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.

--

--

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.