为什么==运算符字符串值比较不能与Java兼容?


50

每个熟练的Java程序员都知道您需要使用String.equals()来比较字符串,而不是==,因为==会检查引用是否相等。

在处理字符串时,大多数时候我都是在检查值相等性,而不是引用相等性。在我看来,如果该语言允许仅使用==比较字符串值,则将更加直观。

作为比较,C#的==运算符检查字符串 s的值相等。并且,如果您确实需要检查引用是否相等,则可以使用String.ReferenceEquals。

另一个重要的一点是,字符串是不可变的,因此允许使用此功能不会造成任何损害。

有什么特殊原因不能在Java中实现?


12
您可能想看看Scala,其中==对象相等和eq引用相等(ofps.oreilly.com/titles/9780596155957/…)。
Giorgio

请注意,这可能对您没有帮助,但据我所知,您可以将字符串文字与'==' 进行比较
Kgrover 2013年

10
@Kgrover:可以,但这只是引用相等性的便捷副产品,以及Java如何积极地将字符串匹配的文字优化为对同一对象的引用。换句话说,它是有效的,但是出于错误的原因。
tdammers

1
@aviv的==运营商只映射到Equals,如果==运营商开始实施的方式。的默认行为==ReferenceEquals(实际上ReferenceEquals是定义为的对象版本==)相同
Patrick Huizinga 2013年

3
这是设计决策的结果,在许多其他场景中,这些决策也很有意义。但是,既然您似乎知道这一点,并且无论如何都会问这个问题,那么我感到不得不解决您的问题:为什么必须要有一个String引用比较用例?
Jacob Raihle

Answers:


90

我想这只是一致性,或“最少惊讶的原则”。字符串是一个对象,因此如果将其与其他对象区别对待,将令人惊讶。

在Java出现之时(〜1995年),String对于大多数习惯于将字符串表示为以null终止的数组的程序员而言,拥有这样的东西是完全奢侈的。String现在的行为就是当时的行为,这很好。稍后巧妙地更改行为可能会对工作程序产生令人惊讶的不良影响。

附带说明一下,您可以使用String.intern()来获取字符串的规范化表示(内部表示),然后可以使用进行比较==。实习需要一些时间,但是在那之后,比较会非常快。

另外:与某些答案不同,它与支持运算符重载无关。该+运营商(串联)运行了String,即使Java不支持操作符重载秒; 它只是在编译器中作为特殊情况处理,解析为StringBuilder.append()。同样,本==可以作为特殊情况处理。

那么,为什么对特殊情况+却感到惊讶==呢?因为,当应用于非对象时,它+根本不会编译String因此很快就会发现。不同行为==将是更明显,从而更加惊人当它击中你。


8
特殊情况会增加惊讶。
Blrfl

17
弦乐在1995年是奢侈品吗?真??查看计算机语言的历史。当时具有某种字符串类型的语言的数量将远远多于没有字符串的语言。除了C及其后代,还有多少种语言使用了以null结尾的数组?
WarrenT

14
@WarrenT:当然,某些(即使不是大多数)语言具有某种类型的字符串,但我认为1995年具有Unicode功能的垃圾收集字符串是一种新颖。例如,Python在2000年发布了2.0版本的Unicode字符串。选择不变性在当时也是一个有争议的选择。
Joonas Pulakka

3
@JoonasPulakka然后,也许您应该编辑答案以这样说。因为就目前而言,答案中的“完全奢侈”部分是完全错误的。
svick

1
实习有一个代价:您将获得一个永远不会被释放的字符串。(好吧,除非您使用自己可以抛弃的实习引擎。)
Donal Fellows

32

Java的创建者James Gosling 在2000年7月就以这种方式进行解释:

我没有将运算符重载作为个人选择,因为我看到太多人在C ++中滥用它。在过去的五到六年中,我花了很多时间来调查有关操作员超载的人员,这确实很令人着迷,因为您将社区分为三部分:大约有20%到30%的人口认为操作员超载是恶魔的产生 有人用运算符重载做了一些事情,这确实使他们不满意,因为他们使用+来插入列表,这使生活真的非常混乱。许多问题源于一个事实,即您可以合理地重载大约六个操作员,而人们却想定义成千上万个操作员-因此您必须选择,


