38

I've been pondering for a while why Java and C# (and I'm sure other languages) default to reference equality for ==.

In the programming I do (which certainly is only a small subset of programming problems), I almost always want logical equality when comparing objects instead of reference equality. I was trying to think of why both of these languages went this route instead of inverting it and having == be logical equality and using .ReferenceEquals() for reference equality.

Obviously using reference equality is very simple to implement and it gives very consistent behavior, but it doesn't seem like it fits well with most of the programming practices I see today.

I don't wish to seem ignorant of the issues with trying to implement a logical comparison, and that it has to be implemented in every class. I also realize that these languages were designed a long time ago, but the general question stands.

Is there some major benefit of defaulting to this that I am simply missing, or does it seem reasonable that the default behavior should be logical equality, and defaulting back to reference equality it a logical equality doesn't exist for the class?

5
  • 5
    Because variables are references? Since variables act like pointers it makes sense for them to be compared like it Jun 9, 2013 at 5:51
  • C# uses logical equality for value types like structs. But what should the "default logical equality" be for two objects of different reference types? Or for two objects where one is of a type A inherited from B? Always "false" like for structs? Even when you have the same object referenced twice, first as A, then as B? Does not make much sense to me.
    – Doc Brown
    Jun 9, 2013 at 6:25
  • 3
    In other words, are you asking why in C#, if you override Equals(), it doesn't automatically change the behavior of ==?
    – svick
    Jun 9, 2013 at 11:11
  • 2
    Note that in modern Java, records (defined using the record keyword instead of class) actually do default to deep equality. This works because records are immutable carriers of data, not stateful objects with behavior.
    – Jack
    Oct 4, 2021 at 3:08
  • 1
    @Jack - yours is the key observation - it has to do with immutable value types (primitives, like integers, floats, and, yes, strings, or records of immutable value types) vs. mutable (i.e., stateful) types which, in O-O languages, are supposed to have identity. The mistake in C# is that strings, immutable, are exposed as reference types (they're nullable) even though they are value types (with value equality!). (Because they were implemented that way.) (A 2nd mistake in C# is that structs are not immutable, even though they behave that way for equality - they can be copied and changed.)
    – davidbak
    Oct 4, 2021 at 15:18

9 Answers 9

37

C# does it because Java did. Java did because Java does not support operator overloading. Since value equality must be redefined for each class, it could not be an operator, but instead had to be a method. IMO this was a poor decision. It is much easier to both write and read a == b than a.equals(b), and much more natural for programmers with C or C++ experience, but a == b is almost always wrong. Bugs from the use of == where .equals was required have wasted countless thousands of programmer hours.

7
  • 9
    I think there are as many supporters of operator overloading as there are detractors, so I would not say "it was a poor decision" as an absolute statement. Example: in the C++ project I work in we have overloaded the == for many classes and a few months ago I discovered a few developers did not know what == was actually doing. There is always this risk when the semantics of some construct is not obvious. The equals() notation tells me that I am using a custom method and that I have to look it up somewhere. Bottom line: I think operator overloading is an open issue in general.
    – Giorgio
    Jun 9, 2013 at 8:36
  • 13
    I'd say Java has no user-defined operator overloading. Plenty of operators have double (overloaded) meanings in Java. Look at + for example, which does addition (of numeric values) and concatenation of strings at the same time. Jun 9, 2013 at 8:39
  • 14
    How can a == b be more natural for programmers with C experience, since C doesn't support user-defined operator overloading? (For example, the C way to compare strings is strcmp(a, b) == 0, not a == b.)
    – svick
    Jun 9, 2013 at 11:12
  • This is basically what I thought, but I figured I'd ask those with more experience to make sure I wasn't missing something obvious.
    – Zipper
    Jun 10, 2013 at 3:30
  • 4
    @svick: in C, there is no string type, nor any reference types. String operations are done via char *. It seems obvious to me that comparing two pointers for equality is not the same as a string comparison. Jun 10, 2013 at 20:01
