为什么Java不提供运算符重载?
从C ++到Java,一个显而易见的未解决的问题是为什么Java没有包含运算符重载?
不是Complex a, b, c; a = b + c;
Complex a, b, c; a = b + c;
比Complex a, b, c; a=b.add(c);
简单得多Complex a, b, c; a=b.add(c);
Complex a, b, c; a=b.add(c);
?
这是否有一个已知的原因,不允许运算符重载的有效参数? 原因是武断的,还是失去了时间?
假设你想覆盖'a'引用的对象的前一个值,那么就必须调用一个成员函数。
Complex a, b, c; ... a = b.add(c)
在C ++中,该expression式告诉编译器在堆栈上创build3个对象,执行加法,并将临时对象的结果值复制到现有对象“a”中。
但是,在java中,operator =不会为引用types执行值复制,用户只能创build新的引用types,而不能创build值types。 因此,对于名为“Complex”的用户定义types,赋值意味着将引用复制到现有值。
请考虑:
b.set(1, 0); // initialize to real number '1' a = b; b.set(2, 0); assert(!a.equals(b));
在C ++中,这会复制值,所以比较结果将不相等。 在Java中,operator =执行引用复制,所以'a'和'b'现在引用相同的值。 结果,比较将产生“相等”,因为对象将等于自己。
副本和引用之间的差异只会增加运算符重载的混乱。 正如塞巴斯蒂安所说的,Java和C#都必须分别处理值和引用的等式 – operator +可能会处理值和对象,但operator =已经被实现来处理引用。
在C ++中,你应该只是一次处理一种比较,所以它可以减less混淆。 例如,在Complex上,operator =和operator ==都在处理值 – 分别复制值和比较值。
有很多post抱怨运营商超载。
我觉得我不得不澄清“运营商超载”的概念,提供了一个替代观点这个概念。
代码混淆?
这个论点是一个谬论。
所有语言都可以进行混淆…
在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
接口 :
你应该克隆实现这个接口的对象。 但是你可以说谎。 并创build一个不同的对象。 事实上,这个接口是如此薄弱,你可以完全返回另一种types的对象,只是为了它的乐趣:
class MySincereHandShake implements Cloneable { public Object clone() { return new MyVengefulKickInYourHead() ; } }
由于Cloneable
接口可以被滥用/混淆,应该禁止在相同的理由C ++运算符重载应该是?
我们可以重载MyComplexNumber
类的toString()
方法,让它返回一天中string化的时间。 是否应该禁止toString()
重载? 我们可以破坏MyComplexNumber.equals
,让它返回一个随机值,修改操作数等等等等。
在Java中,如C ++或其他语言一样,程序员在编写代码时必须遵守最less的语义。 这意味着实现一个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 ++中的任何types。 在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 ++中,每个容器都使用相同的方式来访问其内容,这要归功于操作符重载。
自然先进的types操纵
下面的例子使用了一个Matrix
对象,通过在Google上find的“ Javamatrix对象 ”和“ c ++matrix对象 ”的第一个链接find:
// 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
这不仅限于matrix。 Java的BigInteger
和BigDecimal
类遭受相同的混淆冗长,而C ++中的等价物与内置types一样清晰。
自然迭代器:
// 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
自然的function:
// 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" ;
太…但是,等一下:这是运营商超载,不是吗? 是不是在作弊?
😀
通用代码?
相同的通用代码修改操作数应该既可用于内置/基元(在Java中没有接口),也可以用于标准对象(不能有正确的接口)和用户定义的对象。
例如,计算任意types的两个值的平均值:
// 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中相同的代码之间的公平比较,现在我们可以讨论“运算符重载”这个概念。
运算符在计算机之前已经存在超载
即使在计算机科学之外,也存在运算符重载:例如,在math中,像+
, -
, *
等运算符被重载。
事实上, +
, -
, *
等的含义根据操作数的types(数值,vector,量子波函数,matrix等)而变化。
作为我们科学课程的一部分,我们大多数人根据操作数的types学习了操作符的多重意义。 我们发现他们混淆了吗?
操作符重载取决于其操作数
这是运算符重载的最重要的部分:就像math或物理学一样,运算取决于其操作数的types。
所以,知道操作数的types,你就会知道操作的效果。
即使C和Java也有(硬编码)运算符重载
在C中,操作符的真实行为将根据其操作数而改变。 例如,添加两个整数不同于添加两个双精度,甚至不是一个整数和一个双精度。 甚至有整个指针算术域(没有转换,你可以添加一个指针的整数,但你不能添加两个指针…)。
在Java中,没有指针算术,但有人仍然发现string连接没有+
运算符将是荒谬的,足以certificate“运算符重载是邪恶的”信条exception。
只是你,作为一个C(出于历史原因)或Java(出于个人原因 ,见下文)编码器,你不能提供你自己的。
在C ++中,运算符重载不是可选的
在C ++中,内置types的操作符重载是不可能的(这是一件好事),但是用户定义的types可以具有用户定义的操作符重载。
如前所述,在C ++中,与Java相反,与内置types相比,用户types不被认为是该语言的二级公民。 所以,如果内置types有运营商,用户types也应该能够拥有它们。
事实是,像toString()
, clone()
, equals()
方法是针对Java的( 即类似准标准的 ),C ++运算符重载是C ++的重要组成部分,因此它变得和原始C运算符一样自然或之前提到的Java方法。
结合模板编程,运算符重载成为众所周知的devise模式。 事实上,如果不使用重载操作符,并且为自己的类重载操作符,则不能在STL中走得太远。
…但不应该被滥用
运算符重载应力求尊重运算符的语义。 不要在+
运算符中减去(如“在add
函数中不减”或“在clone
方法中返回垃圾”)。
抛出重载可能是非常危险的,因为它们可能导致含糊不清。 所以他们应该保留下来,以确定明确的情况。 至于&&
和||
,除非你真的知道你在做什么,否则不要重载他们,因为你将失去本地运营商的短路评估&&
和||
请享用。
所以…好吧…那为什么在Java中不可能呢?
因为詹姆斯·高斯林这样说:
我忽略了操作符重载,因为我看到有太多的人在C ++中滥用它。
詹姆斯·高斯林 资料来源: http : //www.gotw.ca/publications/c_family_interview.htm
请将上面的Gosling的文本与Stroustrup的以下内容进行比较:
许多C ++的devise决定都源于我不喜欢强迫人们以某种特定的方式来做事情[…]通常,我被诱惑取缔我个人不喜欢的function,我没有这样做,因为我没有想到我有有权强行对别人的看法 。
Bjarne Stroustrup。 来源:C ++的devise与发展(1.3一般背景)
运算符重载会使Java受益吗?
一些对象将从运算符重载(具体或数字types,如BigDecimal,复数,matrix,容器,迭代器,比较器,parsing器等)中受益匪浅。
在C ++中,由于Stroustrup的谦逊,您可以从中受益。 在Java中,由于Gosling的个人select ,你只是被搞砸了。
它可以被添加到Java?
现在在Java中不增加运算符重载的原因可能是内部政治,对特性的过敏,对开发者的不信任(你知道,似乎困扰Java团队的破坏者),与以前的JVM的兼容性,时间写一个正确的规范等。
所以不要屏住呼吸等待这个function…
但他们在C#中做!
是啊…
虽然这不是两种语言之间的唯一区别,但这个永远不会令我感到厌倦。
显然,C#的人,他们的“每一个原语是一个struct
,并从一个struct
派生的对象” ,一开始就做对了。
而且他们用其他语言来做!
尽pipe所有的FUD反对使用定义的运算符重载,但下列语言支持它: Scala , Dart , Python , F# , C# , D , Algol 68 , Smalltalk , Groovy , Perl 6 ,C ++, Ruby , Haskell , MATLAB , Eiffel , Clojure , Fortran 90 , Swift , Ada , Delphi 2005 …
那么多的语言,有很多不同的(有时是相反的)哲学,但是他们都同意这一点。
食物的思想…
詹姆斯·高斯林(James Gosling)将Java的devise比喻为:
“当你从一个公寓搬到另一个公寓的时候,有一个有趣的实验,一个有趣的实验是收拾你的公寓,把所有东西都放在箱子里,然后搬进下一个公寓,在你需要的时候不要打开任何东西,重新制作你的第一顿饭,然后你从盒子里拿出东西,然后在一个月左右的时间里,你已经用它来了解你生活中真正需要的东西,然后把剩下的东西这些东西 – 忘记你喜欢多less或者多酷 – 然后你就把它扔掉了,这简直让你的生活变得简单,而且你可以在各种devise问题中使用这个原理:不要只因为它们很酷,或者只是因为它们很有趣。“
您可以在这里阅读引用的上下文
基本上,运算符重载对模拟某种点,货币或复数的类非常有用。 但在此之后,您将很快开始耗尽示例。
另一个因素是开发人员滥用C ++中的function,例如“&&”,“||”,演员操作符,当然还有“新”。 Exceptional C ++书中详细介绍了将这个与传值和exception相结合导致的复杂性。
查看Boost.Units: 链接文本
它通过运算符重载提供了零开销的维度分析。 这能得到多less清楚?
quantity<force> F = 2.0*newton; quantity<length> dx = 2.0*meter; quantity<energy> E = F * dx; std::cout << "Energy = " << E << endl;
实际上会输出“能量= 4J”这是正确的。
Javadevise者决定,运算符超载比其值得的更麻烦。 就那么简单。
在每一个对象variables实际上都是一个引用的语言中,运算符重载得到了一个非常不合逻辑的额外危险 – 至less对于一个C ++程序员来说。 比较情况与C#的==运算符重载和Object.Equals和Object.ReferenceEquals(或任何它被称为)。
Groovy有运算符重载,并在JVM中运行。 如果你不介意性能打击(每天都变小)。 它是基于方法名称的自动化。 例如,“+”调用“加(参数)”方法。
那么你真的可以在操作员超载的情况下在自己的脚下开枪自杀。 就像指点人一样,他们犯了愚蠢的错误,所以决定把剪刀带走。
至less我认为这是原因。 无论如何,我在你身边。 🙂
我认为这可能是一个有意识的deviseselect,迫使开发人员创build名称明确expression意图的函数。 在C ++中,开发人员会重载运算符,而这些运算符通常与给定运算符的普遍接受性无关,因此几乎不可能在没有查看运算符的定义的情况下确定一段代码。
说操作符重载导致逻辑错误的types,该操作符不符合操作逻辑,就像什么也没说。 如果函数名称不适合操作逻辑,则会出现相同types的错误 – 那么解决scheme是什么:降低函数使用的能力! 这是一个滑稽的回答 – “不适合操作逻辑”,每个参数名称,每个类,function或任何可能是逻辑上不合适的。 我认为这个选项应该以可敬的编程语言来提供,那些认为它是不安全的 – 嘿,没有人说你必须使用它。 让我们拿C#。 他们下降了指针,但嘿 – 有'不安全的代码'声明 – 程序,只要你喜欢自负风险。
从技术上讲,每种编程语言都有操作符重载,可以处理不同types的数字,例如整数和实数。 说明:术语超载意味着一个函数只有几个实现。 在大多数编程语言中,为运算符+提供了不同的实现,一个用于整数,一个用于实数,这称为运算符重载。
现在很多人都觉得奇怪的是,Java有operator +操作符重载string,从math的angular度来看,这确实是奇怪的,但是从编程语言的开发者的angular度来看,添加内build运算符重载为运营商+其他类如string。 但是,大多数人都同意,一旦为String添加了内置重载+,那么向开发者提供这个function通常也是一个好主意。
完全不同意运算符重载混淆代码的谬误,因为这是由开发人员决定的。 这是天真的想法,而且相当诚实,它正在变老。
在Java 8中增加运算符重载+1。
有人说Java中的运算符重载会导致混淆。 这些人有没有停下来看看一些Java代码做一些基本的math,如使用BigDecimal以百分比增加财务价值? ….这样的练习的冗长成为它自己的混淆的performance。 具有讽刺意味的是,向Java中添加运算符重载将使我们能够创build我们自己的Currency类,从而使这样的math代码优雅而简单(less混淆)。
假设Java作为实现语言,那么a,b和c都将是引用typesComplex,初始值为null。 Also assuming that Complex is immutable as the mentioned BigInteger and similar immutable BigDecimal , I'd I think you mean the following, as you're assigning the reference to the Complex returned from adding b and c, and not comparing this reference to a.
Isn't :
Complex a, b, c; a = b + c;
much simpler than:
Complex a, b, c; a = b.add(c);
Sometimes it would be nice to have operator overloading, friend classes and multiple inheritance.
However I still think it was a good decision. If Java would have had operator overloading then we could never be sure of operator meanings without looking through source code. At present that's not necessary. And I think your example of using methods instead of operator overloading is also quite readable. If you want to make things more clear you could always add a comment above hairy statements.
// a = b + c Complex a, b, c; a = b.add(c);