为什么Java不提供操作符重载?

从c++到Java,一个显而易见的未回答的问题是为什么Java没有包含操作符重载?

难道Complex a, b, c; a = b + c;不是比Complex a, b, c; a = b.add(c);简单得多吗?

是否有一个已知的原因,允许操作符重载的有效参数?理由是随意的,还是被时间消磨了?

189750 次浏览

你可能真的会搬起石头砸自己的脚。这就像指针一样,人们会犯愚蠢的错误,所以我们决定把剪刀拿走。

至少我认为这是原因。 反正我是站在你这边的。:) < / p >

我认为这可能是一个有意识的设计选择,迫使开发人员创建的函数的名称清楚地表达了他们的意图。在c++中,开发人员会用与给定操作符的普遍接受性质无关的功能重载操作符,这使得不查看操作符的定义几乎不可能确定一段代码的功能。

Java设计人员认为操作符重载带来的麻烦大于它的价值。就这么简单。

在一种每个对象变量实际上都是引用的语言中,运算符重载具有相当不合逻辑的额外危险——至少对c++程序员是这样。将这种情况与c#的==操作符重载和Object.EqualsObject.ReferenceEquals(或任何它的名称)进行比较。

假设Java是实现语言,那么a、b和c都是对初始值为null的Complex类型的引用。还假设Complex是不可变的,就像前面提到的BigInteger和类似的不可变BigDecimal一样,我想你的意思是以下,因为你将引用赋值给从添加b和c返回的Complex,而不是将这个引用与a进行比较。

不是:

Complex a, b, c; a = b + c;

比:

Complex a, b, c; a = b.add(c);

Groovy具有操作符重载,并在JVM中运行。如果您不介意性能损失(每天都在减小)。它是基于方法名自动生成的。例如,'+'调用'plus(参数)'方法。

假设你想要覆盖a引用的对象的前一个值,那么必须调用一个成员函数。

Complex a, b, c;
// ...
a = b.add(c);

在c++中,这个表达式告诉编译器在堆栈上创建三个(3)个对象,执行加法,并将临时对象的结果值复制到现有对象a中。

然而,在Java中,operator=不执行引用类型的值复制,用户只能创建新的引用类型,而不能创建值类型。因此,对于名为Complex的用户定义类型,赋值意味着将引用复制到现有值。

考虑:

b.set(1, 0); // initialize to real number '1'
a = b;
b.set(2, 0);
assert( !a.equals(b) ); // this assertion will fail

在c++中,这将复制值,因此比较结果将是不相等的。在Java中,operator=执行引用复制,因此ab现在引用相同的值。结果,比较将产生'equal',因为对象的比较结果将等于自身。

复制和引用之间的差异只会增加操作符重载的混乱。正如@Sebastian提到的,Java和c#都必须分别处理值和引用相等问题——operator+可能会处理值和对象,但operator=已经被实现来处理引用。

在c++中,一次只能处理一种比较,这样就不会那么令人困惑。例如,在Complex上,operator=operator==都在处理值——分别复制值和比较值。

James Gosling把设计Java比作:

“当你从一个公寓搬到另一个公寓时,有一个搬家的原则。一个有趣的实验是把你的公寓收拾好,把所有东西都放进盒子里,然后搬到下一个公寓,直到你需要它时才打开它。你在做你的第一顿饭,你从盒子里拿出一些东西。然后过了一个月左右,你就会发现你生活中真正需要的东西是什么,然后你就会把剩下的东西——忘记你有多喜欢它们,忘记它们有多酷——然后把它们扔掉。这是如何简化你的生活,你可以在各种设计问题上使用这一原则:不要仅仅因为它们很酷或有趣就去做事情。”

你可以读取这里引用的上下文

基本上,运算符重载对于对点、货币或复数建模的类非常有用。但在那之后你很快就会用光例子。

另一个因素是开发人员在c++中滥用了这个特性,重载了'&&'、'||'、强制转换操作符,当然还有'new'。特殊的C + +书中详细介绍了将此与传递值和异常相结合所导致的复杂性。

有时,操作符重载、友类和多重继承会很好。

