您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 
 订阅
Java 缺失的特性:操作符重载
 
作者:周密(之叶)

  755  次浏览      17 次
 2023-3-1
 
编辑推荐:
本文介绍了什么是操作符重载、为什么需要操作符重载、如何在Java中实现操作符重载以及一些建议。希望对您的学习有所帮助。
本文来自于微信公众号阿里开发者,由火龙果软件Linda编辑、推荐。

什么是操作符重载

操作符重载,就是把已经定义的、有一定功能的操作符进行重新定义,来完成更为细致具体的运算等功能。从面向对象的角度说,就是可以将操作符定义为类的方法,使得该操作符的功能可以用来代表对象的某个行为。

为什么需要操作符重载

我们来考虑实现这样的功能:使用 BigInteger 来实现的完全平方差公式(a^2 + 2ab + b^2)

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

 

常规写法:

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

 

假设可以对 Java 中的 *、+、- 进行操作符重载,那么我们就可以直接这样写:

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

 

所以,对于非原始类型的数值运算,如果能够进行操作符重载,至少有 2 个好处:

代码写起来更简单,不容易出错

代码更容易阅读,不会一堆括号嵌套

如何在 Java 中实现操作符重载

在 Java 中实现操作符重载,依然是使用我们的黑科技 Manifold[1]。Manifold 可以为 Java 提供各种场景操作符的重载功能,例如算数操作符(包括 +、-、*、/、%)、比较操作符(>、>=、<、<=、==、!=)、索引操作符(即 [])等。关于 Manifold 的集成,可以参考上一篇文章:Java 缺失的特性:扩展方法

算数操作符

Manifold 是将每个算数操作符的重载,映射到特定名称的函数。例如你在某个类 A 中定义了 plus(B) 的方法,那么这个类就可以使用 a + b 代替 a.plus(b) 进行调用。具体的映射关系为:

—— 用过 Kotlin 的同学应该会会心一笑,这就是模仿的 Kotlin 的操作符重载。

为了方便举例说明,我们定义一个数值类型 Num:

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);

}

}

 

对于下面的代码:

Num a = new Num(1);

Num b = new Num(2);

Num c = a + b - a;

 

Manifold 在编译期处理之后,会变成:

在数学运算上操作符存在优先级,Manifold 当然也是支持的。所以对于这样的代码:

Num c = a + a * b - b;

 

Manifold 处理之后,则是:

而且因为 Java 支持方法重载,所以对于 plus 方法,可以接收多种类型的参数。

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);

}

}

 

这极大的增强了操作符重载的能力:

Num c = a + 1 + b;

 

在 Manifold 处理之后:

值得注意的是,因为 + 和 * 都是满足交换律的,所以 a + b 首先会去对象 a 中寻找符合的 plus 方法,如果 a 中存在,则执行的是 a.plus(b);如果 a 中不存在,而 b 中存在符合的 plus 方法,则执行的是 b.plus(a)。a * b 同理。

Java 对原始类型中的数值支持复合赋值,即 +=、-= 这些,Manifold 也支持:

如果是现有的库,不能直接给它的类加这些方法该怎么办?别忘了 Manifold 支持扩展方法的哦。

比较操作符

我们都知道,对于非原始类型的 Java 对象,进行大小的比较用的是 Comparable<T>。如果你的对象实现了 Comparable<T>,那么恭喜你,Manifold 直接让你拥有了 >、>=、<、<= 这四个比较操作符的重载:

我们让 Num 实现 Comparable<Num>:

public class Num implements Comparable<Num> {

...

@Override

public int compareTo(Num that) {

return this.v - that.v;

}

}

 

那么对于这样的代码:

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");

}

 

运行代码会输出 a < b,因为代码在被 Manifold 处理之后会变为:

你是不是激动的要问,那么 == 和 != 呢,Manifold 支持了吗?是的,我的朋友,它支持了(翻译腔)。Manifold 提供了一个新的接口 ComparableUsing<T>,通过它你可以实现对 == 和 != 的重载。

ComparableUsing<T> 继承了 Comparable<T> 接口,并且添加了两个方法,compareToUsing 和 equalityMode。查看 comparableUsing 的默认实现:

