DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Java String: A Complete Guide With Examples
  • Mule 3 DataWeave(1.x) Script To Resolve Wildcard Dynamically
  • Import a Function/Module in Dataweave
  • Deploying Artemis Broker With SSL Enabled and Use AMQP

Trending

  • Next-Gen IoT Performance Depends on Advanced Power Management ICs
  • Medallion Architecture: Why You Need It and How To Implement It With ClickHouse
  • How to Write for DZone Publications: Trend Reports and Refcards
  • Revolutionizing Financial Monitoring: Building a Team Dashboard With OpenObserve
  1. DZone
  2. Coding
  3. Java
  4. Operator Overloading in Java

Operator Overloading in Java

Write expressions like (myBigDecimalMap[ObjectKey] * 5 > 20) in Java... Manifold makes that happen. Expressions like "5 mph * 3 hr" produces distance!

By 
Shai Almog user avatar
Shai Almog
DZone Core CORE ·
May. 31, 23 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
4.9K Views

Join the DZone community and get the full member experience.

Join For Free

In this post, we'll delve into the fascinating world of operator overloading in Java. Although Java doesn't natively support operator overloading, we'll discover how Manifold can extend Java with that functionality. We'll explore its benefits, limitations, and use cases, particularly in scientific and mathematical code.

We will also explore three powerful features provided by Manifold that enhance the default Java-type safety while enabling impressive programming techniques. We'll discuss unit expressions, type-safe reflection coding, and fixing methods like equals during compilation. Additionally, we'll touch upon a solution that Manifold offers to address some limitations of the var keyword. Let's dive in!


Before we begin, as always, you can find the code examples for this post and other videos in this series on my GitHub page. Be sure to check out the project, give it a star, and follow me on GitHub to stay updated!

Arithmetic Operators

Operator overloading allows us to use familiar mathematical notation in code, making it more expressive and intuitive. While Java doesn't support operator overloading by default, Manifold provides a solution to this limitation.

To demonstrate, let's start with a simple Vector class that performs vector arithmetic operations. In standard Java code, we define variables, accept them in the constructor, and implement methods like plus for vector addition. However, this approach can be verbose and less readable.

Java
 
public class Vec {
   private float x, y, z;

   public Vec(float x, float y, float z) {
       this.x = x;
       this.y = y;
       this.z = z;
   }

   public Vec plus(Vec other) {
       return new Vec(x + other.x, y + other.y, z + other.z);
   }
}


With Manifold, we can simplify the code significantly. Using Manifold's operator overloading features, we can directly add vectors together using the + operator as such:

Java
 
Vec vec1 = new Vec(1, 2, 3);
Vec vec2 = new Vec(1, 1, 1);
Vec vec3 = vec1 + vec2;


Manifold seamlessly maps the operator to the appropriate method invocation, making the code cleaner and more concise. This fluid syntax resembles mathematical notation, enhancing code readability.

Moreover, Manifold handles reverse notation gracefully. Suppose we reverse the order of the operands, such as a scalar plus a vector, Manifold swaps the order and performs the operation correctly. This flexibility enables us to write code in a more natural and intuitive manner.

Let’s say we add this to the Vec class:

Java
 
public Vec plus(float other) {
    return new Vec(x + other, y + other, z + other);
}


This will make all these lines valid:

Java
 
vec3 += 5.0f;
vec3 = 5.0f + vec3;
vec3 = vec3 + 5.0f;
vec3 += Float.valueOf(5.0f);


In this code, we demonstrate that Manifold can swap the order to invoke Vec.plus(float) seamlessly. We also show that the plus equals operator support is built into the plus method support

As implied by the previous code, Manifold also supports primitive wrapper objects, specifically in the context of autoboxing. In Java, primitive types have corresponding wrapper objects. Manifold handles the conversion between primitives and their wrapper objects seamlessly, thanks to autoboxing and unboxing. This enables us to work with objects and primitives interchangeably in our code. There are caveats to this, as we will find out.

BigDecimal Support