24

The short answer: Consistency

To answer your question properly, though, I suggest we take a step backwards and look to the issue of what equality means in a programming language. There are at least THREE different possibilities, which are used in various languages:

  • Reference equality: means that a = b is true if a and b refer to the same object. It would not be true if a and b referred to different objects, even if all the attributes of a and b were the same.
  • Shallow equality: means that a = b is true if all the attributes of the objects to which a and b refer are identical. Shallow equality can easily be implemented by a bitwise comparison of the memory space that represents the two objects. Please note that reference equality implies shallow equality
  • Deep equality: means that a = b is true if each attribute in a and b is either identical or deeply equal. Please note that deep equality is implied by both reference equality and shallow equality. In this sense, deep equality is the weakest form of equality and reference equality is the strongest.

These three types of equality are often used because they are convenient to implement: all three equality checks can easily be generated by a compiler (in the case of deep equality, the compiler might need to use tag bits to prevent infinite loops if a structure to be compared has circular references). But there is another problem: none of these might be appropriate.

In non-trivial systems, equality of objects is often defined as something between deep and reference equality. To check whether we want to regard two objects as equal in a certain context, we might require some attributes to be compared by where it stands in memory and others by deep equality, while some attributes may be allowed to be something different altogether. What we would really like is a “forth type of equality”, a really nice one, often called in the literature semantic equality. Things are equal if they are equal, in our domain. =)

So we can come back to your question:

Is there some major benefit of defaulting to this that I am simply missing, or does it seem reasonable that the default behavior should be logical equality, and defaulting back to reference equality if a logical equality doesn't exist for the class?

What do we mean when we write ‘a == b’ in any language? Ideally, it should always be the same: Semantic equality. But that´s not possible.

One of the main considerations is that, at least for simple types like numbers, we expect that two variables are equal after assignment of the same value. See below:

var a = 1;
var b = a;
if (a == b){
    ...
}
a = 3;
b = 3;
if (a == b) {
    ...
}

In this case, we expect that ‘a equals b’ in both statements. Anything else would be insane. Most (if not all) of the languages follow this convention. Therefore, with simple types (aka values) we know how to achieve semantic equality. With objects, that can be something completely different. See below:

var a = new Something(1);
var b = a;
if (a == b){
    ...
}
b = new Something(1);
a.DoSomething();
b.DoSomething();
if (a == b) {
    ...
}

We expect that the first ‘if’ will always be true. But what do you expect on the second ‘if’? It really depends. Can ‘DoSomething’ change the (semantic) equality of a and b?

The problem with semantic equality is that it cannot be automatically generated by the compiler for objects, nor it´s obvious from the assignments. A mechanism must be provided for the user to define semantic equality. In object-oriented languages, that mechanism is an inherited method: equals. Reading a piece of OO code, we don´t expect a method to have the same exact implementation in all classes. We are used to inheritance and overloading.

With operators, though, we expect the same behavior. When you see ‘a == b’ you should expect the same type of equality (from the 4 above) in all situations. So, aiming for consistency the languages designers used reference equality for all types. It should not depend on whether a programmer has overridden a method or not.

PS: The language Dee is slightly different from Java and C#: the equals operator means shallow equality for simple types and semantic equality for user-defined classes (with the responsibility for implementing the = operation lying with the user — no default is provided). As, for simple types, shallow equality is always semantic equality, the language is consistent. The price it pays, though, is that the equals operator is by default undefined for user-defined types. You have to implement it. And, sometimes, that´s just boring.

