Is it time for Operator Overloading in Java?

Operator Overloading(OO) is one of those strange language features, that you either love, or loathe. Which is understandable, since misusing OO can very quickly lead to confusing code, and more confusing bugs.

But what is Operator Overloading?

“Operator overloading allows C/C++ operators to have user-defined meanings on user-defined types (classes). Overloaded operators are syntactic sugar for function calls” [1]

Meaning if we define a class Foo, we should also be able to define an implementation for the plus operator such that FooBar = Foo + Bar;

OO is widely considered a trivial language feature. Syntactic sugar is the most frequently used adjective used to describe this phenomenon. Which is true(syntactic sugar part), but is not every programming language feature a syntactic sugar abstraction that allows you to write complex machine code...without actually having to do that?

That being said, let us first look at why OO is so often demonized in our domain.

cout<<

It is said, that the crux of this division is symbolized by the ‘<<’ operator which is so amiably overloaded in the C++ statement:

cout<<"what the #@$#"<<endl;

Which is equivalent to:

System.out.println("what the #@$#");

Here, instead of shifting an object to the left, it is used to pipe a string into cout, the standard out(which is usually the console).

Some languages like C++ allow you to overload some exotic operators such as the comma, which, strangely enough, does have its usages. Other languages allow you to define your own operators. Interesting areas, which unfortunately are beyond the scope of this article.

Overloading exotic operators aside, the fact of the matter is if somebody overloads an operator and you use said operator, then you are no longer guaranteed that the operation that will be executed will indeed adhere to the mathematical purpose of said operator.

Take the example above: FooBar = Foo + Bar;

  • How do you know that the plus operator actually does anything resembling addition?
  • What if it changes either Foo or Bar?
  • Or just plainly return a random FooBar on every execution?
  • What about...side-effects?

All good questions, but are they really unique to OO?

Let’s say we rewrite it to FooBar = Foo.plus(Bar);

Does this really guarantee anything different?

This article could go on for 20 pages about why almost anything harmful we can think of about OO  isn’t much different in the method-sphere, but our intention here is to answer the question of whether Java is ready for OO, and how it can be useful.

Let us start by answering the second question first.

How is Operator Overloading useful?

Take a couple of seconds to look at this piece of code:

final BigDecimal result = a.multiply(x.pow(2)).plus(b.multiply(x.plus(c)));

So what do you think it is?

If your answer is a quadratic equation in standard form, then you are most likely correct.

What would this look like if we had OO though?

final BigDecimal result = a * (x * x) + b * x + c;

Let’s take it up a notch. Say we want to derive X from the quadratic equation so we rewrite the equation as:

What would that look like in BigDecimal?

final BigDecimal sqrt = b.pow(2).min(a.multiply(c).multiply(4)).sqrt();
final BigDecimal x1 = b.negate().plus(sqrt).divide(a.multiply(2));

What about OO?

final BigDecimal sqrt = (b * b - 4 * a * c).sqrt();
final BigDecimal x1 = (-b + sqrt) / (2 * a);

Of course it’s a matter of taste, but I would think most of us would find the OO version more readable, but above all, more writable.

More writable?

That’s a strange thing to say. But the reason we introduce abstractions in code is for both readability and writability. 4*a*c is much easier to write than a.multiply(b).multiply(4). It is also simpler, which leaves less room for bugs.

I hope the esteemed readers will forgive me, but the first example actually has a bug in it that most people;the author included, would overlook.

To give you a hint, let’s rewrite the OO version to also include the bug, and it will almost instantly become visible:

final BigDecimal result = a * (x * x) + b * (x + c);

This is a simple operator precedence bug, that is visible right away in the OO version, but evenly disastrous in both versions of the code.

A more subtle bug that is solved for free when you lean on the compiler to interpret OO for you, is the transitive associativity of your user-defined types.

Example:

class Vector{ float x, y, z; }
final Vector result = vec1 + vec2 + vec3;
final Vector result = vec1.plus(vec2.plus(vec3));

In 999 out of 1000 cases, both these lines of code will produce the same results. But in some cases, the result is different. The reason like we mentioned before, is operator precedence.

What difference does it make whether we do a + b + c or c + b + a you might ask? Aren’t these operations associative?

In the real world, yes. But in the digital world...it depends.

As you can see, the Vector class contains 3 fixed floating point members. The way binary floating point arithmetic works, is that the result of an operation is represented by the closest representable binary value to it. So if a.x + b.x = 0.23548787, the result might be 0.2354877F in our code, because we only have a 32bit wide range of values, and we can’t represent every exact fraction(which technically speaking are infinite).

