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

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

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

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

James Gosling把设计Java比作:

"There's this principle about moving, when you move from one apartment to another apartment. An interesting experiment is to pack up your apartment and put everything in boxes, then move into the next apartment and not unpack anything until you need it. So you're making your first meal, and you're pulling something out of a box. Then after a month or so you've used that to pretty much figure out what things in your life you actually need, and then you take the rest of the stuff -- forget how much you like it or how cool it is -- and you just throw it away. It's amazing how that simplifies your life, and you can use that principle in all kinds of design issues: not do things just because they're cool or just because they're interesting."

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

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

另一个因素是开发人员在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() ;
    }
}

由于克隆接口可能会被滥用/混淆,是否应该以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…

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

发人深思……

Saying that operator overloading leads to logical errors of type that operator does not match the operation logic, it's like saying nothing. The same type of error will occur if function name is inappropriate for operation logic - so what's the solution: drop the ability of function usage!? This is a comical answer - "Inappropriate for operation logic", every parameter name, every class, function or whatever can be logicly inappropriate. I think that this option should be available in respectable programing language, and those that think that it's unsafe - hey no bothy says you have to use it. Lets take the C#. They drooped the pointers but hey - there is 'unsafe code' statement - program as you like on your own risk.

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

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

Now, many people find it strange that Java has operator overloading for the operator + for adding strings together, and from a mathematical standpoint this would be strange indeed, but seen from a programming language's developer's standpoint, there is nothing wrong with adding builtin operator overloading for the operator + for other classes e.g. String. However, most people agree that once you add builtin overloading for + for String, then it is generally a good idea to provide this functionality for the developer as well.

我完全不同意操作符重载会混淆代码的谬论,因为这是由开发人员决定的。这是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没有操作符重载,这里有一些你可以考虑的替代方案:

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

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

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

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

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

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

重载操作符会使事情变得混乱,并为新程序员创造了陡峭的学习曲线。他们只是把运算符重载从教学大纲中去掉了。