一个更基本的原因Java不包括运算符重载(至少对于赋值)?

时间:2011-05-16 00:43:06

标签: java c++ operator-overloading

关于Java中没有运算符重载(Why doesn't Java offer operator overloading?),并且从很多C ++年代来到Java,我想知道是否存在更基础的问题,这是一个长达2年的讨论。运算符重载不是Java语言的一部分的原因,至少在赋值的情况下,而不是答案底部附近的最高级别答案(即James Gosling的个人选择)。

具体来说,考虑分配。

// C++
#include <iostream>

class MyClass
{
public:
    int x;
    MyClass(const int _x) : x(_x) {}
    MyClass & operator=(const MyClass & rhs) {x=rhs.x; return *this;}
};

int main()
{
    MyClass myObj1(1), myObj2(2);
    MyClass & myRef = myObj1;
    myRef = myObj2;

    std::cout << "myObj1.x = " << myObj1.x << std::endl;
    std::cout << "myObj2.x = " << myObj2.x << std::endl;

    return 0;
}

输出结果为:

myObj1.x = 2
myObj2.x = 2

然而,在Java中,行myRef = myObj2(假设前一行中myRef的声明是myClass myRef = myObj1,正如Java所要求的那样,因为所有这些变量都是自动的Java风格的'引用')行为非常不同 - 它不会导致myObj1.x更改,输出将

myObj1.x = 1
myObj2.x = 2

C ++和Java之间的这种差异使我认为Java中缺少运算符重载,至少在赋值的情况下,不是James Gosling的“个人选择问题”,而是基础必要的Java语法将所有对象变量视为引用(即MyClass myRef = myObj1myRef定义为Java风格的引用)。我这样说是因为如果Java中的赋值导致左侧引用引用不同的对象,而不是允许对象本身改变其值的可能性,那么似乎不可能提供重载的赋值运算符

换句话说 - 它不仅仅是一种“选择”,甚至没有“屏住呼吸”的可能性,希望它会被引入,因为前面提到的高评价答案也说明了(接近结束) )。引用:“现在不添加它们的原因可能是内部政治,对功能的过敏,对开发人员的不信任(你知道,破坏者),与以前的JVM的兼容性,编写正确规范的时间的混合等等。所以不要屏住呼吸等待这个功能。“。 &lt; - 所以这是不正确的,至少对于赋值运算符:没有运算符重载(至少对于赋值)的原因是Java本质的基础。

这是我的正确评估吗?

附录

假设赋值运算符是一个特例,那么我的后续问题是:是否有任何其他运算符,或者更一般的任何其他语言特性,必然会以与赋值运算符类似的方式受到影响?我想知道Java和C ++之间关于变量作为值/引用的差异有多深。也就是说,在C ++中,变量标记表示值(并注意,即使变量标记最初被声明为引用,它仍然被视为基本上在任何地方使用的值),而在Java中,变量标记表示诚实到良好的引用以后使用令牌的地方。

4 个答案:

答案 0 :(得分:5)

在谈论Java和C ++之间的相似点和不同点时,存在一个很大的误解,这在你的问题中出现了。 C ++引用和Java引用不一样。在Java中,引用是真实对象的可重置代理,而在C ++中,引用是对象的别名。用C ++术语来说,Java引用是垃圾收集指针而不是引用。现在,回到您的示例,使用C ++和Java编写等效代码,您将不得不使用指针:

int main() {
   type a(1), b(2);
   type *pa = &a, *pb = &b;
   pa = pb;
   // a is still 1, b is still 2, pa == pb == &b
}

现在示例是相同的:赋值运算符应用于对象的指针,在这种特殊情况下,您也不能在C ++中重载运算符。重要的是要注意操作符重载很容易被滥用,这是首先避免它的一个很好的理由。现在,如果你添加两种不同类型的实体:对象和引用,那么事情会变得更加混乱。

如果允许您在Java中为特定对象重载operator=,那么您将无法对同一对象进行多次引用,并且该语言将被削弱:

Type a = new Type(1);
Type b = new Type(2);
a = b;                 // dispatched to Type.operator=( Type )??
a.foo();
a = new Type(3);       // do you want to copy Type(3) into a, or work with a new object?

