it-swarm.cn

为什么Java不提供运算符重载?

从C++到Java,明显没有答案的问题是为什么Java不包含运算符重载?

Complex a, b, c; a = b + c;不比Complex a, b, c; a = b.add(c);简单得多吗?

这是否有一个已知的原因, not 允许运算符重载的有效参数?这个理由是武断的,还是输给了时间?

382
rengolin

假设您想要覆盖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=执行引用副本,因此ab现在引用相同的值。结果,比较将产生“相等”,因为对象将比较自身。

副本和引用之间的差异只会增加操作符重载的混乱。正如@Sebastian所提到的,Java和C#都必须分别处理值和引用相等 - operator+可能会处理值和对象,但operator=已经实现来处理引用。

在C++中,您应该一次只处理一种比较,因此可以减少混淆。例如,在Complex上,operator=operator==都在处理值 - 分别复制值和比较值。

18
Aaron

有很多帖子抱怨运营商超载。

我觉得我必须澄清“操作员重载”的概念,为这个概念提供另一种观点。

代码混淆?

这种说法是一种谬误。

所有语言都可以进行混淆......

通过函数/方法对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接口

您应该克隆实现此接口的对象。但你可以说谎。并创建一个不同的对象。实际上,这个界面非常弱,你可以完全返回另一种类型的对象,只是为了它的乐趣:

class MySincereHandShake implements Cloneable
{
    public Object clone()
    {
       return new MyVengefulKickInYourHead() ;
    }
}

由于Cloneable接口可以被滥用/混淆,是否应该以相同的理由禁止C++运算符重载?

我们可以重载MyComplexNumber类的toString()方法,让它返回当天的字符串化小时。是否应该禁止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的BigIntegerBigDecimal类遭受同样令人困惑的冗长,而它们在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(由于历史原因)或Java(对于 私人原因,见下文)编码器,你不能提供自己的。

在C++中,运算符重载不是可选的......

在C++中,内置类型的运算符重载是不可能的(这是一件好事),但是 用户自定义 类型可以有 用户自定义 运算符重载。

如前所述,在C++中,与Java相反,与内置类型相比,用户类型不被视为该语言的二等公民。因此,如果内置类型具有运算符,则用户类型也应该能够拥有它们。

事实是,像toString()clone()equals()方法是用于Java(即准标准),C++运算符重载是C++的重要组成部分,它变得像原始C运算符或前面提到的Java方法一样自然。

结合模板编程,操作符重载成为众所周知的设计模式。实际上,如果不使用重载运算符,并且为自己的类重载运算符,则无法在STL中走得很远。

......但它不应该被滥用

运算符重载应该努力尊重运算符的语义。不要在+运算符中减去(如“不在add函数中减去”或“在clone方法中返回废话”)。

转换重载可能非常危险,因为它们可能导致含糊不清。因此,它们应该被保留用于明确定义的案例。对于&&||,除非你真的知道自己在做什么,否则不要重载它们,因为你将失去本机操作符&&||所享有的短路评估。

那么......好吧......那么为什么Java不可能呢?

因为詹姆斯·高斯林这么说:

我遗漏了运营商超载的情况 相当个人的选择 因为我看到有太多人在C++中滥用它。

詹姆斯戈斯林。资料来源: http://www.gotw.ca/publications/c_family_interview.htm

请将Gosling上面的文字与下面的Stroustrup进行比较:

许多C++设计决策源于我不喜欢强迫人们以某种​​特定方式做事[...]通常,我很想禁止我个人不喜欢的功能,我没有这样做因为 我认为我没有权利强迫我对别人的看法

Bjarne Stroustrup。资料来源:C++的设计和演变(1.3一般背景)

运算符重载会使Java受益吗?

一些对象将极大地受益于运算符重载(具体或数字类型,如BigDecimal,复数,矩阵,容器,迭代器,比较器,解析器等)。

在C++中,由于Stroustrup的谦逊,你可以从这个好处中获益。在Java中,你只是因为Gosling而被搞砸了 个人选择

它可以添加到Java吗?

现在不在Java中添加运算符重载的原因可能是内部政治,对功能的过敏,开发人员的不信任(你知道,似乎困扰Java团队的破坏者......),与以前的JVM的兼容性,是时候写一个正确的规格等了。

所以不要屏住呼吸等待这个功能......

但是他们在C#中做到了!

是啊...

虽然这远不是​​两种语言之间的唯一区别,但这一点永远不会让我高兴。

显然,C#伙伴,他们的 “每个原语都是struct,而struct派生自Object”,一开始就做对了。

他们用 其他语言 !!!

尽管所有FUD都反对使用已定义的运算符重载,但以下语言支持它: ScalaDartPythonF#C#_ d _ALGOL 68SmalltalkGroovyPerl 6 ,C++, RubyHaskell_ matlab _埃菲尔LuaClojureFortran 90SwiftAdaDelphi 2005 ...

如此多的语言,有许多不同的(有时是对立的)哲学,但他们都同意这一点。

值得思考的东西......

746
paercebal

James Gosling将Java设计为以下内容:

“当你从一个公寓搬到另一个公寓时,有一个关于移动的原则。一个有趣的实验是打包你的公寓并把所有东西放在盒子里,然后搬进下一个公寓,除非你需要,否则不要打开任何东西。所以你'重新开始你的第一顿饭,然后你从盒子里拿出一些东西。然后在一个月左右之后你就用它来弄清楚你生活中你真正需要的东西,然后你把剩下的东西 - 忘记你喜欢多少或多酷 - 你只是扔掉它。令人惊讶的是它如何简化你的生活,你可以在各种设计问题中使用这个原则:不要仅仅因为他们做事情“很酷或只是因为他们很有趣。”

