Equality is an essential concept when programming, not only in Java but in pretty much all programming languages. After all, much of what we do when writing code has to do with comparing values and then making decisions based on the results of such comparisons.
Unfortunately, dealing with equality can often be tricky, despite being such a vital part of everyday coding. Equality in Java, specifically, can be quite confusing, for beginners and more experienced developers alike. That’s probably due to the fact that, in Java, there are several ways of handling equality, which can become overwhelming.
Today’s post has the goal of making this whole situation less confusing and less overwhelming. By the end of the post, you’ll have learned about the different ways to deal with equality in Java, and when to use which of them. We’ll also cover some best practices you should adopt and some pitfalls you must be aware of. Let’s get started.
Let’s begin by covering the equality comparison with the == operator. We’ll first show a quick example, and then dive a little deeper, explaining important details you need to be aware of when using the operator.
When you use the equality operator with primitive types, you’re just comparing their values. Take a look at the following examples:
// comparing ints int x, y; x = 10; y = 15; System.out.println(x == y); // prints 'false' // comparing chars char a, b; a = '\n'; b = '\n'; System.out.println(a == b); // prints 'true' // comparing booleans boolean t, f; t = true; f = false; System.out.println(t == f); // prints 'false'
When it comes to object types, the == operator is used to perform a referential equality comparison. What does that mean? It means that when you use the operator with object types, what you’re actually doing is testing whether the two variables have references that point to the same space in memory. Even if the objects referenced by the variables are identical in regards to their values, the results will still be false. This is somewhat unintuitive, and it can be a source of confusion—and bugs—especially for beginners. Let’s illustrate that with a code example. Suppose you have a Person class, like the one below:
public class Person { private final String name; private final int age; public String getName() { return name; } public int getAge() { return age; } public Person(String name, int age) { this.name = name; this.age = age; } }
Now, consider the following main method:
public static void main(String[] args) { Person p = new Person("Alice", 20); Person p2 = new Person("Alice", 20); System.out.println(p == p2); }
What do you think our little program will print when we run it? If your answer is false, then you’ve got it right. But why is that the case?
It has to do with references. When we initialize the p variable, we create a new instance of the Person class, which will live somewhere in the memory. The content of p is a reference (an “address”) to the location where the object resides.
When we utilize the p2 variable, we create another instance of Person. However, this instance will live at a different location in memory, and it’s this location that gets assigned to the variable. When using the == operator to compare the variables, we’re actually comparing the references they store, which are obviously different, so we get false as a result.
When using the operator to compare object types, the arguments must be compatible. That means that you can compare arguments of the same type, but also of types that have a child/parent relationship. If the arguments aren’t of the same type neither extend from one another, and you’ll get a compiler error. An example would show this more clearly. Consider the excerpt of code below:
public class Student extends Person { private final String school; public Student(String name, int age, String school) { super(name, age); this.school = school; } public String getSchool() { return school; } }
The example above features a new class, Student, that extends from the Person class shown in the first example. Now, take a look at the example below, that shows how we can compare instances of the two classes:
Person p = new Person("Alice", 20); Person p1 = new Person("Alice", 20); Student s = new Student("Alice", 20, "Hogwarts"); Student s1 = new Student("Alice", 20, "Hogwarts"); Person p2 = s; System.out.println(p == p1); // prints 'false' System.out.println(p2 == s); // prints 'true' System.out.println(s == s1); // prints 'false' System.out.println(p == s1); // prints 'false' System.out.println(p == "test"); // compiler error
The first comparison returns false. Both arguments have the same type (Person). They point to objects which have the exact same values for their fields. But even though their values are equal, they’re not the same objects. They don’t share the same place in memory, and that’s what the operator is comparing.
The second comparison results in true. Here we’re comparing two variables that are of different, but compatible types, since Person is the parent of Student. The comparison returns true because both variables point to the same object.
The third comparison checks two variables of type Student. The variables point to objects that have the exact same values. Yet, the comparison returns false, since the objects don’t share the same reference.
Following next, we have a comparison between an instance of Person and an instance of Student. The types are compatible, but the result is false since the objects the variables point to aren’t the same.
Finally, we have a comparison between an instance of Person and a string. Since these types aren’t compatible, we get a compiler error.
The second main way of performing an equality comparison in Java is by using the equals() method. How does this differ from the operator? To answer that question, let’s go back to our first example, but replacing the operator with the method. The Person class itself will remain the same, at least for now:
public static void main(String[] args) { Person p = new Person("Alice", 20); Person p1 = new Person("Alice", 20); System.out.println(p.equals(p1)); }
If you run the code, you’ll see that it prints false, just like the first version. So, what’s the difference?
To understand why the previous example behaved the way it did, we must learn how the equals() method really works. We’ll do that by clarifying an oft-repeated—but sadly inaccurate—claim about the method. When somebody asks about the difference between == and equals(), it doesn’t take long for this answer to show up:
The == operator compares the references, while the equals() compare the values themselves.
The inaccurate part is the second half of the quote. You see, the method doesn’t necessarily compare its arguments by their values. It only compares what was asked of it to compare. What does that mean? It means that for a class to have a custom equality comparison, it must override the equals() method, providing its own implementation. Let’s do just that for the Person class:
@Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!Person.class.isAssignableFrom(obj.getClass())) { return false; } Person other = (Person)obj; return other.name.equals(name) && other.age == age; }
The code above should be easy to follow. We first test whether the argument is null, in which case we return false. Then we check whether the argument’s class is compatible with Person. If it’s not, we also return false.
Finally, we cast the argument to Person and compare the values of its private fields with those from the instance’s fields, returning the result of the comparison. Now, if you go back and run the previous main example again, you’ll see that this time it prints true.
As we’ve just seen, it’s relatively easy to write a custom implementation of the equals() method. But what happens when a class doesn’t override it?
At first, it defaults to the implementation of the closest ancestral class which has overridden the method. But what if no ancestral class provided an implementation of the method? For instance, our Person class doesn’t even extend from anyone; from whom would it inherit the equals() implementation?
That’s not quite true, actually. In Java, every class has the Object class as a parent. And Object’s implementation of equals() defaults to ==. In other words: if neither your class nor its ancestors provide a custom implementation of the equals() method, you’ll end-up performing a reference comparison, perhaps inadvertently.
Before we part ways, let’s briefly offer a few tips on how to handle equality in Java, in the form of a brief list of best practices to adhere to and pitfalls to avoid.
First, don’t use == when comparing strings! That is unless you really want to do a comparison by references. This a very common mistake, and it can lead to annoying bugs. Actually, that applies not only to strings but to all object types.
Second, adhere to the principle of least astonishment when overriding the equals() method. That means that you should stick to widespread conventions, so your code doesn’t behave in an unexpected way that will alienate its users. For instance, you should always return false when comparing to null, the reasoning here being that, since null means nothing, it’s always going to be different than “something”, no matter what that something is.
Finally, always override hashCode() if you’re overriding equals(). If two objects are equal (by the equals() method), then they must have the same hash code. That will ensure that they can be used, for instance, as keys on a HashMap.
Java is one of the most popular programming languages ever, and that’s not an easy feat. Even after more than two decades, the language keeps being updated, making it easier to develop applications that are reliable, secure and easy to maintain.
Try Stackify’s free code profiler, Prefix, to write better code on your workstation. Prefix works with .NET, Java, PHP, Node.js, Ruby, and Python.
In Java, as in any other language, equality is a crucial concept, but it can also be somewhat tricky to master. In today’s post we’ve covered how to deal with equality in Java using both the == operator and the equals() method. We’ve explained the difference between the two with code examples, and we’ve walked you through a list of best practices and potential problems to be aware of.
That was just the tip of the iceberg, though. There are much more that can be said and written about equality than would fit into a single blog post. Keep always reading and practicing to learn more about Java and programming in general. This blog always features articles on a variety of subjects, not only on Java, but also on other programming languages and tools, so remember to always check it out.
Additionally, make good use of the tools at your disposal. For instance, check out Retrace, which is the APM solution by Stackify that can help you boost your application performance and quality. Try it today.
If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]