By Mi Zhou (Zhiye)
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.
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.
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.
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:
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:
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:
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.
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:
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 !=
.
ComparableUsing<T>
inherits Comparable<T>
interface and adds two methods: compareToUsing
and equalityMode
. View the default implementation of comparableUsing
:
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.
EqualityMode.CompareTo
, the overloading of ==
and !=
corresponds to the case where the return value of the compareTo
method is 0 and non-0, respectively.EqualityMode.Equals
, the overloading of ==
and !=
corresponds to the case where the return value of the equals
method is true
and false
.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:
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):
It provides a compareUsing
implementation of custom logic in the form of extension methods.
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.
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:
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:
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:
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:
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:
Stunned? Me too. The dt
is the unit. Look at the code after Manifold processing:
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:
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):
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
.):
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:
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.
Alibaba Cloud Community - June 16, 2023
mizhou - June 15, 2023
Alibaba Cloud Native Community - August 30, 2022
Alibaba Cloud Native Community - March 6, 2023
Alibaba Clouder - April 8, 2019
Alibaba Cloud Community - March 27, 2023
A low-code development platform to make work easier
Learn MoreHelp enterprises build high-quality, stable mobile apps
Learn MoreAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreMore Posts by mizhou