你可以在这里阅读报价的 上下文

基本上,运算符重载对于模拟某种点,货币或复数的类非常有用。但在那之后你开始快速耗尽示例。

另一个因素是开发人员重载运算符滥用C++中的功能,例如'&&','||',强制转换操作符,当然还有'new'。将它与值和异常传递相结合所带来的复杂性在 Exceptional C++ book中有详细介绍。

41
Garth Gilmour

看看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”,这是正确的。

22
user15793

Java设计者认为运算符重载比它的价值更麻烦。就那么简单。

在一种语言中,每个对象变量实际上都是一个引用,运算符重载会带来额外的不合逻辑的危险 - 至少对C++程序员来说。将情况与C#的==运算符重载和Object.EqualsObject.ReferenceEquals(或其他任何调用)进行比较。

11
Sebastian Redl

Groovy 有运算符重载,并在JVM中运行。如果你不介意性能损失(每天变小)。它是基于方法名称自动完成的。例如,'+'调用'plus(argument)'方法。

8
noah

我认为这可能是一种有意识的设计选择,迫使开发人员创建名称清楚地传达其意图的功能。在C++中,开发人员会使运算符过载,这些功能通常与给定运算符的普遍接受性质无关,这使得在不查看运算符定义的情况下几乎无法确定代码片段的作用。

6
user14128

那么你可以通过操作员超载来拍摄自己的脚。就像指针一样,人们会对他们犯下愚蠢的错误,因此决定将剪刀拿走。

至少我认为这就是原因。无论如何我都在你身边。 :)

5
Sarien

有人说Java中的运算符重载会导致混淆。让那些人停下来看一些Java代码做一些基本的数学运算,比如用BigDecimal增加一个百分比的财务价值? ......这种运动的冗长使其成为混淆的证明。具有讽刺意味的是,将操作符重载添加到Java将允许我们创建自己的Currency类,这将使这样的数学代码优雅和简单(不太混淆)。

4
Volksman

从技术上讲,每种编程语言都有运算符重载,可以处理不同类型的数字,例如:整数和实数。说明:术语重载意味着一个函数只有几个实现。在大多数编程语言中,为运算符+提供了不同的实现,一个用于整数,一个用于实数,这称为运算符重载。

现在,很多人发现Java为运算符重载操作+将字符串加在一起很奇怪,从数学的角度来看,这确实很奇怪,但从编程语言开发人员的角度来看,添加内置运算符重载没有任何问题。对于运营商+其他类别,例如串。但是,大多数人都同意,一旦为+ for String添加内置重载,那么为开发人员提供此功能通常也是一个好主意。

完全不同意运算符重载模糊代码的谬误,因为这是由开发人员决定的。这是天真的思考,而且说实话,它已经变老了。

+1用于在Java 8中添加运算符重载。

4
Olai

假设运算符重载导致操作符与操作逻辑不匹配的类型的逻辑错误,就像什么也没说。如果函数名称不适合操作逻辑,则会出现相同类型的错误 - 所以解决方案是什么:放弃函数使用的能力!?这是一个滑稽的答案 - “不适合操作逻辑”,每个参数名称,每个类,函数或其他任何东西都是逻辑上不合适的。我认为这个选项应该以可敬的编程语言提供,那些认为它不安全的人 - 嘿,没有他们说你必须使用它。让我们来看看C#。他们扯了指针,但是嘿 - 有“不安全的代码”声明 - 你自己喜欢的程序风险。

4
Kvant

假设Java是实现语言,那么a,b和c都将引用类型为Complex的初始值为null。还假设Complex是不可变的,如提到的 BigInteger 和类似的不可变 BigDecimal ,我认为你的意思是以下内容,因为你正在为从添加中返回的Complex指定引用b和c,而不是将此引用与a进行比较。

不是:

Complex a, b, c; a = b + c;

比以前简单:

Complex a, b, c; a = b.add(c);
2
David Schlosnagle

有时候运算符重载,朋友类和多重继承会很好。

但我仍然认为这是一个很好的决定。如果Java有运算符重载,那么在不查看源代码的情况下,我们永远无法确定运算符的含义。目前没有必要。我认为使用方法而不是运算符重载的示例也非常易读。如果你想让事情更清楚,你总是可以在毛茸茸的陈述之上添加评论。

// a = b + c
Complex a, b, c; a = b.add(c);
1
user14070

本机支持Java运算符重载的替代方法

由于Java没有运算符重载,因此以下是您可以查看的一些替代方法:

  1. 使用其他语言。 GroovyScala 都有运算符重载,并且基于Java。
  2. 使用 Java-oo ,一个允许在Java中运算符重载的插件。请注意,它不是平台独立的。此外,它有许多问题,并且与最新版本的Java(即Java 10)不兼容。 ( 原始StackOverflow源
  3. 使用 _ jni _ ,Java Native Interface或其他选项。这允许您编写用于Java的C或C++(可能是其他?)方法。当然,这也不是平台独立的。

如果有人知道其他人,请发表评论,我会将其添加到此列表中。

0
gagarwa

这不是禁止它的理由,而是一个实际的理由:

人们并不总是负责任地使用它。从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 |>>

这是解释:

/运算符已被用作两层之间的合成运算符。这样做时,较低层可以根据上层重载其一个或多个默认字段。 (你仍然可以给出你想要的价值)。字符串可以用作原始图层。

0
Sarien