5
  • 2
    When you see ‘a == b’ you should expect the same type of equality (from the 4 above) in all situations. The language designers of Java used reference equality for objects and semantic equality for primitives. It's not obvious to me that this was the right decision, or that this decision is more "consistent" than allowing == to be overloaded for semantic equality of objects. Jun 12, 2013 at 20:14
  • They used "the equivalent of reference equality" for primitives too. When you use "int i = 3" there are no pointers for the number, so you can´t use references. With strings, a "sort of" primitive type, it´s more evident: you have to use the ".intern()" or a direct assignment (String s = "abc") to use == (reference equality).
    – Hbas
    Jun 12, 2013 at 20:20
  • 2
    PS: C#, on the other hand, was not consistent with it´s strings. And IMHO, in this case, that´s much better.
    – Hbas
    Jun 12, 2013 at 20:28
  • @CharlesSalvia: In Java, if a and b are the same type, the expression a==b tests whether a and b hold the same thing. If one of them holds a reference to object #291, and the other holds a reference to object #572, they don't hold the same thing. The contents of object #291 and #572 may be equivalent, but the variables themselves hold different things.
    – supercat
    Jan 17, 2014 at 18:40
  • 2
    @CharlesSalvia It is designed in such a way that you can see a == b and know what it does. Likewise, you can see a.equals(b) and presume that a overloads equals. If a == b calls a.equals(b) (if implemented), is it comparing by reference or by content? Don't remember? You have to check class A. The code is no longer as quick to read if you're not even sure what is being called. It'd be as if methods with the same signature were allowed, and the method being called depends on what the current scope is. Such programs would be impossible to read.
    – Neil
    May 8, 2015 at 7:56
3

Some other answerers may be overthinking this.

== compares two values. References are values. Objects are not.

When you write

int a = 5;
int b = 5;
System.out.println(a == b);

the variable a contains the bits that make up the number 5, and so does the variable b. They are equal.

When you write

String a = new String("foo");
String b = new String("foo");
System.out.println(a == b);

the variable a contains a reference to one object, and the variable b contains a reference to a different object. Those are not equal.

They could have designed the language so that a == b would follow each reference and compare the objects instead of the references. In that case we would find different things to complain about. We'd be asking questions like "why does == sometimes dereference variables and sometimes not?" and "why does x == null throw a NullPointerException?" ... unless they special-cased null so it didn't get dereferenced, and then we'd definitely be asking why it sometimes dereferenced variables and sometimes not. And some objects can't even be compared - what does it mean to ask whether two sockets are equal?

Comparing the values is simple and is what they chose to go with, when they designed the language... even though it's not consistent when you use objects to emulate values, all the other options are inconsistent too.

3
  • If I had to choose the right answer to this question, yours would be my pick.
    – edalorzo
    Oct 4, 2021 at 17:48
  • 1
    In Java you can at least be certain that == will compare objects by reference. Operator overloading in C# takes away that guarantee, so you will have to check in code whether it's okay to use == for comparing references.
    – Domi
    Oct 27, 2021 at 13:14
  • Also if you are accustomed to C++, you might note that comparing two object variables with == in C++ does compare the objects, but that's because the variables contain the entire objects. If you compare pointers in C++ it has the same effect as in Java.
    – user253751
    Oct 27, 2021 at 13:33
2

It’s just historical. You use == to compare values. int and double are values in C. What about char* ? C tends to see the pointer as the value, not the C string, especially since char* could point to a single char, not a string. Therefore with char* the pointer is the value to be compared with ==.

C++ is close enough to C that pointers to an instance of a class are the values compared with ==. p == q compared the two pointers. *p == *q compares the instances.

In a newer language (Swift) you don’t use pointers, except for interfacing with other languages. We know that a and b could reference the same object, but we see a and b as the values, so == compares a and b. At the same time we are usually aware that only references are actually stored. So we have another operator === which compares not the values, but the references to the values.

1
  • One important point to make the standard equal comparing values of pointees instead of pointers be sensible (and introducing a special notation for identity testing) is that there are only at most those two levels: No indirection (optional), and exactly one level of indirection. Also the level of indirection must not be the programmers choice. Oct 3, 2021 at 13:05
1

I think it's time for an Update because Microsoft did learn from their mistake and implemented value equality as a default. First with F#:

type Person = { 
    name: string
}    

let p1 = { name = "Tom" }
let p2 = { name = "Tom2".Substring(0, 3) }