Applying that information to our code, we now understand that a + b produces an approximate value, which is then added to c, which again produces yet another approximate value.

Meaning that doing these operations in reverse is no longer associative[2], since we basically have a lossy conversion[3]. It just happens to go right 999 out of 1000 times because we use representable fractions, or we don’t care about the loss.

Example:

final float a = 3.3333333f;
final float b = 0.6373606f;
final float c = 0.36263946f;

System.out.println(a + b + c);
//4.3333335
System.out.println(c + b + a);
//4.333333

Naturally you can still get these bugs with OO if you write operations in the wrong order or add parenthesis for fun, but the basic bug of having a closing parenthesis too soon or too late magically disappears.

For correctness, the bug free version of the code is::

final Vector result = vec1.plus(vec2).plus(vec3);

These types of bugs are very common when you have to do any kind of maths without OO. The reason being that functions aren’t really built to be called in this way, and neither do our brains work like that. Sure you can create something similar to a builder pattern that makes it easier, or introduce some more abstractions to hide the gory details, or even write some kind of maths parser. But are those really the problems an engineer should be solving and maintaining?

To quote James Gosling: Everyone I've talked to who does numerical work insists that operator overloading is totally essential. The method invocation style for writing expressions is so cumbersome as to be essentially useless.” [4]

Another interesting fact about operators, is that an operator is expressive of the side-effects it might cause. Meaning that a plus operator by definition adds two operands creating a result without changing either of the operands(unfortunately this isn’t foolproof as most languages allow the programmer to do whatever she wants within an overloaded operator). An obvious example of this is the plus operator that is overloaded for String. A plus method however makes no such assumption, as a method is allowed to do whatever it wants.

This might be marginally interesting for a programmer, but certainly a compiler could perform some magical optimizations knowing that said operator basically performs a read-only operation.

So is Java ready for Operator Overloading?

Proposing this question is really like shooting oneself in the foot, since it’s nearly impossible to answer.

So instead let us talk about needs and benefits.

Interestingly enough the need has inadvertently risen dramatically with the rise of Java applications in the Big Data[5] & Trading[6] realms. Both of which can rely heavily on advanced mathematical formulations.

Yet another interesting development is videogames. The advent of Minecraft(one of the best selling games in history[7]) which was fully written in Java[8][9], signaled that Java can rub elbows with the best of them when it comes to making games. And of course, 3D game engines are nothing but pure algebra and geometry.

When we look at other JVM languages such as Clojure, Groovy, Kotlin or Scala. We see that they all chose to implement OO in one way or another. We believe that this is also a clear signal that the community is open for such a development.

Project Valhalla will provide us Value Types[10]. User Defined “objects” that act and perform like primitive types(no wonder it is called Valhalla).

Which means that we could define our own int64 type for instance, or unsigned_int32. But then having to do maths with our shiny new Value Types through function calls...would be disheartening.

As a side-note, an int64 would fit in a cpu register for certain CPU architectures. Which also means that further JVM optimization could be done to do arithmetic using 64bit instructions. This would be easier if we had OO or some other mechanism to denote that a specific “function” is a mathematical operation of the addition type for example, for 64 bit wide integers.

Every language feature or library out there is basically a double edged sword. Some are clear in the damage they might cause, making it easier to side-step, while others are very subtle and foreign, making them harder.

The reason we introduce best practices, and in some instances even books are written about those(such as Effective Java), is to minimize damage and educate engineers about the as yet unknown. Operator Overloading is no different in that regard. It is a very helpful tool that when used properly, will save us a lot of engineering time spent programming and debugging problems, that our engineers can use to solve the actual problems they are trying to solve.


[1] https://isocpp.org/wiki/faq/operator-overloading

[2] 754-2008 - IEEE Standard for Floating-Point Arithmetic https://ieeexplore.ieee.org/document/4610935

[3] https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

[4] Gosling. The Evolution of Numerical Computing in Java. http://www.javasoft.com/people/jag/FP.html

[5] https://www.infoworld.com/article/2845147/big-data-sparks-interest-in-statistical-programming-languages.html

[6] https://dzone.com/articles/c-or-java-which-faster-high

[7] https://www.pcgamesn.com/minecraft/minecraft-player-count

[8] https://www.minecraft.net/en-us/store/minecraft-java-edition/

[9] https://www.java.com/en/download/faq/minecraft.xml

[10] https://openjdk.java.net/projects/valhalla/