Manifold goes beyond simple arithmetic and supports more complex scenarios. For example, the manifold-science dependency includes built-in support for BigDecimal arithmetic. BigDecimal is a Java class used for precise calculations involving large numbers or financial computations? By using Manifold, we can perform arithmetic operations with BigDecimal objects using familiar operators, such as +, -, *, and /. Manifold's integration with BigDecimal simplifies code and ensures accurate calculations.

The following code is legal once we add the right set of dependencies, which add method extensions to the BigDecimal class:

Java
 
var x = new BigDecimal(5L);
var y = new BigDecimal(25L);
var z = x + y;


Under the hood, Manifold adds the applicable plus, minus, times, etc. methods to the class. It does so by leveraging class extensions which I discussed before.

Limits of Boxing

We can also extend existing classes to support operator overloading. Manifold allows us to extend classes and add methods that accept custom types or perform specific operations. For instance, we can extend the Integer class and add a plus method that accepts BigDecimal as an argument and returns a BigDecimal result. This extension enables us to perform arithmetic operations between different types seamlessly. The goal is to get this code to compile:

Java
 
var z = 5 + x + y;


Unfortunately, this won’t compile with that change. The number five is a primitive, not an Integer, and the only way to get that code to work would be:

Java
 
var z = Integer.valueOf(5) + x + y;


This isn’t what we want. However, there’s a simple solution. We can create an extension to BigDecimal itself and rely on the fact that the order can be swapped seamlessly. This means that this simple extension can support the 5 + x + y expression without a change:

Java
 
@Extension
public class BigDecimalExt {
    public static BigDecimal plus(@This BigDecimal b, int i) {
        return b.plus(BigDecimal.valueOf(i));
    }
}


List of Arithmetic Operators

So far, we focused on the plus operator, but Manifold supports a wide range of operators. The following table lists the method name and the operators it supports:

Operator Method
+ , += plus
-, -= minus
*, *= times
/, /= div
%, %= rem
-a unaryMinus
++ inc
-- dec

Notice that the increment and decrement operators don’t have a distinction between the prefix and postfix positioning. Both a++ and ++a would lead to the inc method.

Index Operator

The support for the index operator took me completely off guard when I looked at it. This is a complete game-changer… The index operator is the square brackets we use to get an array value by index. To give you a sense of what I’m talking about, this is valid code in Manifold:

Java
 
var list = List.of("A", "B", "C");
var v = list[0];


In this case, v will be “A” and the code is the equivalent of invoking list.get(0). The index operators seamlessly map to get and set methods. We can do assignments as well using the following:

Java
 
var list = new ArrayList<>(List.of("A", "B", "C"));
var v = list[0];
list[0] = "1";


Notice I had to wrap the List in an ArrayList since List.of() returns an unmodifiable List. But this isn’t the part I’m reeling about. That code is “nice.” This code is absolutely amazing:

Java
 
var map = new HashMap<>(Map.of("Key", "Value"));
var key = map["Key"];
map["Key"] = "New Value";


Yes!

You’re reading valid code in Manifold. An index operator is used to lookup in a map. Notice that a map has a put() method and not a set method. That’s an annoying inconsistency that Manifold fixed with an extension method. We can then use an object to look up within a map using the operator.

Relational and Equality Operators

We still have a lot to cover… Can we write code like this (referring to the Vec object from before):

Java
 
if(vec3 > vec2) {
    // …
}


This won’t compile by default. However, if we add the Comparable interface to the Vec class this will work as expected:

Java
 
public class Vec implements Comparable<Vec> {
    // …

    public double magnitude() {
        return Math.sqrt(x  x + y  y + z * z);
    }

    @Override
    public int compareTo(Vec o) {
        return Double.compare(magnitude(), o.magnitude());
    }
}


These >=, >, <, <= comparison operators will work exactly as expected by invoking the compareTo method. But there’s a big problem. You will notice that the == and != operators are missing from this list. In Java, we often use these operators to perform pointer comparisons. This makes a lot of sense in terms of performance. We wouldn’t want to change something so inherent in Java. To avoid that, Manifold doesn’t override these operators by default.

