从C++到Java,明显没有答案的问题是为什么Java不包含运算符重载?
是不是Complex a, b, c; a = b + c;
比简单得多Complex a, b, c; a = b.add(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中的Cloneable
接口:
您应该克隆实现此接口的对象.但你可以说谎.并创建一个不同的对象.实际上,这个界面非常弱,你可以完全返回另一种类型的对象,只是为了它的乐趣:
class MySincereHandShake implements Cloneable { public Object clone() { return new MyVengefulKickInYourHead() ; } }
由于Cloneable
接口可能被滥用/混淆,是否应该以相同的理由禁止C++运算符重载?
我们可以重载类的toString()
方法,MyComplexNumber
让它返回当天的字符串化小时.是否应该toString()
禁止超载?我们可以破坏MyComplexNumber.equals
它返回一个随机值,修改操作数......等等.
在Java中,如在C++或任何语言中,程序员在编写代码时必须遵守最少的语义.这意味着实现一个add
添加的函数,Cloneable
克隆的实现方法和一个++
运算符而不是递增.
现在我们知道即使通过原始的Java方法也可以破坏代码,我们可以问自己C++中运算符重载的真正用法吗?
对于不同的情况,我们将在下面比较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
对象,使用Google上的第一个链接找到" 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
而且这不仅限于矩阵.该BigInteger
和BigDecimal
的Java类来自同一个混乱的冗长痛苦,而他们在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"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讨论运算符重载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++中,内置类型的运算符重载是不可能的(这是一件好事),但用户定义的类型可能具有用户定义的运算符重载.
如前所述,在C++中,与Java相反,与内置类型相比,用户类型不被视为该语言的二等公民.因此,如果内置类型具有运算符,则用户类型也应该能够拥有它们.
事实是,像toString()
,clone()
,equals()
方法是对Java(即准标准样),C++运算符重载是C的这么多的部分++,它变得一样自然原来的C经营者,或之前提到的Java方法.
结合模板编程,操作符重载成为众所周知的设计模式.实际上,如果不使用重载运算符,并且为自己的类重载运算符,则无法在STL中走得很远.
运算符重载应该努力尊重运算符的语义.不要在+
运算符中减去(如"不在add
函数中减去"或"在clone
方法中返回垃圾").
转换重载可能非常危险,因为它们可能导致含糊不清.因此,它们应该被保留用于明确定义的案例.至于&&
和||
,永远不要超载它们,除非你真的知道自己在做什么,因为你会失去短路评价的本土运营商&&
和||
享受.
因为詹姆斯·高斯林这么说:
我遗漏了操作符重载作为一个相当个人的选择,因为我看到有太多人在C++中滥用它.
詹姆斯戈斯林.资料来源:http://www.gotw.ca/publications/c_family_interview.htm
请将Gosling上面的文字与下面的Stroustrup进行比较:
许多C++设计决策源于我不喜欢强迫人们以某种特定方式做事[...]通常,我很想禁止我个人不喜欢的功能,我没有这样做因为我认为我没有这样做强迫我对别人的看法的权利.
Bjarne Stroustrup.资料来源:C++的设计和演变(1.3一般背景)
一些对象将极大地受益于运算符重载(具体或数字类型,如BigDecimal,复数,矩阵,容器,迭代器,比较器,解析器等).
在C++中,由于Stroustrup的谦逊,你可以从这个好处中获益.在Java中,由于Gosling的个人选择,你只是被搞砸了.
现在不在Java中添加运算符重载的原因可能是内部政治,对功能的过敏,开发人员的不信任(你知道,似乎困扰Java团队的破坏者......),与以前的JVM的兼容性,是时候写一个正确的规格等了.
所以不要屏住呼吸等待这个功能......
是啊...
虽然这远不是两种语言之间的唯一区别,但这一点永远不会让我高兴.
显然,C#伙伴们,他们的"每一个原语都是一个struct
,而一个struct
派生自对象",在第一次尝试时就做对了.
尽管所有FUD都反对使用定义的运算符重载,但以下语言支持它:Scala,Dart,Python,F#,C#,D,Algol 68,Smalltalk,Groovy,Perl 6,C++,Ruby,Haskell,MATLAB,Eiffel,Lua,Clojure,Fortran 90,Swift,Ada,Delphi 2005 ......
如此多的语言,有许多不同的(有时是对立的)哲学,但他们都同意这一点.
值得思考的东西......
James Gosling将Java设计为以下内容:
"当你从一个公寓搬到另一个公寓时,有一个关于移动的原则.一个有趣的实验是打包你的公寓,把所有东西放在盒子里,然后进入下一个公寓,不要打开任何东西直到你需要它.所以你'重新开始你的第一顿饭,然后你从盒子里拿出一些东西.然后在一个月左右之后你就用它来弄清楚你生活中你真正需要的东西,然后你拿走其余的东西.东西 - 忘记你喜欢多少或多酷 - 你只是扔掉它.令人惊讶的是它如何简化你的生活,你可以在各种设计问题中使用这个原则:不要仅仅因为他们做事情"很酷或只是因为他们很有趣."
您可以在此处阅读报价的上下文
基本上,运算符重载对于模拟某种点,货币或复数的类非常有用.但在那之后你开始快速耗尽示例.
另一个因素是开发人员重载运算符滥用C++中的功能,例如'&&','||',强制转换操作符,当然还有'new'.Exceptional C++一书中详细介绍了将其与值和异常传递相结合所带来的复杂性.
查看Boost.Units:链接文本
它通过运算符重载提供零开销的维度分析.这会更清楚多少?
quantityF = 2.0*newton; quantity dx = 2.0*meter; quantity E = F * dx; std::cout << "Energy = " << E << endl;
实际上会输出"Energy = 4 J",这是正确的.
假设您想要覆盖引用的对象的先前值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
现在指的是相同的值.结果,比较将产生"相等",因为对象将比较自身.
副本和引用之间的差异只会增加操作符重载的混乱.正如@Sebastian所提到的,Java和C#都必须分别处理值和引用相等 - operator+
可能会处理值和对象,但是operator=
已经实现了处理引用.
在C++中,您应该一次只处理一种比较,因此可以减少混淆.例如,打开Complex
,operator=
并且operator==
都在处理值 - 分别复制值和比较值.
Java设计者认为运算符重载比它的价值更麻烦.就那么简单.
在一种语言中,每个对象变量实际上都是一个引用,运算符重载会带来额外的不合逻辑的危险 - 至少对C++程序员来说.将情况与C#的==运算符重载Object.Equals
和Object.ReferenceEquals
(以及它所谓的)进行比较.
Groovy有运算符重载,并在JVM中运行.如果你不介意性能损失(每天变小).它是基于方法名称自动完成的.例如,'+'调用'plus(argument)'方法.
我认为这可能是一种有意识的设计选择,迫使开发人员创建名称清楚地传达其意图的功能.在C++中,开发人员会使运算符过载,这些功能通常与给定运算符的普遍接受性质无关,这使得在不查看运算符定义的情况下几乎无法确定代码片段的作用.
那么你可以通过操作员超载来拍摄自己的脚.就像指针一样,人们会犯愚蠢的错误,因此决定将剪刀拿走.
至少我认为这就是原因.无论如何我都在你身边.:)
有人说Java中的运算符重载会导致混淆。那些人是否曾经停下来看一些Java代码做一些基本的数学运算,例如使用BigDecimal来增加财务价值?....这种做法的冗长性成为其自身对混淆的证明。具有讽刺意味的是,将运算符重载添加到Java中将使我们能够创建自己的Currency类,这将使此类数学代码既优雅又简单(减少混淆)。