从C ++到Java,一个显而易见的未解决问题是Java为什么不包括运算符重载?
没有Complex a, b, c; a = b + c;
比这简单Complex a, b, c; a = b.add(c);
吗?
是否存在已知的原因,有效的论据,不使运算符重载?原因是任意的,还是迷失了时间?
从C ++到Java,一个显而易见的未解决问题是Java为什么不包括运算符重载?
没有Complex a, b, c; a = b + c;
比这简单Complex a, b, c; a = b.add(c);
吗?
是否存在已知的原因,有效的论据,不使运算符重载?原因是任意的,还是迷失了时间?
Answers:
假设您想覆盖所引用的对象的先前值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==
都致力于价值-复制值分别比较值。
有很多帖子抱怨操作员超载。
我觉得我必须澄清“操作员超载”的概念,对此概念提供另一种观点。
这种说法是谬论。
通过函数/方法对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
并且这不限于矩阵。Java 的BigInteger
和BigDecimal
类具有相同的混淆性冗长的术语,而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 ++中,不可能对内置类型进行运算符重载(这是一件好事),但是用户定义的类型可以具有用户定义的运算符重载。
如前所述,在C ++中(与Java相反),与内置类型相比,用户类型不被视为该语言的二等公民。因此,如果内置类型具有运算符,则用户类型也应具有它们。
事实是,像toString()
,clone()
,equals()
方法是对Java(即准标准样),C ++运算符重载是C的这么多的部分++,它变得一样自然原来的C运营商,或所提及的Java方法之前。
结合模板编程,操作员重载已成为众所周知的设计模式。实际上,如果不使用重载运算符,并且不为自己的类重载运算符,就无法在STL中走得太远。
运算符重载应努力尊重运算符的语义。不要在+
运算符中进行减法(如“在add
函数中不减法”或“在clone
方法中返回废话”中所述)。
转换过载可能非常危险,因为它们可能导致歧义。因此,应将它们确实保留用于定义明确的案例。至于&&
和||
,永远不要超载它们,除非你真的知道自己在做什么,因为你会失去短路评价的本土运营商&&
和||
享受。
因为詹姆斯·高斯林(James Gosling)这样说:
我没有将运算符重载作为个人选择,因为我看到太多人在C ++中滥用它。
詹姆斯·高斯林。资料来源:http://www.gotw.ca/publications/c_family_interview.htm
请比较上面的高斯林的文字和下面的Stroustrup的文字:
许多C ++的设计决策有他们在我的厌恶根迫使人们做的事情在一些特殊的方式[...]通常情况下,我很想取缔一个功能,我个人不喜欢,我没有这样做,因为我不认为我有将我的观点强加给他人的权利。
Bjarne Stroustrup。资料来源:C ++的设计和演变(1.3总体背景)
某些对象将从运算符重载(诸如BigDecimal,复数,矩阵,容器,迭代器,比较器,解析器等具体或数字类型)中受益匪浅。
在C ++中,由于Stroustrup的谦逊,您可以从中受益。在Java中,由于Gosling的个人选择,您只会被搞砸。
现在在Java中不添加运算符重载的原因可能是内部政治,对功能的过敏,对开发人员的不信任(您似乎破坏了Java团队的破坏者...),与以前的JVM的兼容性,是时候编写正确的规范等了。
因此,不要屏住呼吸等待此功能...
是的
虽然这远不是两种语言之间的唯一区别,但是这从未使我感到高兴。
显然,C#人员以“每个原语都是a struct
,并且是struct
从Object派生的”,在第一次尝试时就正确了。
尽管所有针对使用已定义的运算符重载的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 ++本书很好地涵盖了将其与按值传递和异常相结合而导致的复杂性。
Many C++ design decisions have their roots in my dislike for forcing people to do things in some particular way [...] Often, I was tempted to outlaw a feature I personally disliked, I refrained from doing so because I did not think I had the right to force my views on others. (B. Stroustrup)
。
I'd like them to go have a look at some C++ code out there that is hideously put together with weird hacks and "exceptional" features of the language
:无论语言如何,不良程序员都会编写不良代码。只需尝试模拟Java中函数参数的“传递引用”即可。我看过代码,笑得很伤心。这是Gosling没使用过的东西,因此需要Java拥有骇人的骇客,但C#和C ++都以零成本原生存在。
查看Boost.Units:链接文本
它通过运算符重载提供零开销的维分析。这能弄清楚多少?
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”,这是正确的。
Java设计人员认为,运算符重载比它值得的麻烦更多。就那么简单。
在每个对象变量实际上都是引用的语言中,运算符重载会带来额外的危险,即至少对于C ++程序员而言,这是非常不合逻辑的。将情况与C#的==运算符重载和Object.Equals
和Object.ReferenceEquals
(或任何称为)进行比较。
Groovy具有运算符重载,并在JVM中运行。如果您不介意性能下降(每天变小)。它是根据方法名称自动生成的。例如,“ +”调用“加号(参数)”方法。
where ...
成为.Where(i => ...
)采用了正确的方法。如果仅对算术运算符执行相同的操作,那么很多事情将变得更加简单和强大。Java具有干净的优势,并且可以做到这一点(尽管出于宗教原因,它可能永远不会这样做)。
我认为这可能是一种有意识的设计选择,它可以迫使开发人员创建名称明确传达其意图的功能。在C ++中,开发人员将使运算符过载,而这些功能通常与给定运算符的普遍接受的性质无关,从而使得几乎不可能在不查看运算符定义的情况下确定一段代码的功能。
In C++ developers would overload operators with functionality that would often have no relation to the commonly accepted nature of the given operator
:这是一个毫无根据的断言。我从12年以来一直是专业的C ++开发人员,而且很少遇到此问题。实际上,我在C ++中看到的大多数错误和设计错误都是C风格的代码(void *
,强制转换等)
add
功能可能真的被滥用了(例如进行乘法或获取互斥锁)……user14128提到的滥用不仅限于运算符,而是我认为对运算符重载有一些病理上的恐惧,我认为这是C和C ++的较早时期产生的一种恐惧,这种恐惧未经修改就直接传入Java中,但值得庆幸的是,它没有加入C#中……最后,尊重语义编写清晰的功能/操作员是开发人员的工作。不是语言的。
cout << f() || g();
括号没有使它更清楚,它们使它正确。而且它不会滥用位运算符,因此没有必要。为什么cout << (5&3) << endl;
优于cout.fmt(5&3)(endl);
?在函子成员变量上使用函数调用运算符将比使用位运算符仅仅因为字形看起来不错而对流进行无限优化。但这远非流唯一的问题。
好吧,您真的可以在操作员超载的情况下踩死自己。就像指针使人们犯了愚蠢的错误,因此决定将剪刀剪掉。
至少我认为这是原因。无论如何我都站在你这边。:)
从技术上讲,每种编程语言中都有运算符重载,可以处理不同类型的数字,例如整数和实数。说明:重载一词意味着一个功能只有几种实现。在大多数编程语言中,为+运算符提供了不同的实现,一个为整数,一个为实数,这称为运算符重载。
现在,很多人感到奇怪的是,Java的操作符+会将运算符+用于将字符串加在一起,这确实很奇怪,但是从编程语言开发人员的角度来看,添加内置的运算符重载没有任何问题。对于运算符+对于其他类,例如String。但是,大多数人都同意,一旦为String为+添加了内置重载,那么通常也为开发人员提供此功能是个好主意。
完全不同意运算符重载会混淆代码的谬误,因为这留给开发人员来决定。想想是天真,老实说,它已经老了。
+1用于在Java 8中添加运算符重载。
+
用来连接任何字符串形式的东西是恕我直言的,这很可怕,因为/
C和FORTRAN中的重载用于整数和分数除法。在许多Pascal版本中,在任何数值类型上使用算术运算符都会产生数值上等同于将操作数强制转换为的Real
结果,尽管可能不是整数的结果必须被馈入Trunc
或Round
分配给整数之前。
假设将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);
有时,最好有运算符重载,朋友类和多重继承。
但是我仍然认为这是一个不错的决定。如果Java会有运算符重载,那么如果不查看源代码,我们将永远无法确定运算符的含义。目前没有必要。而且我认为您使用方法代替运算符重载的示例也很容易理解。如果您想使事情更清楚,您可以随时在多毛的陈述上方添加注释。
// a = b + c
Complex a, b, c; a = b.add(c);
这不是一个不允许这样做的好理由,而是一个实际的理由:
人们并不总是负责任地使用它。在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项目中使用Manifold编译器插件来启用它。它支持Java 8-13(当前的Java版本),并且在IntelliJ IDEA中得到完全支持。