从 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);
吗?
是否有一个已知的原因,不允许运算符重载的有效参数?原因是随意的,还是被时间遗忘了?
有很多帖子抱怨运算符重载。
我觉得我必须澄清“操作员重载”的概念,为这个概念提供另一种观点。
这种说法是谬误。
在 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++ 中,由于运算符重载,每个容器使用相同的方式访问其内容。
下面的示例使用了一个对象,该对象是使用在 Google 上找到的“ Java Matrix object ”和“ C++ Matrix objectMatrix
”的第一个链接找到的:
// 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 相反,与内置类型相比,用户类型不被视为该语言的二等公民。因此,如果内置类型有运算符,那么用户类型也应该能够拥有它们。
事实是,就像 Java 的toString()
, clone()
,equals()
方法一样(即类标准),C++ 运算符重载是 C++ 的重要组成部分,以至于它变得与原始 C 运算符或前面提到的 Java 方法一样自然。
结合模板编程,运算符重载成为众所周知的设计模式。事实上,如果不使用重载运算符以及为您自己的类重载运算符,您将无法在 STL 中走得太远。
运算符重载应努力尊重运算符的语义。不要在+
运算符中减去(如“不要在add
函数中减去”或“在clone
方法中返回废话”)。
强制重载可能非常危险,因为它们会导致歧义。因此,它们确实应该保留用于定义明确的案例。至于&&
和,除非您真的知道自己在做什么,否则永远不要使它们超载,因为您将失去本机运算符并享受||
的短路评估。&&
||
因为詹姆斯·高斯林是这么说的:
我忽略了运算符重载作为一个相当个人的选择,因为我看到太多人在 C++ 中滥用它。
詹姆斯·高斯林。资料来源:http ://www.gotw.ca/publications/c_family_interview.htm
请将上面的 Gosling 的文字与下面的 Stroustrup 的文字进行比较:
许多 C++ 设计决策的根源在于我不喜欢强迫人们以某种特定方式做事 [...] 通常,我很想禁止我个人不喜欢的功能,我没有这样做,因为我认为我没有把我的意见强加给别人的权利。
比亚内·斯特劳斯特鲁普。来源:C++ 的设计与演进(1.3 一般背景)
一些对象将极大地受益于运算符重载(具体或数字类型,如 BigDecimal、复数、矩阵、容器、迭代器、比较器、解析器等)。
在 C++ 中,由于 Stroustrup 的谦逊,您可以从中受益。在 Java 中,您只是因为 Gosling 的个人选择而被搞砸了。
现在不在 Java 中添加运算符重载的原因可能是内部政治、对该功能的过敏、对开发人员的不信任(你知道,那些似乎困扰 Java 团队的破坏者......)、与以前的 JVM 的兼容性,是时候编写正确的规范等了。
所以不要屏住呼吸等待这个功能......
是的...
虽然这远不是两种语言之间的唯一区别,但这种语言总是能让我感到开心。
显然,C# 的人,他们的“每个原语都是 a struct
,astruct
派生自 Object”,第一次尝试就做对了。
尽管所有 FUD 都反对使用定义的运算符重载,但以下语言支持它:Kotlin,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:链接文本
它通过运算符重载提供零开销维度分析。这能清楚多少?
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
(或其他任何名称)进行比较。
假设您想覆盖由 引用的对象的先前值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++ 中,您一次应该只处理一种比较,这样可以减少混乱。例如, onComplex
和operator=
都operator==
在处理值——分别复制值和比较值。
Groovy具有运算符重载,并在 JVM 中运行。如果您不介意性能下降(每天都会变小)。它是基于方法名称的自动的。例如,'+' 调用 'plus(argument)' 方法。
我认为这可能是一种有意识的设计选择,它迫使开发人员创建名称清楚地传达其意图的函数。在 C++ 中,开发人员会使用通常与给定运算符的普遍接受的性质无关的功能来重载运算符,从而几乎不可能在不查看运算符定义的情况下确定一段代码的作用。
好吧,您真的可以在操作员超载的情况下将自己开枪打死。就像人们用指针犯愚蠢的错误一样,所以决定把剪刀拿走。
至少我认为这是原因。反正我是站在你这边的。:)
有人说Java中的运算符重载会导致混淆。那些人有没有停下来看一些 Java 代码做一些基本的数学运算,比如使用 BigDecimal 将财务价值增加一个百分比?....这种练习的冗长变成了它自己的混淆证明。具有讽刺意味的是,将运算符重载添加到 Java 将允许我们创建自己的 Currency 类,这将使此类数学代码优雅而简单(不那么模糊)。
从技术上讲,每种编程语言都存在运算符重载,可以处理不同类型的数字,例如整数和实数。解释:术语重载意味着一个函数只有几个实现。在大多数编程语言中,为运算符 + 提供了不同的实现,一种用于整数,一种用于实数,这称为运算符重载。
现在,很多人觉得奇怪的是Java有运算符重载,用于将字符串加在一起的运算符+,从数学的角度来看这确实很奇怪,但从编程语言的开发人员的角度来看,添加内置运算符重载并没有错用于运算符 + 用于其他类,例如字符串。但是,大多数人都同意,一旦为 + 为 String 添加了内置重载,那么为开发人员提供此功能通常是一个好主意。
A 完全不同意运算符重载混淆代码的谬误,因为这是由开发人员决定的。这种想法太天真了,老实说,它已经变老了。
+1 用于在 Java 8 中添加运算符重载。
说运算符重载导致运算符与运算逻辑不匹配的类型的逻辑错误,就像什么都没说。如果函数名不适合操作逻辑,也会出现同样类型的错误——那么解决办法是什么:放弃函数使用的能力!?这是一个可笑的答案——“不适合操作逻辑”,每个参数名称、每个类、函数或任何可能在逻辑上不合适的东西。我认为这个选项应该在受人尊敬的编程语言中可用,并且那些认为它不安全的人 - 嘿,不,他们说你必须使用它。让我们以 C# 为例。他们放弃了指针,但是嘿 - 有“不安全代码”声明 - 随意编程,风险自负。
假设 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);
Java 运算符重载的本机支持的替代方案
由于 Java 没有运算符重载,因此您可以考虑以下一些替代方案:
如果有人知道其他人,请发表评论,我会将其添加到此列表中。
我认为,当重载允许使用标准符号而不将所有内容构建到语言中时,做出决策的人只是忘记了复杂的值、矩阵代数、集合论和其他情况。无论如何,只有面向数学的软件才能真正受益于这些功能。一个通用的客户应用程序几乎从不需要它们。
当程序员定义一些特定于程序的运算符时,他们关于不必要的混淆的论点显然是有效的,而它可能是函数。函数的名称在清晰可见时提供了它所做的提示。运算符是一个没有可读名称的函数。
Java 通常是基于这样的理念设计的,即一些额外的冗长并不坏,因为它使代码更具可读性。做同样事情的构造只是需要更少的代码来输入,过去被称为“语法糖”。这与 Python 哲学非常不同,例如,即使为第二位读者提供的上下文更少,也总是认为越短越好。
这不是一个禁止它的好理由,而是一个实际的理由:
人们并不总是负责任地使用它。看看 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 - 17(当前的 Java 版本),并在 IntelliJ IDEA 中得到完全支持。