然而,我仍然认为这是一个很好的决定。如果Java有运算符重载,那么如果不查看源代码,我们就永远无法确定运算符的含义。目前还没有必要。而且我认为你使用方法而不是操作符重载的例子也是相当可读的。如果你想让事情更清楚,你总是可以在烦人的语句之上添加注释。

// a = b + c
Complex a, b, c; a = b.add(c);

看看Boost吧。单位:链接文本

它通过操作符重载提供零开销的维数分析。这一点还能清楚多少呢?

quantity<force>     F = 2.0*newton;
quantity<length>    dx = 2.0*meter;
quantity<energy>    E = F * dx;
std::cout << "Energy = " << E << endl;

实际上会输出“Energy = 4 J”,这是正确的。

有很多帖子抱怨操作员超载。

我觉得我必须澄清“操作符重载”;概念,提供了关于这个概念的另一种观点。

代码混淆?

这个论点是谬论。

混淆在所有语言中都是可能的……

在C或Java中,通过函数/方法混淆代码很容易,就像在c++中通过操作符重载一样:

// C++
T operator + (const T & a, const T & b) // add ?
{
T c ;
c.value = a.value - b.value ; // subtract !!!
return c ;
}


// Java
static T add (T a, T b) // add ?
{
T c = new T() ;
c.value = a.value - b.value ; // subtract !!!
return c ;
}


/* C */
T add (T a, T b) /* add ? */
{
T c ;
c.value = a.value - b.value ; /* subtract !!! */
return c ;
}

...甚至在Java的标准接口中

另一个例子,让我们看看Java中的Cloneable接口:

您应该克隆实现此接口的对象。但你可以撒谎。并创建一个不同的对象。事实上,这个接口非常弱,你可以返回另一种类型的对象,只是为了好玩:

class MySincereHandShake implements Cloneable
{
public Object clone()
{
return new MyVengefulKickInYourHead() ;
}
}

由于Cloneable接口可以被滥用/混淆,它应该被禁止在同样的理由,c++操作符重载应该是?

我们可以重载MyComplexNumber类的toString()方法,让它返回一天中经过字符串化的小时。toString()重载也应该被禁止吗?我们可以破坏MyComplexNumber.equals,让它返回一个随机值,修改操作数……等等,等等。

在Java中,就像在c++或其他语言中一样,程序员在编写代码时必须尊重最低限度的语义。这意味着实现一个add函数进行添加,Cloneable实现方法进行克隆,++操作符进行递增。

到底什么是混淆?

现在我们知道,即使通过原始的Java方法,代码也可以被破坏,我们可以问问自己,在c++中操作符重载的真正用途是什么?

清晰自然的符号:方法vs.操作符重载?

我们将在下面比较,对于不同的情况,“相同”;用Java和c++编写代码,以了解哪种编码风格更清晰。

自然的比较:

// C++ comparison for built-ins and user-defined types
bool    isEqual          = A == B ;
bool    isNotEqual       = A != B ;
bool    isLesser         = A <  B ;
bool    isLesserOrEqual  = A <= B ;


// Java comparison for user-defined types
boolean isEqual          = A.equals(B) ;
boolean isNotEqual       = ! A.equals(B) ;
boolean isLesser         = A.comparesTo(B) < 0 ;
boolean isLesserOrEqual  = A.comparesTo(B) <= 0 ;

请注意,A和B可以是c++中的任何类型,只要提供了操作符重载。在Java中,当A和B不是原语时,代码会变得非常混乱,即使是类似原语的对象(BigInteger等)……

自然数组/容器访问器和下标:

// C++ container accessors, more natural
value        = myArray[25] ;         // subscript operator
value        = myVector[25] ;        // subscript operator
value        = myString[25] ;        // subscript operator
value        = myMap["25"] ;         // subscript operator
myArray[25]  = value ;               // subscript operator
myVector[25] = value ;               // subscript operator
myString[25] = value ;               // subscript operator
myMap["25"]  = value ;               // subscript operator


// Java container accessors, each one has its special notation
value        = myArray[25] ;         // subscript operator
value        = myVector.get(25) ;    // method get
value        = myString.charAt(25) ; // method charAt
value        = myMap.get("25") ;     // method get
myArray[25]  = value ;               // subscript operator
myVector.set(25, value) ;            // method set
myMap.put("25", value) ;             // method put