However, we can implement the ComparableUsing interface, which is a sub-interface of the Comparable interface. Once we do that the == and != will use the equals method by default. We can override that behavior by overriding the method equalityMode() which can return one of these values:

  • CompareTo — will use the compareTo method for == and !=
  • Equals (the default) — will use the equals method
  • Identity — will use pointer comparison as is the norm in Java

That interface also lets us override the compareToUsing(T, Operator) method. This is similar to the compareTo method but lets us create operator-specific behavior, which might be important in some edge cases.

Unit Expressions for Scientific Coding

Notice that Unit expressions are experimental in Manifold. But they are one of the most interesting applications of operator overloading in this context.

Unit expressions are a new type of operator that significantly simplifies and enhances scientific coding while enforcing strong typing. With unit expressions, we can define notations for mathematical expressions that incorporate unit types. This brings a new level of clarity and types of safety to scientific calculations.

For example, consider a distance calculation where speed is defined as 100 miles per hour. By multiplying the speed (miles per hour) by the time (hours), we can obtain the distance as such:

Java
 
Length distance = 100 mph * 3 hr;
Force force = 5kg * 9.807 m/s/s;
if(force == 49.035 N) {
    // true
}


The unit expressions allow us to express numeric values (or variables) along with their associated units. The compiler checks the compatibility of units, preventing incompatible conversions and ensuring accurate calculations. This feature streamlines scientific code and enables powerful calculations with ease.

Under the hood, a unit expression is just a conversion call. The expression 100 mph is converted to:

Java
 
VelocityUnit.postfixBind(Integer.valueOf(100))


This expression returns a Velocity object. The expression 3 hr is similarly bound to the postfix method and returns a Time object. At this point, the Manifold Velocity class has a times method, which, as you recall, is an operator, and it’s invoked on both results:

Java
 
public Length times( Time t ) {
    return new Length( toBaseNumber() * t.toBaseNumber(), LengthUnit.BASE, getDisplayUnit().getLengthUnit() );
}


Notice that the class has multiple overloaded versions of the times method that accept different object types. A Velocity times Mass will produce Momentum. A Velocity times Force results in Power.

Many units are supported as part of this package even in this early experimental stage, check them out here.

You might notice a big omission here: Currency. I would love to have something like:

Java
 
var sum = 50 USD + 70 EUR;


If you look at that code, the problem should be apparent. We need an exchange rate. This makes no sense without exchange rates and possibly conversion costs. The complexities of financial calculations don’t translate as nicely to the current state of the code. I suspect that this is the reason this is still experimental. I’m very curious to see how something like this can be solved elegantly.

Pitfalls of Operator Overloading

While Manifold provides powerful operator overloading capabilities, it's important to be mindful of potential challenges and performance considerations. Manifold's approach can lead to additional method calls and object allocations, which may impact performance, especially in performance-critical environments. It's crucial to consider optimization techniques, such as reducing unnecessary method calls and object allocations, to ensure efficient code execution.

Let’s look at this code:

Java
 
var n = x + y + z;


On the surface, it can seem efficient and short. It physically translates to this code:

Java
 
var n = x.plus(y).plus(z);


This is still hard to spot but notice that in order to create the result, we invoke two methods and allocate at least two objects. A more efficient approach would be:

Java
 
var n = x.plus(y, z);


This is an optimization we often do for high-performance matrix calculations. You need to be mindful of this and understand what the operator is doing under the hood if performance is important. I don’t want to imply that operators are inherently slower. In fact, they’re as fast as a method invocation, but sometimes the specific method invoked and the number of allocations are unintuitive.

Type Safety Features

The following aren’t related to operator overloading, but they were a part of the second video, so I feel they make sense as part of a wide-sweeping discussion on type safety. One of my favorite things about Manifold is its support of strict typing and compile time errors. To me, both represent the core spirit of Java.