printfn "%A" (p1 = p2) // true

And more recently with C#:

record Person(string Name);

var p1 = new Person("Tom");
var p2 = new Person("Tom2".Substring(0, 3));

Console.WriteLine(p1 == p2); // true
1
  • This is because F# is a functional language which deals in immutable values, and because in C# strings are immutable values just like in a functional language. C# value types (structs) also have value equality (member-by-member). The mistake in C# is that strings are nullable without special syntax (like trailing ?) to indicate that. That is because they're implemented as references, not values, but that should never have been exposed at the language level, or even the CLR level (even though it has performance implications).
    – davidbak
    Oct 4, 2021 at 15:02
0

I was trying to think of why both of these languages went this route instead of inverting it and having == be logical equality and using .ReferenceEquals() for reference equality.

Because the latter approach would be confusing. Consider:

if (null.ReferenceEquals(null)) System.out.println("ok");

Should this code print "ok", or should it throw a NullPointerException?

1
  • 3
    ReferenceEquals is static, taking the two references to compare as parameters.
    – Joe Sewell
    Apr 13, 2020 at 3:49
0

What does "logical equality" mean? Shallow equality? Probably not, because this suffers from the same problems as reference equality. Therefore, it must be deep equality. As a language designer, you have to make sure your implementation is as generic as possible. This would probably work as long as you are dealing with object graphs that are acyclic. How would you implement deep equality for a circular linked list (or simply object A referencing object B and vice versa)? How about a complex mesh of objects each referencing other objects in the mesh, forward and backward. This cannot be solved generically. You need to understand the semantics of the object graph.

-3

For Java and C# the benefit lies in their being object oriented.

From a performance point of view - the easier to write code should also be quicker: since OOP intends for logically distinct elements to be represented by different objects, checking reference equality would be quicker, taking into consideration that objects can become quite large.

From a logical point of view - equality of an object to another does not have to be as obvious as comparing to object's properties for equality (ex. how is null==null logically interpreted? this can differ from case to case).

I think what it boils down to, is your observation that "you always want logical equality over reference equality". The consensus amongst the language designers was probably the opposite. I personally find it hard to evaluate this, since I lack the broad spectrum of programming experience. Roughly, I use reference equality more in optimisation algorithms, and logical equality more in handling data-sets.

3
  • 9
    Reference equality has nothing to do with object-orientation. Quite the opposite, actually: one of the fundamental properties of object-orientation is that objects that have the same behavior are indistinguishable. One object must be able to simulate another object. (After all, OO was invented for simulation!) Reference equality allows you to distinguish between two different objects that have the same behavior, it allows you to distinguish between a simulated object and a real one. Therefore, Reference Equality breaks object-orientation. An OO program must not use Reference Equality. Jun 9, 2013 at 10:55
  • @JörgWMittag: To do object-oriented program properly requires that there be a means of asking object X whether its state is equal to that of Y [a potentially transient condition], and also a means of asking object X whether it is equivalent to Y [X is equivalent to Y only if its state is guaranteed to be eternally equal to Y]. Having separate virtual methods for equivalence and state-equality would be good, but for many types, reference inequality will imply non-equivalence, and there's no reason to spend time on virtual method dispatch to prove it.
    – supercat
    Jan 17, 2014 at 18:52
  • In the implementation of object equality operator, a very cheap check for object reference is often done first - if the references are the same then obviously the values are the same.
    – gnasher729
    Oct 3, 2021 at 12:43
-3

.equals() compares variables by their contents. instead of == that compares the objects by their contents...

using objects is more accurate tu use .equals()

1
  • 3
    You assumption is incorrect. .equals() does whatever .equals() was coded to do. It is normally by contents, but it doesn't have to be. Also it's not more accurate to use .equals(). It just depends on what you are trying to accomplish.
    – Zipper
    Jun 12, 2013 at 22:02

Not the answer you're looking for? Browse other questions tagged or ask your own question.