×
Community Blog Java's Missing Feature: Operator Overloading

Java's Missing Feature: Operator Overloading

This article explains the operator overloading, why we need it, and how to implement it in Java.

By Mi Zhou (Zhiye)

1

What Is Operator Overloading?

Operator overloading aims to redefine an operator that has been defined and has certain functions to complete more detailed and specific operations and other functions. From an object-oriented perspective, it means an operator can be defined as a method of a class, so the function of the operator can be used to represent a certain behavior of the object.

Why Is Operator Overloading Required?

Let's consider the realization of such a function. The complete square difference formula (a^2 + 2ab + b^2) is implemented by BigInteger.

private static final BigInteger BI_2 = BigInteger.valueOf(2);

Standard Syntax:

BigInteger res = a.multiply(a).subtract(BI_2.multiply(a).multiply(b)).add(b.multiply(b));

If you can overload the *, +, and - operators in Java, you can write the following code:

BigInteger res = a * a - BI_2 * a * b + b * b;

There are at least two benefits to being able to perform operator overloading for numeric operations of non-primitive types.

  1. The code is simpler to write and less error-prone.
  2. The code is easier to read without many parentheses.

How to Implement Operator Overloading in Java

The implementation of operator overloading in Java still uses Manifold. Manifold allows you to overload Java operators in various scenarios, such as arithmetic operators (including +,-, *, /, and %), comparison operators (>, >=, <, <=, ==, and !=), and index operators ([]). Please see Java's Missing Feature: Extension Methods for more information about the integration of Manifold.

Arithmetic Operator

Manifold is a function that maps each overload of an arithmetic operator to a specific name. For example, if you define a plus(B) method in class A, that class can be called using a + b instead of a.plus(b). The following chart describes the mappings:

Operator Method Call
c = a + b c = a.plus(b)
c = a - b c = a.minus(b)
c = a * b c = a.times(b)
c = a / b c = a.div(b)
c = a % b c = a.rem(b)

Those familiar with Kotlin should know that this is an imitation of Kotlin's operator overloading.

Let's define a numeric Num to facilitate illustration.

public class Num {

    private final int v;

    public Num(int v) {
        this.v = v;
    }

    public Num plus(Num that) {
        return new Num(this.v + that.v);
    }

    public Num minus(Num that) {
        return new Num(this.v - that.v);
    }

    public Num times(Num that) {
        return new Num(this.v * that.v);
    }
}

For the following code:

Num a = new Num(1);
Num b = new Num(2);

Num c = a + b - a;

After Manifold processing at compile time, it becomes:

2

Operators have precedence on the mathematical operation, and Manifold supports it. So, for code like this:

Num c = a + a * b - b;

After Manifold processing, it is:

3

Since Java supports method overloading, you can receive multiple types of parameters for the plus method.

public class Num {

    ...

    public Num plus(Num that) {
        return new Num(this.v + that.v);
    }

    public Num plus(int i) {
        return new Num(v + i);
    }
}

This enhances the ability to overload operators.

Num c = a + 1 + b;

After Manifold processing:

4

Note: Since + and * satisfy the commutative law, a + b will first look for the conforming plus method in object A. If it exists in A, a.plus(b) will be executed. If it does not exist in a and the conforming plus method exists in b, b.plus(a) is executed. a * b is the same.

Java supports the compound assignment of values in primitive types (such as += and -=). Manifold also supports this.

Operator Method Call
a += b a = a.plus(b)
a -= b a = a.minus(b)
a *= b a = a.times(b)
a /= b a = a.div(b)
a %= b a = a.rem(b)

What if it is an existing library and cannot add these methods to its classes? Don't forget that Manifold supports extension methods.

Comparison Operators

For non-primitive types of Java objects, we use Comparable<T> to compare sizes. If your object implements Comparable, Manifold gives you the overload of the four comparison operators >, >=, <, and <=.

Operator Method Call
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0
a < b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0

We let Num achieve Comparable<Num>.