在Java中,我们看到每个容器做同样的事情(通过索引或标识符访问它的内容),我们有不同的方法来做,这是令人困惑的。

在c++中,由于操作符重载,每个容器都使用相同的方式访问其内容。

自然高级类型操作

下面的例子使用了一个Matrix对象,通过在谷歌上找到的“__abc1”的第一个链接找到。和“c++矩阵对象":

// C++ YMatrix matrix implementation on CodeProject
// http://www.codeproject.com/KB/architecture/ymatrix.aspx
// A, B, C, D, E, F are Matrix objects;
E =  A * (B / 2) ;
E += (A - B) * (C + D) ;
F =  E ;                  // deep copy of the matrix


// Java JAMA matrix implementation (seriously...)
// http://math.nist.gov/javanumerics/jama/doc/
// A, B, C, D, E, F are Matrix objects;
E = A.times(B.times(0.5)) ;
E.plusEquals(A.minus(B).times(C.plus(D))) ;
F = E.copy() ;            // deep copy of the matrix

这并不局限于矩阵。Java中的BigIntegerBigDecimal类同样有令人困惑的冗长之处,而它们在c++中的等价类则像内置类型一样清晰。

自然的迭代器:

// C++ Random Access iterators
++it ;                  // move to the next item
--it ;                  // move to the previous item
it += 5 ;               // move to the next 5th item (random access)
value = *it ;           // gets the value of the current item
*it = 3.1415 ;          // sets the value 3.1415 to the current item
(*it).foo() ;           // call method foo() of the current item


// Java ListIterator<E> "bi-directional" iterators
value = it.next() ;     // move to the next item & return the value
value = it.previous() ; // move to the previous item & return the value
it.set(3.1415) ;        // sets the value 3.1415 to the current item

自然的仿函数:

// C++ Functors
myFunctorObject("Hello World", 42) ;


// Java Functors ???
myFunctorObject.execute("Hello World", 42) ;

文本连接:

// C++ stream handling (with the << operator)
stringStream   << "Hello " << 25 << " World" ;
fileStream     << "Hello " << 25 << " World" ;
outputStream   << "Hello " << 25 << " World" ;
networkStream  << "Hello " << 25 << " World" ;
anythingThatOverloadsShiftOperator << "Hello " << 25 << " World" ;


// Java concatenation
myStringBuffer.append("Hello ").append(25).append(" World") ;

好的,在Java中你也可以使用MyString = "Hello " + 25 + " World" ;…但是,等一下:这是操作符重载,不是吗?这不是作弊吗??

: - d

泛型代码?

相同的泛型代码修改操作数应该可用于内置/原语(在Java中没有接口)、标准对象(不可能有正确的接口)和用户定义对象。

例如,计算任意类型的两个值的平均值:

// C++ primitive/advanced types
template<typename T>
T getAverage(const T & p_lhs, const T & p_rhs)
{
return (p_lhs + p_rhs) / 2 ;
}


int     intValue     = getAverage(25, 42) ;
double  doubleValue  = getAverage(25.25, 42.42) ;
complex complexValue = getAverage(cA, cB) ; // cA, cB are complex
Matrix  matrixValue  = getAverage(mA, mB) ; // mA, mB are Matrix


// Java primitive/advanced types
// It won't really work in Java, even with generics. Sorry.

讨论操作符重载

现在我们已经看到了使用操作符重载的c++代码与使用Java的相同代码之间的公平比较,现在我们可以讨论“操作符重载”。作为一个概念。

运算符重载早在计算机出现之前就存在了

即使在计算机科学之外,也存在操作符重载:例如,在数学中,+-*等操作符是重载的。

实际上,+-*等的意义取决于操作数的类型(数字、向量、量子波函数、矩阵等)。

作为科学课程的一部分,我们大多数人都学习了操作符的多种含义,这取决于操作数的类型。那我们是不是觉得很困惑呢?

操作符重载取决于它的操作数

这是操作符重载中最重要的部分:就像在数学或物理中一样,操作取决于其操作数的类型。

所以,知道操作数的类型,你就会知道操作的效果。

甚至C和Java也有(硬编码的)操作符重载