可见对于 >、>=、<、<= 这四种操作符的重载,直接是使用 Comparable<T> 的 compareTo 的实现。而对于 == 和 !=,则是根据 equalityMode 方法的返回值,来选择使用何种实现:

如果是 EqualityMode.CompareTo,则 == 和 != 的重载分别对应的是 compareTo 方法返回值为 0 和 非0 的情况。

如果是 EqualityMode.Equals,则 == 和 != 的重载分别对应的是 equals 方法返回值为 true 和 false 的情况。

如果是 EqualityMode.Identity,那使用的是 Java 的默认实现,即比较对象的引用地址是否相同。

而 equalityMode 默认的方法返回值为 EqualityMode.Equals,即 Manifold 默认使用 equals 方法来进行 == 和 != 的判断。当然,你也可以不使用 Manifold 的 equalityMode 这套逻辑,直接实现自己的 compareUsing 方法,处理各种 Operator 的比较逻辑。

我们让 Num 实现 ComparableUsing<Num> 接口,并覆写 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);

}

}

 

则此时我们对 == 和 != 进行了重载,并且使用的是基于 equals 方法的实现。那么对于下面的代码:

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");

}

 

运行代码会打印 a == b,因为 Manifold 处理之后的代码会变为:

Amazing!我们终于实现了 N 年前的梦想,让 == 和 != 是使用 equals 方法的逻辑进行比较,而不是比较引用地址。

你应该也发现了,如果某个类型 T 要实现 ComparableUsing<T>,那么说明 T 一定是 Comparable<T>。也就是说,如果你想要对 T 重载 == 和 !=,则要求 T 一定是可比较的。Manifold 之所以这样做,而不是为重载 == 和 != 提供单独的接口,是因为作者目前认为用 == 和 != 来代替 equals,弊大于利 —— 毕竟用 equals 来比较两个对象是否相等这件事,在 Java 中太深入人心了。所以目前 Manifold 作者希望大家只对数值和量词这类的对象使用 == 和 !=,不要产生滥用行为。

如果是现有的库,比如 String、BigInteger,不能直接给它的类新增接口实现怎么办?你可以给这个类建一个扩展类,然后让扩展类实现 ComparableUsing<T>,然后 Manifold 会按照这个类实现了 ComparableUsing<T> 进行处理。比如 Manifold 对于 BigInteger 的扩展类 ManBigIntegerExt(位于 manifold-science 库中):

它以扩展方法的形式,提供了自定义逻辑的 compareUsing 实现:

注意,这个时候要用 abstract 关键字修饰扩展类,因为它不是真的要以常规方式来实现 ComparableUsing<T> 接口。或者,你也可以把扩展类声明为接口,然后继承 ComparableUsing<T> 接口。

索引操作符

Java 对数组是支持索引操作符的,比如 nums[i] 是访问数组索引为 i 的元素,nums[i] = n 是对数组索引为 i 的位置进行赋值。但对 List 和 Map,Java 说 “不好意思,因为我是 Java,这个支持不了”。所以 Manifold 又出手了,让你不再只能羡慕其他语言。

因为 java.util.List 已经具备了这两个方法,所以有了 Manifold,你可以这样写代码:

图片

而 Map 只有 get 方法,没有 set 方法,所以你可以在 Map 扩展类里面,加一个 set:

@Extension

public class MapExt {

public static <K, V> V set(@This Map<K, V> map, K key, V value) {

return map.put(key, value);

}

}

 

然后我们就可以这样写代码了:

简直不要太爽!需要注意的是,Manifold 对 set 方法是有要求的:set 方法的返回值不能为 void,并且应该返回和第二个参数一样类型的值(一般是返回旧值)。之所以有这样的要求,是为了和 Java 本身的数组的索引赋值表达式保持一致(如果 set 返回的是 void,索引赋值表达式就无法支持了)。在 Java 中,你可以这样赋值:

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

int value = nums[0] = 10;

 

执行完成之后,num[0] 和 value,都会是 10。所以,当我们使用索引赋值表达式的时候:

List<String> list = Arrays.asList("a", "b", "c");

String value = list[0] = "A";

 

Manifold 处理之后,代码会变成:

因而类似于 T value = list[0] = obj 的表达式,执行完之后 value 不是 set 方法的返回值,而是最右侧的值。

单位操作符