反过来会使该类型在语言中无法使用:容器存储引用,并且它们重新分配它们(即使是第一次创建对象时),函数也没有真正使用传递引用语义,而是按值传递引用(这是一个完全不同的问题,同样,差异是void foo( type* )void foo( type& ):代理实体复制后,您无法修改调用者传入的引用

问题是该语言正在努力隐藏aa 引用的对象不同的事实(同样的事情发生在C#),这反过来意味着您无法明确声明将一个操作应用于引用/引用,这是由语言解决的。该设计的结果是任何可以应用于引用的操作都不能应用于对象本身。

从其他运算符开始,决定很可能是任意,因为语言隐藏引用/对象差异,它可以设计为{ {1}}被编译器翻译为a+b。因为你不能使用算术,所以没有问题,因为编译器会认识到type* operator+( type*, type* )是一个必须应用于对象的操作(它对引用没有意义)。但是,你可以认为有点尴尬,你可以重载a+b,但你不能重载+=== ......

这是C#采用的路径,其中赋值不能为引用类型重载。有趣的是,在C#中有值类型,可以为引用和值类型重载的运算符集是不同的。没有在大型项目中编写C#,我无法确定这种混淆的潜在来源是否是这样或者人们是否已经习惯了(但是如果你搜索SO,你会发现有些人会问为什么X不能在C#中为参考类型重载其中 X 是可以应用于引用本身的操作之一。

答案 1 :(得分:3)

这并不能解释为什么他们不允许重载+-等其他运营商。考虑到James Gosling设计了Java语言,他说这是他个人的选择,他在你所链接的问题中提供的link中更详细地解释了,我认为这是你的答案:

  

有些事情让我觉得有些不知所措,比如操作员超载。我遗漏了操作符重载作为一个相当个人的选择,因为我看到有太多人在C ++中滥用它。在过去的五到六年里,我花了很多时间来调查人们关于操作员超载的问题,这真的很吸引人,因为你把社区分成了三个部分:可能大约20%到30%的人认为操作员超载是魔鬼的产卵;有人已经做过一些操作符重载的事情,它们已经真正勾选了它们,因为它们使用了类似的+用于列表插入,这让生活真的非常混乱。很多问题源于这样一个事实,即只有大约六个运营商可以理智地超载,但是人们想要定义的数千或数百万运营商 - 所以你必须选择,而且往往是选择与你的直觉感相冲突。然后有一个大约10%的社区实际上已经适当地使用了运算符重载,并且真正关心它,对谁来说它实际上非常重要;这几乎完全是那些从事数字工作的人,其中符号对于吸引人们的直觉非常重要,因为他们直接了解了+的意思,以及说“a + b”的能力a和b是复杂的数字或矩阵,还是确实有意义的东西。当你遇到像乘法这样的东西时,你会变得有些不稳定,因为实际上有多种乘法运算符 - 有矢量乘积和点乘积,它们从根本上是非常不同的。然而,只有一个操作员,所以你做什么?并且没有平方根运算符。这两个阵营是两极,然后就是60%的中间人,他们真的不在乎这两个方面。认为操作员超载是一个坏主意的人的阵营,仅仅来自我的非正式统计抽样,比数字家伙显着更大,肯定更有声音。因此,考虑到今天事情已经过去的方式,语言中的某些功能被社区投票 - 它不仅仅是一些小标准委员会,它确实是大规模的 - 它很难让运营商超载然而,它让这个相当重要的人群完全被拒之门外。这是公地问题悲剧的一种风格。

更新:Re:您的附录,其他作业运营商+=-=等也会受到影响。您也无法编写swap函数,例如void swap(int *a, int *b);。和其他东西。

答案 2 :(得分:1)

  

这是我的正确评估吗?

缺乏操作员一般是“个人选择”。 C#是一种非常相似的语言,它允许运算符重载。但你还是can't overload assignment。甚至在参考语义语言中会做什么?

  

是否有其他运营商或更多运营商   通常任何其他语言功能,   必然会受到影响   与任务类似的方式   运营商?我想知道怎么做   “深层”之间存在差异   关于Java和C ++   变量 - 作为值/引用。

最明显的是复制。在引用语义语言中,clone()并不常见,并且对于String等不可变类型,根本不需要。但是在C ++中,默认赋值语义基于复制,复制构造函数非常常见。如果您没有定义,则会自动生成。

一个更微妙的区别是,引用语义语言比值语义语言更难以支持RAII,因为对象生命周期更难跟踪。 Raymond Chen has a good explanation.

答案 3 :(得分:-1)

在C ++语言中滥用运算符重载的原因是因为它太复杂了。以下是它的一些方面,使其变得复杂:

  1. 表达式是树
  2. 运算符重载是这些表达式的接口/文档
  3. 接口在c ++中基本上是不可见的功能
  4. 自由函数/静态函数/友元函数在C ++中是一个很大的混乱
  5. 功能原型已经很复杂了
  6. 选择运算符重载的语法不太理想
  7. c ++语言中没有其他类似的api
  8. 用户定义的类型/函数名称的处理方式与函数原型中的内置类型/函数名称不同
  9. 它使用高级数学,如运算符&lt;&lt;(ostream&amp;,ostream&amp;(* fptr)(ostream&amp;));
  10. 即使最简单的例子也使用了多态
  11. 这是唯一一个包含2d数组的c ++功能
  12. 这个指针是不可见的,你的运算符是成员函数还是类外是程序员的重要选择
  13. 由于这些复杂性,极少数程序员实际上了解它是如何工作的。我可能错过了它的许多重要方面,但上面的列表很好地表明它是非常复杂的功能。

    更新:关于#4的一些解释:该论点几乎如下:

    class A { friend void f(); }; class B { friend void f(); }
    void f() { /* use both A and B members inside this function */ }
    

    使用静态功能,您可以执行以下操作:

    class A { static void f(); }; void f() { /* use only class A here */ }
    

    使用免费功能:

    class A { }; void f() { /* you have no special access to any classes */ }
    

    更新#2:#10,我想的例子在stdlib中看起来像这样:

      ostream &operator<<(ostream &o, std::string s) { ... } // inside stdlib
      int main() { std::cout << "Hello World" << std::endl; }
    

    现在这个例子中的多态性发生了,因为你可以在std :: cout和std :: ofstream以及std :: stringstream之间进行选择。这是可能的,因为运算符&lt;&lt;第一个参数引用ostream。这是此示例中的正常运行时多态性。

    更新#3:关于原型仍然如此。运算符重载和原型之间的真正交互是因为重载的运算符成为类接口的一部分。这就把我们带到了2d数组的事情,因为在编译器中,类接口是一个二维数据结构,其中包含相当复杂的数据,包括布尔值,类型,函数名。需要使用规则#4,以便您可以选择运算符何时位于此2d数据结构中以及何时位于其外部。规则#8处理存储在2d数据结构中的布尔值。规则#7是因为类'接口用于表示表达式树的元素。