在C语言中,操作符的实际行为将根据其操作数而改变。例如,两个整数相加不同于两个双精度数相加,甚至一个整数和一个双精度数相加。甚至还有整个指针算术域(在没有强制转换的情况下,您可以向指针添加一个整数,但不能添加两个指针……)

在Java中,没有指针算术,但有人仍然发现没有+操作符的字符串连接将是荒谬的,足以证明“操作符重载是邪恶的”异常。信条。

只是你,作为一个C(由于历史原因)或Java(对于个人原因,见下文)编码器,你不能提供你自己的。

在c++中,操作符重载是不可选的…

在c++中,内置类型的操作符重载是不可能的(这是一件好事),但是用户定义的类型可以有用户定义的操作符重载。

如前所述,与Java相反,在c++中,与内置类型相比,用户类型不被认为是语言的二等公民。因此,如果内置类型具有操作符,那么用户类型也应该能够具有操作符。

事实是,就像toString()clone()equals()方法是用于Java (即quasi-standard-like)一样,c++操作符重载是c++的重要组成部分,它变得像原始的C操作符或前面提到的Java方法一样自然。

与模板编程相结合,操作符重载成为众所周知的设计模式。事实上,在STL中,如果不使用重载操作符,以及为自己的类重载操作符,就不能走得很远。

...但它不应被滥用

操作符重载应尽量尊重操作符的语义。不要在+操作符中使用减法(如“不要在add函数中使用减法”,或“在clone方法中返回废话”)。

强制转换重载可能非常危险,因为它们可能导致歧义。所以它们应该被保留在明确定义的情况下。对于&&||,永远不要重载它们,除非你真的知道你在做什么,因为你将失去本机操作符&&||所享受的短路计算。

所以…好吧……那么为什么在Java中不可能呢?

因为詹姆斯·高斯林说过:

我省略了操作符重载作为相当个人的选择,因为我看到太多的人在c++中滥用它。

詹姆斯·高斯林。来源:http://www.gotw.ca/publications/c_family_interview.htm

请将上面Gosling的文本与下面Stroustrup的文本进行比较:

许多c++设计决策都源于我不喜欢强迫人们以某种特定的方式做事[…]通常,我想取缔一个我个人不喜欢的功能,我克制自己这样做,因为我认为我没有权利把自己的观点强加于人

内定。来源:c++的设计和发展(1.3通用背景)

操作符重载对Java有利吗?

一些对象将从操作符重载中获益良多(具体或数值类型,如BigDecimal、复数、矩阵、容器、迭代器、比较器、解析器等)。

在c++中,由于Stroustrup的谦逊,您可以从这个好处中获益。在Java中,你只是因为Gosling的个人选择而搞砸了。

它能被添加到Java中吗?

现在不在Java中添加操作符重载的原因可能是内部政治、对特性的过敏、对开发人员的不信任(你知道,那些破坏者似乎一直困扰着Java团队……)、与以前的jvm的兼容性、编写正确规范的时间等等。

所以不要屏住呼吸等待这个功能…

但是他们是用c#做的!!

是的…

虽然这不是两种语言之间的唯一区别,但这一点总是让我很开心。

显然,c#的人,用他们的每个原语都是struct,而struct派生自object;,第一次尝试就正确了。

他们在其他语言中做到了!!

尽管FUD反对使用的已定义操作符重载,但以下语言支持它:芬兰湾的科特林Scala飞镖Pythonf#c#D大陵五68SmalltalkGroovyScala0, c++, Scala1, Scala2, Scala3, Scala4, Scala5, Scala6, Scala7, Scala8, Scala9, 飞镖0…

如此多的语言,有如此多不同的(有时是相反的)哲学,但他们都同意这一点。

发人深思……

说操作符重载会导致操作符不匹配操作逻辑类型的逻辑错误,这就像什么都没说。如果函数名不适合操作逻辑,也会出现相同类型的错误——那么解决方案是什么:放弃使用函数的能力!? 这是一个滑稽的回答——“不适合操作逻辑”,每个参数名、每个类、函数或任何东西都可能在逻辑上不合适。 我认为这个选项应该在受人尊敬的编程语言中可用,而那些认为它不安全的人——嘿,没有人说你必须使用它。 让我们以c#为例。他们放下了指针,但是嘿——有“不安全代码”语句——你想怎么编程就怎么编程,风险自负