Manifold 还提供一种非常有意思的功能:单位操作符。顾名思义,就是我们可以在代码中提供“单位”功能。比如下面这种代码:

你是不是已经惊呆了?我第一次见到的时候也是满脑子“还能这样操作”的惊奇。而这个 dt,就是“单位”。查看 Manifold 处理后的代码:

也就是说 Manifold 将 "xxx"dt 替换为了 dt.postfixBind("xxx"),那么你也就可以猜到 DateTimeUnit 类的代码:

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 表示这个单位是“后缀单位”,就是你看到的 "xxx"dt,dt 在 "xxx" 的后面。Manifold 同时也支持“前缀单位”,对应的方法是 prefixBind,比如:

public class DateTimeUnit {

...

public LocalDateTime prefixBind(String value) {

return LocalDateTime.parse(value, FORMATTER);

}

}

 

添加了 prefixBind(String) 后,那么就可以这样定义 LocalDateTime:

Amazing!有了“单位”功能,我们就可以做出很多实用的“字面量”功能。比如定义 BigInteger 的“单位”:

public class BigIntegerUnit {

public BigInteger postfixBind(Integer value) {

return BigInteger.valueOf(value);

}

public BigInteger postfixBind(String value) {

return new BigInteger(value);

}

}

 

配合 Manifold 的 auto(类似于 Java10 提供的 var,但是 auto 还可以用来定义属性):

谁还会认为你用的是 Java8?对于不知道 Manifold 的同事,你和他说你用的是一门新的名叫 Java888 的语言,他都会相信的 :)。而且我们还可以将 postfixBind 和 prefixBind 放在一起使用,比如提供下面的类:

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);

}

}

}

 

那么,便可以通过下面这种方式来创建 Map.Entry(先通过 to.postfixBind 创建 EntryKey,再通过 EntryKey 的 prefixBind 方法创建 Map.Entry):

如果我们再为 Map 提供如下静态扩展方法:

@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);

}

}

 

那么你可以这样创建 Map:

建议

Java 一直以来都不支持操作符重载,肯定是有其原因的。作为一门之前主打企业应用开发的语言,确实操作符重载不是必要的。但随着硬件的发展,我们也看到 Java 越来越多的出现在数据科学/高性能计算的领域,同时 Java 也开始尝试提供值类型:Project Valhalla[2]。所以,也许在不久后的将来,随着值类型在计算方面的广泛应用,在 Java 中提供操作符重载的呼声会越来越高,进而被 JCP 采纳。而 Manifold 作为先驱者,提前让我们可以体验未来的 Java,幸甚至哉!

当然,和扩展方法一样,如果决定在项目中采用 Manifold 提供操作符重载,我们一定要做到“管住自己的手”。当想要添加某个操作符重载时,一定要先问自己一遍 “这个类是否具备该操作符对应语义的功能,用操作符写的代码是否会降低代码可读性”。

 

 

 
   
755 次浏览       17
相关文章

Java微服务新生代之Nacos
深入理解Java中的容器
Java容器详解
Java代码质量检查工具及使用案例
相关文档

Java性能优化
Spring框架
SSM框架简单简绍
从零开始学java编程经典
相关课程

高性能Java编程与系统性能优化
JavaEE架构、 设计模式及性能调优
Java编程基础到应用开发
JAVA虚拟机原理剖析

最新活动计划
MBSE(基于模型的系统工程)4-18[北京]
自然语言处理(NLP) 4-25[北京]
基于 UML 和EA进行分析设计 4-29[北京]
以用户为中心的软件界面设计 5-16[北京]
DoDAF规范、模型与实例 5-23[北京]
信息架构建模(基于UML+EA)5-29[北京]
 
 
最新文章
Java虚拟机架构
JVM——Java虚拟机架构
Java容器详解
Java进阶--深入理解ArrayList实现原理
Java并发容器,底层原理深入分析
最新课程
java编程基础到应用开发
JavaEE架构、 设计模式及性能调优
高性能Java编程与系统性能优化
SpringBoot&Cloud、JavaSSM框架
Spring Boot 培训
更多...   
成功案例
国内知名银行 Spring+SpringBoot+Cloud+MVC
北京 Java编程基础与网页开发基础
北京 Struts+Spring
华夏基金 ActiveMQ 原理
某民航公 Java基础编程到应用开发
更多...