JailBreak: Type-Safe Reflection

@JailBreak is a feature that grants access to the private state within a class. While it may sound bad, @JailBreak offers a better alternative to using traditional reflection to access private variables. By jailbreaking a class, we can access its private state seamlessly, with the compiler still performing type checks. In that sense, it’s the lesser of two evils. If you’re going to do something terrible (accessing private state), then at least have it checked by the compiler.

In the following code, the value array is private to String, yet we can manipulate it thanks to the @JailBreak annotation. This code will print “Ex0osed…”:

Java
 
@Jailbreak String exposedString = "Exposed...";
exposedString.value[2] = '0';
System.out.println(exposedString);


JailBreak can be applied to static fields and methods as well. However, accessing static members requires assigning null to the variable, which may seem counterintuitive. Nonetheless, this feature provides a more controlled and type-safe approach to accessing the internal state, minimizing the risks associated with using reflection.

Java
 
@Jailbreak String str = null;
str.isASCII(new byte[] { 111, (byte)222 });


Finally, all objects in Manifold are injected with a jailbreak() method. This method can be used like this (notice that fastTime is a private field):

Java
 
Date d = new Date();
long t = d.jailbreak().fastTime;


Self Annotation: Enforcing Method Parameter Type

In Java, certain APIs accept objects as parameters, even when a more specific type could be used. This can lead to potential issues and errors at runtime. However, Manifold introduces the @Self annotation, which helps enforce the type of object passed as a parameter.

By annotating the parameter with @Self, we explicitly state that only the specified object type is accepted. This ensures type safety and prevents the accidental use of incompatible types. With this annotation, the compiler catches such errors during development, reducing the likelihood of encountering issues in production.

Let’s look at the MySizeClass from my previous posts:

Java
 
public class MySizeClass {
    int size = 5;

    public int size() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public boolean equals(@Self Object o) {
        return o != null && ((MySizeClass)o).size == size;
    }
}


Notice I added an equals method and annotated the argument with Self. If I remove the Self annotation, this code will compile:

Java
 
var size = new MySizeClass();
size.equals("");
size.equals(new MySizeClass());


With the @Self annotation, the string comparison will fail during compilation.

Auto Keyword: A Stronger Alternative to Var

I’m not a huge fan of the var keyword. I feel it didn’t simplify much, and the price is coding to an implementation instead of to an interface. I understand why the devs at Oracle chose this path. Conservative decisions are the main reason I find Java so appealing. Manifold has the benefit of working outside of those constraints, and it offers a more powerful alternative called auto. auto can be used in fields and method return values, making it more flexible than var. It provides a concise and expressive way to define variables without sacrificing type safety.

Auto is particularly useful when working with tuples, a feature not yet discussed in this post. It allows for elegant and concise code, enhancing readability and maintainability. You can effectively use auto as a drop-in replacement for var.

Finally

Operator overloading with Manifold brings expressive and intuitive mathematical notation to Java, enhancing code readability and simplicity. While Java doesn't natively support operator overloading, Manifold empowers developers to achieve similar functionality and use familiar operators in their code. By leveraging Manifold, we can write more fluid and expressive code, particularly in scientific, mathematical, and financial applications.

The type of safety enhancements in Manifold makes Java more… Well, “Java-like.” It lets Java developers build upon the strong foundation of the language and embrace a more expressive type-safe programming paradigm.

Should we add operator overloading to Java itself?

I'm not in favor. I love that Java is slow, steady, and conservative. I also love that Manifold is bold and adventurous. That way, I can pick it when I'm doing a project where this approach makes sense (e.g., a startup project) but pick standard conservative Java for an enterprise project.

Java (programming language) Operator (extension)

Published at DZone with permission of Shai Almog, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Java String: A Complete Guide With Examples
  • Mule 3 DataWeave(1.x) Script To Resolve Wildcard Dynamically
  • Import a Function/Module in Dataweave
  • Deploying Artemis Broker With SSL Enabled and Use AMQP

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!