有人说Java中的操作符重载会导致混淆。这些人是否曾经停下来查看一些Java代码进行一些基本的数学运算,比如使用BigDecimal将财务值按百分比增加?.... 这种做法的冗长本身就证明了混淆视听。具有讽刺意味的是,向Java中添加运算符重载将允许我们创建自己的Currency类,这将使此类数学代码优雅而简单(不那么混乱)。

从技术上讲,每一种编程语言都有运算符重载,可以处理不同类型的数字,例如整数和实数。解释:术语重载意味着一个函数有多个实现。在大多数编程语言中,为运算符+提供了不同的实现,一个用于整数,一个用于实数,这被称为运算符重载。

现在,很多人发现Java为操作符+重载来将字符串加在一起很奇怪,从数学的角度来看,这确实很奇怪,但从编程语言开发者的角度来看,为其他类(如String)的操作符+添加内置操作符重载并没有什么错。然而,大多数人都同意,一旦你为+ for String添加了内置重载,那么为开发人员提供这个功能通常是一个好主意。

我完全不同意操作符重载会混淆代码的谬论,因为这是由开发人员决定的。这是naïve的想法,老实说,它已经过时了。

+1用于在Java 8中添加操作符重载。

这不是一个很好的理由来禁止它,而是一个实际的理由:

人们并不总是负责任地使用它。请看下面这个来自Python库scapy的例子:

>>> IP()
<IP |>
>>> IP()/TCP()
<IP frag=0 proto=TCP |<TCP |>>
>>> Ether()/IP()/TCP()
<Ether type=0x800 |<IP frag=0 proto=TCP |<TCP |>>>
>>> IP()/TCP()/"GET / HTTP/1.0\r\n\r\n"
<IP frag=0 proto=TCP |<TCP |<Raw load='GET / HTTP/1.0\r\n\r\n' |>>>
>>> Ether()/IP()/IP()/UDP()
<Ether type=0x800 |<IP frag=0 proto=IP |<IP frag=0 proto=UDP |<UDP |>>>>
>>> IP(proto=55)/TCP()
<IP frag=0 proto=55 |<TCP |>>

下面是解释:

/操作符被用作两个操作符之间的复合操作符 层。当这样做时,下层可以有一个或多个它的 默认字段根据上层重载。(你仍然 可以给出你想要的值)。字符串可以用作原始层

Java操作符重载本机支持的替代方案

因为Java没有操作符重载,这里有一些你可以考虑的替代方案:

  1. 使用另一种语言。GroovyScala和Kotlin具有操作符重载,并且基于Java。
  2. 使用java-oo,一个在Java中支持操作符重载的插件。注意,它不是平台独立的。此外,它有许多问题,并且与Java的最新版本(即Java 10)不兼容。(StackOverflow原始源代码)
  3. 使用JNI、Java本机接口或其他选项。这允许您编写在Java中使用的C或c++方法。当然,这也不是平台独立的。

如果有人知道其他人,请评论,我会把它添加到这个列表中。

我认为人们在做决定时忘记了复杂的值、矩阵代数、集合论和其他情况,因为重载可以使用标准的符号,而不用把所有东西都构建到语言中。无论如何,只有面向数学的软件才能真正受益于这些特性。一般的客户应用程序几乎不需要它们。

当程序员定义一些特定于程序的操作符时,这些关于不必要混淆的参数显然是有效的,而这些操作符可能是函数。函数名在清晰可见时,提供了函数名存在的提示。运算符是一个没有可读名称的函数。

Java通常是按照这样的理念设计的:一些额外的冗长并不是坏事,因为它使代码更具可读性。具有相同功能的结构只是需要输入的代码更少,过去被称为“语法糖”。在过去。这与Python的哲学非常不同,例如,在Python哲学中,更短的几乎总是被视为更好的,即使为第二个读者提供的上下文更少。

Java不允许操作符重载,因为它的创建者没有添加将不同含义与同一个操作符关联起来的功能。他们只是想让事情简单化通过在整个编程语言中保持运算符统一的含义来实现。

重载操作符会使事情变得混乱,并为新程序员创造了陡峭的学习曲线。所以,他们只是把操作符重载排除在教学大纲之外。