50
嗯,是的,旧的“让钝器变尖了,这样燕麦就不会伤害自己了”的借口。
Blrfl 2013年

22
@Blrfl:如果工具所产生的问题超出了解决的范围,则它不是一个好工具。当然,确定操作符重载是否是这种情况可能会使讨论变得很漫长。
Giorgio

15
-1。这根本无法回答问题。Java 确实有运算符重载。该==操作符被重载对象和原语。该+运营商目前超载byteshortintlongfloatdoubleString,可能其他几个我忘了。这本来是完全有可能超载==String为好。
约尔格W¯¯米塔格

10
@Jorg-不,不是。在用户级别无法定义操作员重载。确实在编译器中有一些特殊情况,但几乎没有资格
AZ01 2013年

9
@Blrfl:我不介意燕麦会伤害自己。当他们不小心戳了我一眼时,我很生气。
乔纳斯(Jonas)2013年

9

语言内的一致性。让操作员采取不同的行动可能令程序员感到惊讶。Java不允许用户重载运算符-因此,引用相等是==对象之间唯一合理的含义。

在Java中:

  • 在数字类型之间,==比较数字相等性
  • 在布尔类型之间,==比较布尔相等
  • 在对象之间,==比较引用标识
    • 使用.equals(Object o)比较值

而已。简单的规则和简单的标识您想要的东西。JLS的15.21节涵盖了所有内容。它包括三个小节,这些小节易于理解,实现和解释。

一旦允许重载==,确切的行为就不再是您可以依靠JLS并把手指放在某个特定项目上并说“这就是它的工作原理”的原因,因此代码就很难推理了。用户的确切行为==可能令人惊讶。每次看到它时,您都必须返回并检查它的实际含义。

由于Java不允许操作符重载,因此需要一种方法来进行值相等性测试,您可以覆盖其基本定义。因此,这些设计选择要求它。 ==在Java中,对于数字类型,对数字进行测试;对于布尔类型,对布尔等式进行测试;对于其他所有类型,则对引用进行相等性测试(.equals(Object o)对于值相等,它们可以重写以执行其所需的任何操作)。

这不是“该设计决策的特定结果是否有用例”的问题,而是“这是为了促进这些其他事情的设计决策,这是它的结果”。

字符串实习,就是这样的一个例子。根据JLS 3.10.5,所有字符串文字均被保留。如果有人调用其他字符串,则会将.intern()其保留。这"foo" == "foo"是设计决策的结果,该决策旨在最大程度地减少String文字占用的内存。除此之外,String实习是在JVM级别上对用户有所了解的东西,但在绝大多数情况下,不应与程序员有关(并且程序员的用例不是在考虑此功能时,对于设计师而言,这是最重要的事情)。

人们会指出这一点,++=为String重载。但是,这既不是这里也不是那里。仍然存在这样的情况:如果==对String(仅String)具有值相等的含义,则需要一种方法(仅存在于String中)来实现引用相等。此外,这将不必要地使采用Object并期望==以一种方式.equals()表现并以另一种方式表现的方法复杂化,这要求用户对String的所有这些方法进行特殊处理。

==on Object 的一致约定是,它仅是引用相等性,并且.equals(Object o)对于应该测试值相等性的所有对象均存在。使这一点复杂化会使太多事情复杂化。


感谢您抽出宝贵的时间回答。对于我链接的其他问题,这将是一个很好的答案。不幸的是,这不适合这个问题。我将根据这些评论对OP进行更新。我正在寻找语言用户在比较字符串时希望使用假负数的用例。该语言提供此功能是为了保持一致性,现在我希望我们更进一步。也许是从新语言设计师那里想到的,是否需要?(遗憾的是,没有lang-design.SE)
Anonsage 2015年

3
@ Anonsage不是一个假阴性。它们不是同一对象。就是这么说。我还必须指出,在Java 8中new String("foo") == new String("foo")可能是正确的(请参阅String Deduplication)。

1
关于语言设计,CS.SE宣传它可能是那里的话题。

