V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Braisdom
V2EX  ›  程序员

Java 运算符重载(Operator Overloading)原理

  •  2
     
  •   Braisdom ·
    braisdom · 2020-12-04 13:40:25 +08:00 · 4547 次点击
    这是一个创建于 1232 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Java 语言的出现是为了降低 C++ 的开发成本和学习难,但也将 C++ 一些非常有价值的特性搞丢了,运算符重载就是一个非常优秀的特性,但 Java 搞丢了,虽然运算符重载不是一个常用特性,但随着数据分析领域的发展,表达式的编译也就有了强烈的需求。

    首先,我解释一下为什么需要运算符重载,Java 是一个严谨的逻辑性编程语句,有丰富的工程化集成能力,通常 Java 编译后的代码只是 JVM 中运行,但也会在其它的可执行单元中运行,常见的有:数据库(关系型、对象型、KV 型和预计算引擎等)、也可通过是一次远程服务调用。倘若这类可执行单元的协议中存在表达式(算术、比较和逻辑),这也就意味着在 Java 中需要通过字符串的形式表现这类表达式,此时就会出现下列的代码:

    目标输出协议:

    "(sum(order.no) + 100) > 1000 and order.id < 999"
    

    构造协议的 Java 代码:

    and(gt(plus(funCall("sum", "order.no"), 100)), lt(fieldCall("order", "id"), 999));
    

    对比一下将计算表达式转换为 Java 中的方法调用,变得极难理解,但这种方式也是目前大都 ORM 框架所使用的方式,这样做的方式可以使得构造过程动态化,而不是简单的字符串拼接,当然其它内部原理还是字符串拼接。

    在这样的使用场景下,如果 Java 的表达式支持重载则会变的非常简单,下面是 ObjectiveSQL 的代码:

    Order.Table order = Order.asTable();
    
    Expression expr = (sum(order.no) + 100) > 1000 && order.id < 999;
    

    其中的Expression 是实现了 Java 运算符重载,内部有种数学计算、比较和逻辑计算的相关方法,最终也是通过拼接字符串的形式输出上述目标协议。

    详细可参考: https://github.com/braisdom/ObjectiveSql

    如果认可项目,请点个赞,欢迎交流...

    48 条回复    2020-12-05 15:31:07 +08:00
    Braisdom
        1
    Braisdom  
    OP
       2020-12-04 13:51:29 +08:00
    有兴趣的,可以交流
    lewis89
        2
    lewis89  
       2020-12-04 14:03:18 +08:00   ❤️ 1
    重载运算符是一个优秀的特性???????老哥不是开玩笑吧
    operate *
    operate +
    operate =
    犹如噩梦啊,再配合 拷贝构造 引用 指针 指针的引用 右值引用 左值引用 不把脑子搞炸那还是 C++?
    Braisdom
        3
    Braisdom  
    OP
       2020-12-04 14:08:20 +08:00
    没有运算符重载者是恶梦的开始
    wysnylc
        4
    wysnylc  
       2020-12-04 14:13:35 +08:00
    xzenge
        5
    xzenge  
       2020-12-04 14:26:32 +08:00
    为什么不直接用 C++呢
    no1xsyzy
        6
    no1xsyzy  
       2020-12-04 14:31:27 +08:00
    @lewis89 使下层变复杂的同时使上层变得清晰。
    Python 也能重载运算符(__add__ 等魔法方法),并且 NumPy 时常依赖重载
    如果运算符重载没有使上层更清晰,那肯定是不需要重载瞎 JB 重载。
    PVG
        7
    PVG  
       2020-12-04 14:33:55 +08:00
    scala 在这一块做得很好,你可以为所欲为
    lewis89
        8
    lewis89  
       2020-12-04 14:41:11 +08:00   ❤️ 1
    @no1xsyzy #6 有的时候 选择太多并不是一件好事,C++为什么被称为 horrible 的语言 就是因为选择太多了,一个 option 的选择有 3 个 另一个 option 有 3 个 再加上一个 option 的选项有 7 个 3 * 3 * 7 = 42 种选项.. 脑子都会炸
    no1xsyzy
        9
    no1xsyzy  
       2020-12-04 14:46:18 +08:00
    @wysnylc Smalltalk 是目前公认无二的最符合 OOP 思想的语言,操作符只是一种特殊的信号,可以通过 double dispatch 来重构行为
    OOP 的 “代码与对象 / 类绑定” 的想法甚至比函数式更允许运算符重载(天然带 single dispatch )。
    跟 OOP 没关系,但具体什么原因我就不清楚了,可能是严谨性的因素。
    Braisdom
        10
    Braisdom  
    OP
       2020-12-04 14:48:29 +08:00
    @PVG 认可你的想法,的确可以想干什么就干什么
    Braisdom
        11
    Braisdom  
    OP
       2020-12-04 14:49:50 +08:00
    @lewis89 一个语言要能跟随的时代的发展,C++ 的设计就是为了解决“我们不知道的不知道的东西”,很伟大。
    Braisdom
        12
    Braisdom  
    OP
       2020-12-04 14:50:45 +08:00
    @xzenge 有时候由不得你去选择,解决 Java 的运算符重载,也是被迫的
    no1xsyzy
        13
    no1xsyzy  
       2020-12-04 14:51:24 +08:00
    @lewis89 3*3*7=63 你的算数是体育老师教的罢!(ry
    滥用功能与语言没关系…… checked exception 不也被 throws Exception 了……
    Braisdom
        14
    Braisdom  
    OP
       2020-12-04 14:52:12 +08:00
    @no1xsyzy Smalltalk 是面向对象的鼻祖,很多概念都被借鉴了,为什么不能发展起来,也是有它内在的原因
    no1xsyzy
        15
    no1xsyzy  
       2020-12-04 14:53:45 +08:00
    准确地说,运算符重载是一个 DSL-ish 的做法。
    要不是现在的语言从抽象设计上都羸弱不堪,哪还需要运算符重载?来个宏就行了。
    Braisdom
        16
    Braisdom  
    OP
       2020-12-04 14:54:17 +08:00
    @no1xsyzy 小细节...
    lewis89
        17
    lewis89  
       2020-12-04 14:55:53 +08:00   ❤️ 1
    @no1xsyzy #13 唉,年轻人啊就是没法理解我的梗,option 太多了 就算你数学是数学老师教的又能怎么样,人脑是有限的,大部分人 口算 心算 超过 2 个以上 出问题的概率激增,99 乘法表 两个一位数当然可以肌肉肌肉背诵,保证出错率低
    4 个呢 5 个呢 只要复杂度在增加 人脑是肯定不够用的
    no1xsyzy
        18
    no1xsyzy  
       2020-12-04 14:57:05 +08:00
    @Braisdom 能不能发展起来从来不是内在原因决定的,是要它有能被使用的地方。
    发展的意思就是参与者变多。
    Braisdom
        19
    Braisdom  
    OP
       2020-12-04 14:57:15 +08:00
    @no1xsyzy 我相信 Java 后面必定会支持运算符重载,虽然现在没有真实的项目在用,我是第一个,国外有几篇论文在讨论。

    DSL 是一个很泛的概念,SQL 是一个 DSL,基于 Java 的 API 封装的 SQL 不一定属于 DSL 的范畴,因为它是一个高度抽象的语言,存在的只是一堆 Interface,基于 Interface 可以实现很多特性
    Vedar
        20
    Vedar  
       2020-12-04 14:58:22 +08:00
    @lewis89 c++的运算符重载复杂是因为 c++ raii 的复杂,运算符重载本身对于语言表达能力是一个很大的帮助
    Braisdom
        21
    Braisdom  
    OP
       2020-12-04 14:59:26 +08:00
    @no1xsyzy 不同的思考维度,肯定不是某一个,或某一些原因。

    我们生活的世界有两个,一个是真实运行的世界,还有一个是脑子里的世界(也就是“应该”的世界),但两都往往偏差很大
    Braisdom
        22
    Braisdom  
    OP
       2020-12-04 15:00:22 +08:00
    @Vedar 同意,我很敬佩 C++ 的设计者,他们能解决我们“不知道我们不知道的事情”
    ximigou007
        23
    ximigou007  
       2020-12-04 15:02:19 +08:00
    当你熟练掌握一个东西你就想要灵活性,当你不熟练的时候灵活性就是灾难
    no1xsyzy
        24
    no1xsyzy  
       2020-12-04 15:02:59 +08:00
    @lewis89 所以我都标了 “(ry” 了……
    Perl 也火了好一段…… 这可是经典的 TIMTOWTDI 语言。
    重载是 DSL-ish 做法,你这么说的话,其实所有 DSL 都可以下岗了。
    ximigou007
        25
    ximigou007  
       2020-12-04 15:03:13 +08:00
    重载不重载本质上是设计哲学的冲突,总会有 trade off
    no1xsyzy
        26
    no1xsyzy  
       2020-12-04 15:15:33 +08:00
    @Braisdom #19 路线预测我就不清楚了。

    所以我称为 DSL-ish 。 #0 举的例子确实发生了范式转移( paradigm shift ),从命令式的代码转换成了描述式的代码。保持 Java 的思维粗略地一看会彻底误解这段代码的作用。

    #21 外部因素对语言成否的影响更大,这有统计上的实证,C - Unix,Python - Data Science,JavaScript - Web,至于 Java 跨平台方面始终不温不火,直到 Android 。
    和理论上的解释,见 #18
    cpstar
        27
    cpstar  
       2020-12-04 16:38:39 +08:00
    个人认为对于 OO 概念来讲,加减乘除运算符也不应该有,而是 Number.Add/Minus/Multiply/Divide,所以,重载,睡了吧
    FireFoxAhri
        28
    FireFoxAhri  
       2020-12-04 16:42:01 +08:00
    可以了解一下 Scala 。。运算符也可以定义成函数
    Braisdom
        29
    Braisdom  
    OP
       2020-12-04 17:29:48 +08:00
    @ximigou007 我觉得是个人偏好的问题,只是 Java 的创始人不喜欢运算符重载,但它想的不够远
    Braisdom
        30
    Braisdom  
    OP
       2020-12-04 17:33:29 +08:00
    @cpstar 这是 OO 里最传统的想法,不能为了 OO 而去 OO,过于理论化的思考,而不考虑实际。

    一个同环比的计算是最常见的:(sum(current) - sum(last)) / sum(last) * 100
    这样的表达式用函数做就太复杂了
    aguesuka
        31
    aguesuka  
       2020-12-04 18:04:17 +08:00 via Android
    中缀表达式有 Monoid 之类的讲究,但是 java 没有静态检验的能力,社区也没有 plt 的气氛。java 一切皆对象,java 中的运算符都是纯函数,重载的运算符不管是实例方法还是静态函数都很尴尬。
    kotlin 有中缀表达式,但是表达能力和 "实例.方法(参数,参数)"没有区别。我觉得你的需求应该自己定义语法,用 idea 的 language inject 做代码提示和静态分析。
    Braisdom
        32
    Braisdom  
    OP
       2020-12-04 18:08:15 +08:00
    @aguesuka 是的,所以我采用的是比较迂回的方法,先解析表达式,然后在编译期覆盖代码实现的

    期待 JDK 后续变化,我提个 issue 给 JDK
    wysnylc
        33
    wysnylc  
       2020-12-04 18:10:10 +08:00
    lambda 可以实现运算符重载的一切功能
    Braisdom
        34
    Braisdom  
    OP
       2020-12-04 18:11:07 +08:00
    @wysnylc 语法太复杂,我需要的是最简单的表达式,没必要绕那么多弯
    namelosw
        35
    namelosw  
       2020-12-04 18:26:31 +08:00
    运算符和方法本质区别不大, 像 Ruby Scala Haskell 之类的设计其实比较优雅. 比较尴尬的是 C++那种有限重载.

    a + b 就是 a.+(b) 就是 a.add(b)

    运算符在命名方面不会让语言可读性变得更差, 反而是该用运算符非得用字母比如.eq()之类的可读性不好.

    但是运算符最大的问题是优先级容易乱. 像 LISP 之类强制括号的话, 其实无所谓是运算符还是字母.
    secondwtq
        36
    secondwtq  
       2020-12-04 18:42:43 +08:00
    这贴感觉开错了 ... 这标题就注定了下面必然会以 OT 为主 ...
    优先级是个老问题,个人偏向于放弃优先级,强制括号
    S-expr 也行,不过我习惯看中缀是改不过来了,我喜欢 S-expr 是因为——目前基本所有中缀运算符的语法,都必须硬点一部分符号给运算符用(就不说还要钦定优先级了),S-expr 只占括号和空格,其他符号都可以解放出来,甚至包括 hyphen

    C++、Java 的运算符重载(以及函数重载)的问题很大程度也是钦定的东西太多了
    Braisdom
        37
    Braisdom  
    OP
       2020-12-04 21:04:37 +08:00
    @namelosw
    @secondwtq

    两位没有仔细看我帖子的内容,使用 运算符计算 和函数计算 在简单的时候没有区别,关键,如果一个表达式过于复杂会是个什么样子,例如:(1+1) * 1 / 1 > 1 && (2 + 2) * 2 /2 < 2 你们可以试一下这样一眼能看清的表达式通过函数什么是个什么样子
    ximigou007
        38
    ximigou007  
       2020-12-04 21:04:42 +08:00
    @Braisdom 问题是可以随意重载运算符的话,如果使用不当会有不可预知的风险,如果不可重载,所有结果是可以预期的
    Braisdom
        39
    Braisdom  
    OP
       2020-12-04 21:05:55 +08:00
    @ximigou007 不是随意重载,有兴趣看一下我的代码: https://github.com/braisdom/ObjectiveSql

    我只是翻译
    TheCure
        40
    TheCure  
       2020-12-04 21:11:10 +08:00
    本人 Python 程序员, 极度讨厌运算符重载

    从这里面的代码看, string interpolation 可以解决一部分问题, 可以试试 kotlin
    Braisdom
        41
    Braisdom  
    OP
       2020-12-04 21:22:43 +08:00
    @callofmx 如果语言可以选择,kotlin 不是我最佳选择。

    算符重载 适合不同的场景,滥用了也就失去意义了
    namelosw
        42
    namelosw  
       2020-12-04 23:35:18 +08:00   ❤️ 1
    @Braisdom 我已经说了, 问题是优先级. 所以:

    1. 主要问题不在操作符重载, 而在于有优先级而不强制括号才会导致你的表达式看不清. LISP 可以乱用操作符, 也没看谁搞不清, 因为跟普通函数没有任何区别.
    2. 次要问题是传递性, 即使是同一个操作符优先级一样, 也有左传递右传递问题, 比如加减和赋值的传递是相反的.

    写一个 C 语言的 parser 你就全理解了.
    Braisdom
        43
    Braisdom  
    OP
       2020-12-05 09:32:38 +08:00
    @namelosw 你说的很对,优先级是主要问题,是从数学延续下来的,也是目前无法改变的事实,只能用其它的方法来解决,表达式的表现力 相比 函数嵌套的表达力 还是强很多的。

    所以重载在这种场景下,是必须的
    1490213
        44
    1490213  
       2020-12-05 12:12:21 +08:00 via Android   ❤️ 2
    我不认为加入运算符重载能带来的好处有多少,但重载操作符极大的增加了阅读代码的心智负担。这种东西本质就是有了一种新的 DSL,导致不能通过现有认知的心理模型去理解,必须重新学习或者查询,看到熟悉的符号也不能断定其意义。
    另外,有了重载运算符,下一步是不是就需要搞 haskell 的自定义结合律和优先级了?学术语言或者专业语言搞搞可以理解,但是普通人使用的语言我觉得应该简化复杂度和清晰规则,而不是相反。
    ruyu
        45
    ruyu  
       2020-12-05 13:21:01 +08:00
    所以 scheme 多好, scheme 表示不知道你们在争论什么
    Braisdom
        46
    Braisdom  
    OP
       2020-12-05 13:28:19 +08:00   ❤️ 1
    @1490213 @ruyu 兄弟,我们先要忘记一切,从纯粹的数学出发

    1 ) Java 和 SQL 都有运算符,这就表明运算符是表示复杂表达式的最佳方式,我做的只是让两者一致。
    2 ) haskell 是一个调试抽象的语言,可能会符合一部分人群,世界万物没有实质的对与错,只是喜欢和不喜欢
    3 ) ObjectiveSQL 不是一个能够满足所有人偏好的技术,我只是想满足更多的人
    ruyu
        47
    ruyu  
       2020-12-05 15:20:20 +08:00
    我觉得楼主这个挺好的, 我就歪个楼, scheme 根本不用争论运算符应不应该重载的问题, 因为它根本没有运算符 (
    Braisdom
        48
    Braisdom  
    OP
       2020-12-05 15:31:07 +08:00
    @ruyu 诚心做事,也希望得到更多人认可,https://github.com/braisdom/ObjectiveSql 这是我的项目,感兴的点个赞
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1024 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 19:14 · PVG 03:14 · LAX 12:14 · JFK 15:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.