从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() ;
}
}
// 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 ;
// 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
// 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
类也遭受着同样令人困惑的冗长问题,而它们在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中,没有指针算术,但是有人仍然发现没有使用+
运算符的字符串连接足够荒谬,以至于可以证明"运算符重载是邪恶的"信条存在例外。toString()
、clone()
、equals()
方法一样(即准标准),C++运算符重载已经成为C++的一部分,就像原始的C运算符或前面提到的Java方法一样自然。+
运算符中减去(例如“不要在add
函数中减去”,或者“在clone
方法中返回垃圾”)。&&
和 ||
,除非您确实知道自己在做什么,否则不要重载它们,因为您将失去本机运算符 &&
和 ||
所享有的短路评估。一些对象会极大地受益于操作符重载(具体或数值类型,如 BigDecimal、复数、矩阵、容器、迭代器、比较器、解析器等)。
在 C++ 中,你可以从中获益,因为 Stroustrup 很谦虚。在 Java 中,你只是因为 Gosling 的个人选择而束手无策。
现在在 Java 中不添加操作符重载的原因可能是内部政治、对该功能的过敏、对开发者的不信任(你知道,那些似乎总是困扰 Java 团队的破坏分子...)、与以前的 JVM 兼容性、编写正确规范的时间等。
所以不要指望这个功能能够很快被添加到 Java 中...
是的...
虽然这远非两种语言之间唯一的区别,但这个区别总是让我感到有趣。
显然,C# 的人们通过他们的 "每个基元都是一个结构体,并且结构体派生自 Object",第一次尝试就做对了。
尽管有很多关于使用自定义运算符重载的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...
有这么多语言,它们有各自不同(有时甚至相反)的哲学观点,但它们都在这一点上达成了共识。
值得深思...
詹姆斯·高斯林(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;
实际输出为"能量= 4焦耳",这是正确的。
假设你想要覆盖由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==
都是处理值 - 复制值和比较值。
Groovy具有运算符重载功能,并且可以在JVM上运行。如果您不介意性能损失(这种损失每天都在减小),它会根据方法名称自动执行。例如,'+'调用'plus(argument)'方法。
where ...
变成 .Where(i => ...
)。如果他们也用算术运算符做同样的事情,那么很多事情将变得更简单而且更强大。Java拥有一个干净的起点,可以做到这一点(尽管出于宗教原因,它可能永远不会这样做)。 - Daniel Earwicker有些人说在Java中进行运算符重载会导致混淆。但是这些人是否曾经停下来看过一些Java代码,比如使用BigDecimal对金融价值增加百分比的基本数学计算?……这种练习的冗长程度成为了混淆的证明。具有讽刺意味的是,在Java中添加运算符重载将允许我们创建自己的货币类,使这样的数学代码变得优雅而简单(不再混淆)。
我认为这可能是一种有意的设计选择,旨在强制开发人员创建其名称清晰表达其意图的函数。在C++中,开发人员会重载运算符,并赋予与给定运算符的通常接受性质完全不相关的功能,使得几乎不可能在不查看运算符定义的情况下确定代码的作用。
void *
,强制类型转换等)。 - paercebaladd
函数可能会被非常滥用(比如做乘法,或获取互斥锁)... 用户14128提到的滥用不限于运算符,但是有一种关于运算符重载的病态恐惧感,我认为这源自 C 和 C++ 早期的竞争。这种恐惧一直延续到了 Java,但幸运的是没有出现在 C# 中... 最终,尊重语义并编写清晰的函数/运算符是开发人员的工作,而不是语言的责任。 - paercebalcout << f() || g()
中的括号并没有使其更清晰,而是使其正确。如果没有滥用位移操作符,它们就不是必要的。为什么 cout << (5&3) << endl;
比 cout.fmt(5&3)(endl);
更好?在流中使用函数调用运算符与成员变量结合会比仅仅因其外形美观而重用位运算符更好。但这远非流设计中唯一的问题。 - Ben Voigt+
连接任何字符串都相当丑陋,C和FORTRAN中/
的重载也是如此。在许多版本的Pascal中,对任何数字类型使用算术运算符将产生数值上等同于将操作数转换为“Real”的结果,尽管可能不是整数的结果必须通过Trunc
或Round
进行处理,然后才能分配给整数。 - supercat
==
运算符。每次从GO、PHP、RUBY、Python等返回后,我总会犯错。 - Gellweiler