哦,谢谢!我将在这里发布我将来的语言设计问题。:)而且,很遗憾,“假阴性”不是描述我的问题和我正在寻找的最准确的方法。我需要写更多的单词,这样人们就不必猜测我在说什么。试图说。
Anonsage 2015年

2
“语言内的一致性”也有助于泛型
Brendan

2

Java不支持运算符重载,这意味着==仅适用于原始类型或引用。其他任何事情都需要调用方法。设计师为什么要这样做,这只是他们可以回答的问题。如果我不得不猜测,那可能是因为运算符重载带来了复杂性,他们对添加没有兴趣。

我不是C#的专家,但是该语言的设计师似乎已经将其设置为每个原始都是a struct,每个struct都是对象。因为C#允许操作符重载,所以这种安排使任何类(不仅仅是)都很容易String以任何操作符的“预期”方式进行工作。C ++允许同样的事情。


1
“ Java不支持运算符重载,这意味着==仅适用于原始类型或引用。其他所有内容都需要调用方法。”:可以补充一点,如果==意味着字符串相等,我们将需要另一种标记来表示引用相等。
Giorgio

@Giorgio:是的。请参阅我对Gilad Naaman答案的评论。
Blrfl 2013年

尽管可以通过比较两个对象(或运算符)的引用的静态方法来解决。例如在C#中。
Gilad Naaman

@GiladNaaman:那将是一个零和游戏,因为它引起了与Java现在相反的问题:运算符将相等,并且您必须调用一种方法来比较引用。此外,您必须强加所有类都必须实现可绑定到的要求==。这有效地增加了运算符重载,这将对Java的实现方式产生巨大的影响。
Blrfl 2013年

1
@Blrfl:不是。始终会有一种定义的方法来比较reference(ClassName.ReferenceEquals(a,b)),并且默认==操作符和Equals方法都指向ReferenceEquals
Gilad Naaman

2

在其他语言中这已变得不同。

在对象Pascal(Delphi / Free Pascal)和C#中,定义相等运算符以在对字符串进行操作时比较值,而不是引用。

特别是在Pascal中,字符串是一种原始类型(我真的很喜欢Pascal,其中之一就是因为未初始化的字符串而导致NullreferenceException只是令人讨厌),并且具有写时复制的语义,因此(大部分时间)使字符串操作非常方便便宜(换句话说,只有在开始连接数兆字节的字符串后才值得注意)。

因此,这是Java的语言设计决策。当他们设计语言时,他们遵循C ++的方式(例如Std :: String),因此字符串是对象,恕我直言,这是对缺乏真正字符串类型的C的一种补偿,而不是使字符串成为原始类型(它们是)。

因此,出于某种原因,我只能推测它们很容易实现,而没有对操作符进行编码,则使编译器对字符串产生异常。


这如何回答所提问题?
gnat 2013年

参见最后一句话(我在编辑中的适当段落中将其分开)。
Fabricio Araujo 2013年

1
恕我直言,String应该是Java中的原始类型。与其他类型不同,编译器需要了解String; 此外,对其进行的操作将非常普遍,以至于对于许多类型的应用程序,它们可能会导致性能瓶颈(可以通过本机支持来缓解)。典型的string[小写字母]将在堆上分配一个对象以容纳其内容,但是在任何地方都不存在对该对象的“正常”引用。因此它可能是一个单进行间接寻址Char[]Byte[]而不必是一个Char[]通过另一个对象进行间接寻址。
2014年

1

在Java中,没有任何运算符重载,这就是为什么比较运算符仅对原始类型重载的原因。

'String'类不是原始类型,因此它没有对'=='的重载,并使用默认值比较计算机内存中对象的地址。

我不确定,但是我认为在Java 7或8中,oracle在编译器中做了一个例外,以识别str1 == str2str1.equals(str2)


“我不确定,但是我认为在Java 7或8中,oracle在编译器中例外,将str1 == str2识别为str1.equals(str2)”:我不会感到惊讶:Oracle似乎不太担心与Sun相比具有极简主义。
Giorgio

2
如果为真,那将是一个非常丑陋的破解,因为这意味着该语言现在与其他所有类都区别对待,并且破坏了用于比较引用的代码。:-@
Blrfl