public class Num implements Comparable<Num> {

    ...
    
    @Override
    public int compareTo(Num that) {
        return this.v - that.v;
    }
}

So, for code like this:

Num a = new Num(1);
Num b = new Num(2);

if (a > b) {
    System.out.println("a > b");
}

if (a < b) {
    System.out.println("a < b");
}

Run the code, and it will output a < b since after being processed by Manifold, the code will become:

5

You may ask about == and !=. Does Manifold support them? Yes. Manifold provides a new interface (ComparableUsing<T>) that allows you to implement the overloading of == and !=.

6

ComparableUsing<T> inherits Comparable<T> interface and adds two methods: compareToUsing and equalityMode. View the default implementation of comparableUsing:

7

The overloading of the four operators >, >=, <, and <= uses compareTo of Comparable<T> to implement it. For == and !=, they are based on the return value of the equalityMode method to choose which implementation to use.

8

  • If it is EqualityMode.CompareTo, the overloading of == and != corresponds to the case where the return value of the compareTo method is 0 and non-0, respectively.
  • If it is EqualityMode.Equals, the overloading of == and != corresponds to the case where the return value of the equals method is true and false.
  • If it is EqualityMode.Identity, Java's default implementation is used, such as whether the reference addresses of the comparison objects are the same.

The equalityMode default method returns a value of EqualityMode.Equals, which means Manifold uses equals methods by default to judge == and !=. You can implement your compareUsing methods directly and handle the comparison logic of various Operator without using Manifold's equalityMode logic.

Let's let Num implement the ComparableUsing<Num> interface and override equals.

public class Num implements ComparableUsing<Num> {

    ...
    
    @Override
    public int compareTo(Num that) {
        return this.v - that.v;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) { return true; }

        if (obj instanceof Num) {
            Num that = (Num) obj;
            return this.v == that.v;
        }

        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(v);
    }
}

At this time, == and != are overloaded and use an implementation based on the equals method. So, for the following code:

Num a = new Num(1);
Num b = new Num(1);

if (a == b) {
    System.out.println("a == b");
}

if (a != b) {
    System.out.println("a != b");
}

Run the code, and it will print a == b since the code after Manifold processing becomes:

9

Amazing! We finally realized this. Let == and != use the logic of the equals method to compare rather than using reference addresses.

You should have noticed that if a type T wants to implement ComparableUsing<T>, the T must be Comparable<T>. If you want to overload == and != for T, the T is required to be comparable. Manifold does this instead of providing a separate interface for overloading == and != because the author currently believes that using == and != instead of equals does more harm than good. After all, using equals to compare whether two objects are equal is too popular in Java. So, the author of Manifold hopes we will only use == and != for objects (such as values and quantifiers). Do not abuse it.

What if it is an existing library (such as String and BigInteger) that cannot directly add interface implementations to its classes? You can create an extension class for this class, let the extension class implement ComparableUsing<T>, and Manifold will handle it as the class has implemented ComparableUsing<T>. For example, Manifold for BigInteger extension class ManBigIntegerExt (located in the manifest-science library):

10

It provides a compareUsing implementation of custom logic in the form of extension methods.

11

Note: The extension class should be decorated with the abstract keyword at this time because it is not intended to implement the ComparableUsing<T> interface in a normal way. Alternatively, you can declare an extension class as an interface and inherit ComparableUsing<T> interface.

Index Operators

Java supports index operators for arrays. For example, nums[i] access the element with the array index i, and nums[i] = n assign values to the position with the array index i. However, Java cannot support List and Map. So, here comes Manifold again.

Operator Method Call
c = a[b] c = a.get(b)
a[b] = c a.set(b, c)

Since java.util.List has these two methods. With Manifold, you can write code like this:

12

Map only has the get methods and no set methods, so you can add a set to the Map extension class.

@Extension
public class MapExt {

    public static <K, V> V set(@This Map<K, V> map, K key, V value) {
        return map.put(key, value);
    }
}

Then, we can write the code like this:

13

