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

复合物不是a b c吗?A = b + c;比复合物a, b, c简单得多;A = b.add(c);?

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


当前回答

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

其他回答

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

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

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

不是: 复合物a, b, c;A = b + c; 简单得多: 复合物a, b, c;A = b.add(c);

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

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

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

代码混淆?

这个论点是谬论。

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

在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() ;
    }
}

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

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

在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对象,使用谷歌上找到的“Java Matrix对象”和“c++ Matrix对象”的第一个链接:

// 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中的BigInteger和BigDecimal类也有同样令人困惑的冗长之处,而c++中的BigInteger和BigDecimal类就像内置类型一样清晰。

自然的迭代器:

// 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(即准标准),c++操作符重载是c++的重要组成部分,它变得像原始的C操作符或前面提到的Java方法一样自然。

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

...但它不应被滥用

操作符重载应尽量尊重操作符的语义。不要在+运算符中使用减法(就像“不要在add函数中使用减法”,或者“在克隆方法中返回废话”一样)。

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

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

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

我没有使用操作符重载,这是我个人的选择,因为我见过太多的人在c++中滥用它。 詹姆斯·高斯林。来源:http://www.gotw.ca/publications/c_family_interview.htm

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

许多c++设计决策都源于我不喜欢强迫人们以某种特定的方式做事[…]通常,我很想取缔我个人不喜欢的功能,但我克制自己不这么做,因为我认为我没有权利把自己的观点强加给别人。 内定。来源:c++的设计和发展(1.3通用背景)

操作符重载对Java有利吗?

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

在c++中,由于Stroustrup的谦逊,您可以从这个好处中获益。在爪哇,你被Gosling的个人选择搞砸了。

它能被添加到Java中吗?

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

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

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

是的…

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

显然,c#的人,他们的“每个原语都是一个结构体,而结构体派生于对象”,在第一次尝试时就得到了正确的答案。

他们用其他语言也这么做!!

尽管所有的FUD都反对使用已定义的操作符重载,但以下语言支持它:Kotlin, Scala, Dart, Python, f#, c#, D, Algol 68, Smalltalk, Groovy, Raku(以前是Perl 6), c++, Ruby, Haskell, MATLAB, Eiffel, Lua, Clojure, Fortran 90, Swift, Ada, Delphi 2005…

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

发人深思……

假设您想要覆盖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=执行引用复制,因此a和b现在引用相同的值。结果,比较将产生'equal',因为对象的比较结果将等于自身。

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

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