1
@WillihamTotland:考虑相反的情况。目前,如果我创建两个字符串,s1s2给他们相同的内容,他们通过平等的(s1.equals(s2))的比较,但不一样的引用(==)比较,因为它们是两个不同的对象。将的语义更改为==均等将导致s1 == s2在以前评估的true地方进行评估false
Blrfl 2013年

2
@Brlfl:的确如此,但这首先听起来是一件非常糟糕的事情,因为字符串是不可变的,可替代的对象。
Williham Totland

2
@Giorgio:“ Java 7或8 oracle在编译器中发生异常,将str1 == str2识别为str1.equals(str2)”,不,Java语言规范说:Reference Equal Operators“如果相等运算符的操作数是无论是引用类型还是null类型,那么操作就是对象相等。” 那是所有人。在Java 8早期草案中,我也没有发现任何新的东西。
David Tonhofer 2014年

0

Java似乎已被设计为遵守一个基本规则,即==只要一个操作数可以转换为另一个操作数的类型,运算符就应该合法,并且应该将这种转换结果与未转换的操作数进行比较。

该规则并非Java独有,但它对语言的其他与类型相关的方面的设计产生了深远的影响(恕我直言,不幸的是)。==相对于操作数类型的特定组合来指定的行为会更加干净,x1==y1并且x2==y1在不暗示的情况下禁止X和Y类型的组合x1==x2,但是语言很少这样做[在这种哲学下,double1 == long1要么必须指出是否double1不是的精确表示long1,否则拒绝编译;int1==Integer1应该被禁止,但是应该有一种方便有效的非抛出方式来测试一个对象是否是具有特定值的装箱整数(与非装箱整数进行比较应该只返回false)]。

关于将==运算符应用于字符串,如果Java禁止直接比较类型为String和的操作数Object,则可以很好地避免行为的意外==,但是对于这种比较,它没有实现任何令人惊讶的行为。具有两个保留类型的字符串引用的Object行为与保留类型的引用String不同,这比具有合法混合类型比较的任何行为都令人惊讶的少。如果String1==Object1是合法的,则意味着String1==String2Object1==Object2匹配行为的唯一方法String1==Object1是使它们彼此匹配。


我必须缺少一些东西,但是==对象上的恕我直言应该简单地调用(空安全)等于,而其他一些东西(例如===System.identityEqual)应该用于身份比较。最初将禁止混合图元和对象(在1.5之前没有自动装箱),然后可以找到一些简单的规则(例如,空安全取消装箱,然后进行强制转换,然后进行比较)。
maaartinus

@maaartinus:一种好的语言设计应使用单独的相等运算符来实现值和引用相等。虽然我同意从概念上讲,如果a 为null 可能有一个int==Integer运算符返回,否则可以比较值,但这种方法与所有其他情况下的行为都不同,在这种情况下,它无条件地将两个操作数强制转换为同一类型比较它们。我个人想知道是否设置了自动拆箱功能,以使这种行为不是荒唐的……falseInteger==int==Integer
supercat 2014年

...由于自动装箱int和进行参考比较会很愚蠢[但不会总是失败]。否则,我认为没有理由允许使用NPE失败的隐式转换。
2014年

我认为我的想法是一致的。请记住,在更美好的世界中,==这无关identityEquals。+++“用于值和引用相等的独立相等运算符”-但是哪一个呢?我会考虑这两个原始==equals因为这样做在一定意义比较equals着眼于价值的参考。+++的==意思是equals,然后int==Integer应该进行自动装箱并使用null安全等于来比较引用。+++恐怕,我的想法不是我的想法,而是Kotlin的想法。
maaartinus

@maaartinus:如果==从未测试过引用相等性,那么它可以明智地执行一个空安全的值相等性测试。但是,它确实测试了引用是否相等,这一事实严重限制了它在处理引用/值混合比较时不会出现不一致的情况。还要注意,Java固定在这样一个概念上,即运算符将两个操作数提升为同一类型,而不是根据所涉及类型的组合产生特殊的行为。例如,16777217==16777216.0f返回true是因为它执行第一个操作数到的有损转换float,而a ...
超级猫

0