Amazing! It should be noted that Manifold has requirements for the set methods: the return value of the set method cannot be void, and it should return a value of the same type as the second parameter (usually the old value). The reason for this requirement is to be consistent with the index assignment expression of Java's array (if set returns void, the index assignment expression cannot be supported). In Java, you can assign a value like this:

int[] nums = {1, 2, 3};
int value = nums[0] = 10;

After the execution is completed, the num[0] and value will be 10. So, when we use index assignment expressions:

List<String> list = Arrays.asList("a", "b", "c");
String value = list[0] = "A";

After Manifold processing, the code becomes:

14

Thus, similar to the T value = list[0] = obj expression, the value after execution is not the return value of the set method but the rightmost value:

Unit Operator

Manifold also provides an interesting feature: unit operator. As the name implies, we can provide the unit function in the code. For example, the following code:

15

Stunned? Me too. The dt is the unit. Look at the code after Manifold processing:

16

In other words, Manifold replaces "xxx"dt with dt.postfixBind("xxx"), and you can guess the code of the DateTimeUnit class:

public class DateTimeUnit {

    private static final 
    DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public LocalDateTime postfixBind(String value) {
        return LocalDateTime.parse(value, FORMATTER);
    }
}

postfixBind means this unit is a suffix unit, which is the "xxx"dt. dt is after xxx. Manifold also supports prefix units, which correspond to prefixBind methods, such as:

public class DateTimeUnit {

    ...

    public LocalDateTime prefixBind(String value) {
        return LocalDateTime.parse(value, FORMATTER);
    }
}

After adding prefixBind(String), you can define LocalDateTime as:

17

Amazing! With the unit function, we can make many practical literal functions. For example, define the unit of BigInteger.

public class BigIntegerUnit {

    public BigInteger postfixBind(Integer value) {
        return BigInteger.valueOf(value);
    }

    public BigInteger postfixBind(String value) {
        return new BigInteger(value);
    }
}

auto coupled with Manifold (similar to var provided by Java 10, but auto can also be used to define attributes):

18

Who would think you are using Java 8? Also, we can use postfixBind and prefixBind together, such as by providing the following class:

public class MapEntryBuilder {

    public <K> EntryKey<K> postfixBind(K key) {
        return new EntryKey<>(key);
    }

    public static class EntryKey<K> {

        private final K key;

        public EntryKey(K key) {
            this.key = key;
        }

        public <V> Map.Entry<K, V> prefixBind(V value) {
            return new AbstractMap.SimpleImmutableEntry<>(key, value);
        }
    }
}

Then, you can create Map.Entry this way (EntryKey is created through to.postfixBind, and then Map.Entry is created through prefixBind method of EntryKey.):

19

If we provide Map with the following static extension methods:

@Extension
public class MapExt {

    @Extension
    @SafeVarargs
    public static <K, V> Map<K, V> of(Map.Entry<K, V>... entries) {
        Map<K, V> map = new LinkedHashMap<>(entries.length);

        for (Map.Entry<K, V> entry : entries) {
            map.put(entry.getKey(), entry.getValue());
        }

        return Collections.unmodifiableMap(map);
    }
}

Then, you can create Map like this:

20

Suggestion

Java has always not supported operator overloading, but there must be a reason. As a language that previously focused on enterprise application development, operator overloading was unnecessary. However, with the development of hardware, we have seen more Java appear in the data science/high-performance computing fields, and Java has begun to provide value types: Project Valhalla. Perhaps, with the application of value types, there will be more calls to provide operator overloading in Java soon. Maybe it will be adopted by JCP.

Like extension methods, when adding an operator overload, we must ask ourselves whether this class has the function of the corresponding operator semantics and whether the code written with the operator will reduce readability.

References

[1] https://github.com/manifold-systems/manifold

[2] https://openjdk.org/jeps/8277163

0 2 1
Share on

mizhou

2 posts | 0 followers

You may also like

Comments

mizhou

2 posts | 0 followers

Related Products