Why it matters
Programs constantly ask questions about objects. Are these two customers the same? Is this point already in the set of visited cells? Which order is printed first? Each question needs an answer that the language can compute. The answer for a plain object is usually wrong, because the default rule compares where an object lives in memory, not what it holds.
This lesson shows how each language lets a class answer these questions. Java uses methods with fixed names: equals for comparing, hashCode for grouping, and toString for printing. C++ uses operators, which are symbols such as == and << that the class can give a meaning to. Both languages need the same rules to be followed, or sets and maps quietly break. The lesson ends with the rules that tie the pieces together.
This lesson assumes you have seen classes and const from the earlier lessons in this module. Immutable objects, which make the rules easier to keep, are covered in the previous lesson, Immutable objects and records.
The idea: what does equal mean for an object
Start with two identical paper forms. Each one has the name Asha, the age 31 and the city Pune, written in blue ink. Are they the same form? If you ask "are these the same piece of paper?", the answer is no, because they are two sheets. If you ask "do these hold the same information?", the answer is yes.
A computer has the same two questions. The first is identity: is this the very same object stored in one place in memory? The second is equality: do these two objects hold the same values? A class decides what equality means for its own objects.
In Java, the == operator asks the identity question for objects. It is true only when both sides point to the same object. The equals method is where a class answers the equality question.
In C++, the == operator asks whatever the class has defined. A class that defines operator== decides what == means for its objects. A class that defines nothing gets a compile error when someone writes == on it.
Java first: equals, hashCode and toString
Java has three methods that every object inherits from the Object class. Each one has a default that is almost never what you want for a plain data class.
equals(Object other)returns true when two objects are the same. The default checks identity.hashCode()returns a whole number that summarises the object. The default is a number the JVM assigns to each object, unrelated to its fields.toString()returns a text form of the object. The default prints the class name and a hexadecimal code.
Here is a small class that uses the default methods. The output shows why the defaults are not enough.
public class Main {
static class Point {
final int x;
final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println("a == b: " + (a == b));
System.out.println("a.equals(b): " + a.equals(b));
System.out.println("a: " + a.toString().startsWith("Main$Point@"));
}
}Output:
a == b: false
a.equals(b): false
a: trueThe two points hold the same numbers, but Java says they are different. The default equals checks whether a and b are the same object in memory, and they are two objects. The default toString prints a name followed by a code that differs for each object, so the printed text is not useful for a human.
A correct version overrides the methods. The word @Override asks the compiler to check that the method really replaces one from the parent class.
public class Main {
static class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof Point)) return false;
Point p = (Point) other;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return java.util.Objects.hash(x, y);
}
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
Point c = new Point(3, 4);
System.out.println("a = " + a);
System.out.println("a == b (reference): " + (a == b));
System.out.println("a.equals(b): " + a.equals(b));
System.out.println("a.equals(c): " + a.equals(c));
System.out.println("a.hashCode() == b.hashCode(): " + (a.hashCode() == b.hashCode()));
}
}Output:
a = (1, 2)
a == b (reference): false
a.equals(b): true
a.equals(c): false
a.hashCode() == b.hashCode(): trueThe equals method first checks identity as a shortcut. It then checks that other is a Point. Only then does it compare the numbers. The instanceof check is needed because equals receives any object, not just a point.
The hashCode method uses Objects.hash to combine the two numbers into one whole number. Equal points must give equal hash codes. The next section explains why this matters.
The toString method returns text that is useful when you print the object with System.out.println or join it into a string with +.
Java == on objects is still identity. This is why a == b is false above while a.equals(b) is true. The same trap appears with strings and boxed numbers, as the Common mistakes section shows.
C++: operator== and friend operator<<
C++ does not have one fixed method for equality. Instead, a class can define the == operator. Here is what happens when it does not.
class Point {
public:
Point(int x, int y) : x_{x}, y_{y} {}
private:
int x_;
int y_;
};
int main() {
Point a{1, 2};
Point b{1, 2};
return a == b ? 0 : 1;
}The compiler reports:
no_eq.cpp:13:14: error: no match for 'operator==' (operand types are 'Point' and 'Point')The compiler does not guess. It refuses to compare two objects of a type that has no == rule. A C++ class defines the rule by writing an operator function. The name of the function is operator followed by the symbol.
The simplest way to write operator== for a plain data class is to ask the compiler to compare each data member in order. Since C++20, you can write = default:
bool operator==(const Point& other) const = default;This line says: two points are equal when x_ is equal and y_ is equal. The const at the end promises that the comparison does not change either point. Without that const, the comparison fails on objects declared const, which is a common mistake shown below.
Printing works in a similar way. The << operator sends a value to an output stream such as std::cout. A class can define << so that std::cout << point prints the point. The function is usually a friend, which is a function outside the class that is still allowed to read its private members.
Output:
a = (1, 2)
a == b: 1
a == c: 0Look at the return type of operator<<. It returns std::ostream&, which is a reference to the same stream. That is what lets the chain std::cout << "a = " << a << "\n" work. Each << takes the stream returned by the one before it. If the function returned void, the chain would stop after the first use of the point.
The output shows 1 and 0 for the comparisons, not true and false. That is the default way std::cout prints a bool. Use std::boolalpha before the line if you want the word. This is a small detail, but it is easy to miss when you are checking output.
Java has no operator for printing. Java calls toString, and System.out.println(p) uses it automatically. In C++, the stream operator plays the same role. The main difference is that C++ can print any type that defines <<, while Java calls toString on any object.
C++ and the != operator
Once operator== is defined, != works without extra code. Since C++20, the compiler uses the == rule to answer != too. The following program has only == and it still compiles and runs.
Output:
a != b: 1
a == a: 1Java has no such rule. A Java class has equals but no != for objects, so the code writes !a.equals(b).
Hashing: hashCode and std::hash
Sets and maps need a fast way to find an item. Java HashSet and HashMap and C++ std::unordered_set and std::unordered_map both use a hash, which is a whole number computed from the item. The container stores items in buckets chosen by that number. To check whether an item is present, it computes the hash, goes to one bucket, and compares only the few items in that bucket.
This is why a hash value must agree with equality. If two objects are equal, they must produce the same hash. If they produce different hashes, the container may look in a different bucket and miss the equal item.
In Java, the class provides hashCode. In C++, the class provides a specialisation of the std::hash template for its type. Here is a C++ version for Point. The class is the same as before, with a std::hash specialisation added.
Output:
distinct points: 2The set stores a and then b. Because a == b, the set treats b as the same item and does not add it again. The result is two points, not three. The code uses the hash to find the bucket and operator== to decide that a and b match.
The hash function here combines the two numbers with a simple formula, h * 31 + y. Any formula works as long as it uses the same fields as operator==. The next section shows what goes wrong when it does not.
Java's Objects.hash(x, y) plays the same role as the C++ formula above.
Ordering: operator<=> and sorting
Sorting needs a way to say that one item comes before another. In C++ the operator for this is <=>, which is called the three-way comparison or spaceship operator. Writing auto operator<=>(const Cents&) const = default; gives the class all of <, <=, >, >= and <=>, comparing the fields in order. Sorting with std::sort uses these operators.
Java uses a different approach. A class implements the Comparable interface with a method named compareTo, or passes a separate Comparator to the sort. Both return a negative number, zero or a positive number. The C++ rule is shorter for plain data, because the compiler writes it for you.
The third example combines operators for a small money type. It defines addition, addition with assignment, equality, ordering and printing.
Output:
125 cents
500 cents
999 cents
total: 1624 cents
largest is 999: 1
sum equals 1624: 1
smallest < largest: 1Read the class from the top. The explicit keyword on the constructor stops code from turning a plain number into a Cents by accident. Writing Cents{0} is clear; writing total = 5 without the keyword would compile and hide a mistake. The constexpr keyword lets the compiler run these functions at compile time when the values are known. The rules in this lesson do not depend on it.
The operator+ returns a new Cents, and the operands stay the same. The operator+= changes the left side and returns a reference to it, which is the convention for compound assignment. The compiler generates operator== and operator<=> from the field-by-field rule.
The sort uses operator<. The sorted vector holds 125, 500 and 999. The front and back calls show the smallest and largest items.
The matching rules
The earlier examples used rules that must all hold at the same time. If one of them breaks, a set or map may lose items, a sort may give a wrong order, or a test may pass by accident. These rules apply to both languages. The Java name is in brackets when it differs.
- Equal objects have equal hashes. If
aequalsb, thena.hashCode()must equalb.hashCode()in Java, andstd::hashmust give the same number in C++. Sets and maps depend on this rule. - Equality uses the same fields as the hash. If
equalscomparesxandy, the hash must usexandy. A hash that uses fewer fields is allowed. A hash that uses an extra field breaks rule 1. - Equality is reflexive, symmetric and transitive. Every object equals itself. If
aequalsb, thenbequalsa. Ifaequalsbandbequalsc, thenaequalsc. - Equality ignores the printed form. Two objects can print differently and still be equal. Use the printed text only for people to read, never to decide equality.
- Operators that only read are marked
const. This lets them run onconstobjects, which is how most code receives them. - Immutable objects are the easiest to hash. If a value can change after it is put in a set, its hash may no longer match the bucket it was stored in. The previous lesson explains why immutable fields avoid this.
- Override
equalswith the exact Java signature. The parameter must beObject. If it is written asPoint, Java treats it as a new method, not an override.
Rule 7 is one of the most common Java mistakes. The Common mistakes section shows the compiler message.
Common mistakes
Calling a non-const operator== on a const object
In C++, the comparison operator must be const if it can be used on constant objects. The mistake is easy to make because the program still compiles when the objects are not const.
class Point {
public:
Point(int x, int y) : x_{x}, y_{y} {}
bool operator==(const Point& other) { return x_ == other.x_ && y_ == other.y_; }
private:
int x_;
int y_;
};
int main() {
const Point a{1, 2};
const Point b{1, 2};
return a == b ? 0 : 1;
}The compiler reports:
no_const_eq.cpp:15:17: error: passing 'const Point' as 'this' argument discards qualifiers [-fpermissive]The fix is to add const after the parameter list: bool operator==(const Point& other) const.
Forgetting to define operator== at all
This is the error from the start of the lesson. The message says no match for 'operator==', which means the compiler has no rule for the two types. The fix is to define the operator or use = default.
Equal objects that land in different buckets
This example breaks rule 2. The equality looks only at x, but the hash uses both x and y. Two points that are equal under == get different hashes, so the set stores both.
#include <cstddef>
#include <functional>
#include <iostream>
#include <unordered_set>
class Point {
public:
Point(int x, int y) : x_{x}, y_{y} {}
bool operator==(const Point& other) const { return x_ == other.x_; }
int x() const { return x_; }
int y() const { return y_; }
private:
int x_;
int y_;
};
namespace std {
template <>
struct hash<Point> {
std::size_t operator()(const Point& p) const noexcept {
std::size_t h = std::hash<int>{}(p.x());
return h * 31 + std::hash<int>{}(p.y());
}
};
} // namespace std
int main() {
Point a{1, 2};
Point b{1, 5};
std::cout << "a == b: " << (a == b) << "\n";
std::unordered_set<Point> seen;
seen.insert(a);
seen.insert(b);
std::cout << "set size: " << seen.size() << "\n";
}Output:
a == b: 1
set size: 2The program says a == b is true and then stores two items in the set. The set is wrong. The fix is to make the hash use only x, or to make operator== compare both fields. The second choice is almost always right for a value type.
Overriding equals with the wrong parameter type in Java
This is the mistake that rule 7 describes. The method takes Point, not Object, so it does not replace Object.equals. The @Override annotation catches it.
public class Main {
static class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Point other) {
return x == other.x && y == other.y;
}
}
public static void main(String[] args) {
System.out.println(new Point(1, 2).equals(new Point(1, 2)));
}
}The compiler reports:
Main.java:7: error: method does not override or implement a method from a supertype
@Override
^
1 errorThe fix is to write public boolean equals(Object other) and check the type inside the method, as the earlier Java example did. Without @Override, the program would compile and quietly call the default equals, which returns false here.
Using == to compare Java objects
Java programmers often write == where they meant equals. For objects, == checks identity. Java also keeps small whole numbers in a cache, so a comparison can look correct by accident.
public class Main {
public static void main(String[] args) {
String a = new String("hi");
String b = new String("hi");
System.out.println("== : " + (a == b));
System.out.println("equals: " + a.equals(b));
Integer i = 1000;
Integer j = 1000;
System.out.println("boxed == : " + (i == j));
System.out.println("boxed equals: " + i.equals(j));
}
}Output:
== : false
equals: true
boxed == : false
boxed equals: trueThe Integer values 1000 and 1000 are not equal under == because they are two separate boxed objects. A small value such as 100 might print true, because Java reuses cached objects for that range. Use equals for every object, including boxed numbers and strings.
Printing an object without an operator or toString
In C++, a class without operator<< cannot be sent to std::cout. The compiler reports that no << matches. In Java, a class without toString prints the default text with a memory code, as the first Java example showed. Both are signs that the class needs a readable text form. Write it once, and keep it simple. The text should show the data that matters, and it must not be used to decide equality.
Making the hash depend on something that changes
If a mutable object is stored in a set and then changed, the stored hash no longer matches the new value. The set may then fail to find the object. Immutable objects avoid this, which is why the rules put immutability in a separate item.
Practice
Exercise 1. Write a C++ class Money with two fields, long currency_code and long cents. Add operator== using = default, and operator<< that prints 840 1250 for a value of 1250 cents in currency 840. Use std::cout in main to print one value and to compare two equal values.
Show the answer
#include <iostream>
class Money {
public:
Money(int currency_code, long cents) : currency_code_{currency_code}, cents_{cents} {}
bool operator==(const Money& other) const = default;
friend std::ostream& operator<<(std::ostream& out, const Money& m) {
return out << m.currency_code_ << " " << m.cents_;
}
private:
int currency_code_;
long cents_;
};
int main() {
Money a{840, 1250};
Money b{840, 1250};
std::cout << a << "\n";
std::cout << "equal: " << (a == b) << "\n";
}Output:
840 1250
equal: 1The class uses the rule that = default compares each member in order. The printed form is the currency code followed by the cents, which is enough to show the idea. A real program would print the currency name.
Exercise 2. The Java class below is supposed to be used in a HashSet. It has a correct equals method but no hashCode. Explain what happens when two equal points are added to the set, then fix the class.
import java.util.HashSet;
import java.util.Set;
public class Main {
static class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof Point)) return false;
Point p = (Point) other;
return x == p.x && y == p.y;
}
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println(set.size());
}
}Show the answer
Without hashCode, each Point inherits the default hash, which is a number the JVM assigns to each object. Two equal points get different numbers, so they usually land in different buckets. The set does not compare them, so it keeps both, and the printed size is 2. The fix is to add the method below to the class, so that equal points get equal hashes:
@Override
public int hashCode() {
return java.util.Objects.hash(x, y);
}With this method, the program prints 1.
Exercise 3. Write a C++ class Name with one std::string field. Give it a const operator== that compares the strings. Then predict and check the output of std::cout << (Name{"Asha"} != Name{"Ravi"}).
Show the answer
#include <iostream>
#include <string>
#include <utility>
class Name {
public:
explicit Name(std::string text) : text_{std::move(text)} {}
bool operator==(const Name& other) const { return text_ == other.text_; }
private:
std::string text_;
};
int main() {
std::cout << (Name{"Asha"} != Name{"Ravi"}) << "\n";
}Output: 1. The != is written by the compiler from ==, so the result is the opposite of equality. The two names are different, so the output is 1, which means true.
Exercise 4. The C++ class below defines operator== correctly but has a hash that uses an extra field. Explain what goes wrong with std::unordered_set and fix the hash.
#include <cstddef>
#include <functional>
#include <unordered_set>
class Tag {
public:
Tag(int id, int version) : id_{id}, version_{version} {}
bool operator==(const Tag& other) const { return id_ == other.id_; }
int id() const { return id_; }
int version() const { return version_; }
private:
int id_;
int version_;
};
namespace std {
template <>
struct hash<Tag> {
std::size_t operator()(const Tag& t) const noexcept {
return std::hash<int>{}(t.id()) * 31 + std::hash<int>{}(t.version());
}
};
} // namespace stdShow the answer
Two tags with the same id but different version are equal under ==, but their hashes differ because the hash uses version. The set can store both, so it treats equal items as different. The fix is to hash only the fields that == compares:
namespace std {
template <>
struct hash<Tag> {
std::size_t operator()(const Tag& t) const noexcept {
return std::hash<int>{}(t.id());
}
};
} // namespace stdExercise 5. Predict the output of this Java program, then run it to check.
public class Main {
public static void main(String[] args) {
String a = "hi";
String b = "hi";
String c = new String("hi");
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a.equals(c));
}
}Show the answer
Output: true, false, true. The two literals "hi" are the same object in Java's string pool, so == is true. The new String creates a separate object, so == is false. equals compares the text, so it is true.
Summary
- Java gives every object
equals,hashCodeandtoString. The defaults compare identity and print an identity code, so override them for data classes. - C++ uses operators. A class defines
operator==for equality,operator<<for printing, andoperator<=>for ordering. The= defaultform writes them for plain data. std::hashin C++ andhashCodein Java must give equal hashes for equal objects, and must use the same fields that equality uses.- Sets and maps break silently when the rules are broken, so test equality and hashing together.
- Use
equalsin Java, not==, when you want to compare the contents of objects.