通常,有充分的理由要能够测试两个对象引用是否指向同一对象。我写了很多次

Address oldAddress;
Address newAddress;
... populate values ...
if (oldAddress==newAddress)
... etc ...

在这种情况下,我可能具有等于函数,也可能没有。如果我这样做,则equals函数可能会比较两个对象的全部内容。通常,它只是比较一些标识符。当然,“ A和B是对相同对象的引用”和“ A和B是具有相同内容的两个不同对象”是两个截然不同的想法。

对于不可变的对象(例如字符串),这确实是一个小问题。对于不可变的对象,我们倾向于认为对象和值是同一件事。好吧,当我说“我们”时,至少是“我”。

Integer three=new Integer(3);
Integer triangle=new Integer(3);
if (three==triangle) ...

当然返回的是false,但是我可以看到有人认为它应该是真实的。

但是一旦您说==通常比较对象的引用句柄而不是内容,对Strings进行特殊处理可能会造成混淆。正如这里的其他人所说,如果要比较两个String对象的句柄怎么办?会有一些特殊的功能仅对字符串吗?

那...

Object x=new String("foo");
Object y=new String("foo");
if (x==y) ...

是因为它们是两个不同的对象,所以是错误的;还是因为它们是内容相等的String,所以是true?

所以,是的,我了解程序员如何对此感到困惑。我自己做了,我的意思是当myString ==“ foo”时写myString.equals(“ foo”)。但是除了重新设计所有对象的==运算符的含义之外,我看不到如何解决它。


请注意,JVM上的其他现代语言(例如Scala)用于==表示“相等字符串”。
Andres F.

@AndresF。(耸耸肩)在Java中,“ <”表示“小于”,而在XML中则“打开标签”。在VB中,“ =”可以表示“等于”,而在Java中,它仅用于分配。等等,不同的语言使用不同的符号表示相同的事物,或者使用相同的符号表示不同的事物也就不足为奇了。
杰伊

这不是您的答案。这只是一条评论。我只是指出,就像Java ==末尾提到的那样,比Java更现代的语言抓住了机会来重新设计的含义。
Andres F.

@AndresF。我的回答并不是您的评论,只是说不同的语言以不同的方式处理这些问题。:-)我实际上很喜欢VB的处理方式...暂停VB仇恨者的嘶嘶声和嘘声...“ =”总是比较值(对于系统定义的类型),无论是原始值还是对象值。“是”比较两个对象的句柄。对我来说,这似乎更直观。
杰伊

当然。但是Scala比Visual Basic更接近Java。我喜欢认为Scala的设计师意识到Java的使用==容易出错。
Andres F.

0

这是一个有效的问题Strings,而不是只为字符串,也为代表的一些“价值”等不可变对象,例如DoubleBigInteger甚至InetAddress

为了使==运算符可用于字符串和其他值类,我看到了三种选择:

  • 让编译器知道所有这些值类以及比较它们的内容的方法。如果只是java.lang软件包中的少数几个类,我会考虑的,但这并不涵盖InetAddress之类的情况。

  • 允许运算符重载,以便类定义其==比较行为。

  • 删除公共构造函数,并使用静态方法从池中返回实例,并始终以相同的值返回相同的实例。为避免内存泄漏,您需要在池中使用诸如SoftReferences之类的东西,而Java 1.0中则不存在。现在,为了保持兼容性,String()无法再删除构造函数。

今天唯一仍然可以做的事情就是引入运算符重载,而且我个人不希望Java走那条路线。

对我来说,代码的可读性是最重要的,并且Java程序员知道运算符具有固定的含义(在语言规范中定义),而方法是由某些代码定义的,并且它们的含义必须在方法的Javadoc中查找。我想保留这种区别,即使这意味着String比较将无法使用==运算符。

Java比较的一个方面令我很烦:自动装箱和-unboxing的效果。它隐藏了基本类型和包装类型之间的区别。但是,当您将它们与进行比较时==,它们是完全不同的。

    int i=123456;
    Integer j=123456;
    Integer k=123456;
    System.out.println(i==j);  // true or false? Do you know without reading the specs?
    System.out.println(j==k);  // true or false? Do you know without reading the specs?
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.