Notes on Equality Comparison in Java
- Java
Personal notes on Java equality, covering primitives, references, and the difference between `==` and `equals()`.
The Context
In Java, we have two methods for equality comparison of values: equals() and the operator ==.
equals()exists since JDK 1.0 as a public method ofObject.==is part of Java’s equality and relational operators.
So what’s the difference between using == and equals() in Java? Turns out there’s a lot.
Example: java.net.URL equality comparison
java.net.URL is a good reminder that equals() is not automatically
“compare every field” in Java. The JDK defines two URL instances as equal when
they refer to the same resource, so the comparison is about the class’s meaning of equality,
not object identity.
That already makes it different from ==. If we create two separate
URL objects, == still asks whether both variables store the same
reference value, while equals() asks whether both URLs should count as the same
location according to
the API contract.
The surprising part is that host comparison may require name resolution
1 .
URL a = new URL("http://example.com/docs");
URL b = new URL("http://EXAMPLE.COM/docs");
a == b; // false: distinct objects
a.equals(b); // can be true: equality follows URL semantics
This is a compact example of the whole topic: == compares the stored value itself,
while equals() compares whatever the class defines as meaningful equality.
The Mechanics
To understand how primitive values and objects are asserted for equality in Java, I think we must first roughly understand how they are managed memory-wise.
For context, Java is a statically and strongly typed language. Static here means that every variable and expression has a type that is known at compile time, and strongly here means that the type of the value does not change. See Chapter 4 of the Java Language Specification for more detail on the type system.
Java Type System
Java is object-oriented, but not everything is an object since there are primitive values. In other words, there are two kinds of types in Java: primitive types and reference types.
It is clear that a primitive value such as an integer 5 is simply represented as ...000101 in
binary format in memory.
However, in Java, according to
JVM Spec section 2.3, the
specification only guarantees the semantics are 32-bit for int values. JVM implementations such
as HotSpot and OpenJ9 still have freedom in how they map the abstract machine to physical hardware.
Meanwhile, reference type values such as classes, arrays, interfaces, and String are really just
reference values that point the JVM toward the actual object in heap space.
Most simply, reference type values are values that the JVM can use to find the actual object during runtime.
The first distinction is conceptual: primitives are represented by their bits directly, while reference variables store a value the JVM can use to locate an object.
That is why == feels simple for primitives and more subtle for objects. The variable does not
carry the whole object; it carries enough information for the runtime to resolve one.
Less simply, a reference is a value that identifies an object; how it is represented, whether as a pointer, handle, compressed pointer, or something else, is JVM-dependent.
In the HotSpot implementation, a reference is a bit pattern representing a virtual memory address, either a pointer or a compressed offset decoded into a pointer, that indicates the starting byte of the JVM object header on the heap.
A useful mental model is that the reference value identifies where the object begins, not what the object contains semantically. That keeps the distinction between object identity and object state visible.
The exact encoding remains JVM-dependent, so this is an implementation-oriented picture rather than a universal law of the language.
Pass-by-value
Now that we’ve seen something about how Java manages its objects, we can look at why Java is pass-by-value and not pass-by-reference.
- The assumption: when passing an object to a method, we are passing the object itself, or rather the direct link to the original one.
This is the trap: because a method can mutate the object through the received reference, it is easy to describe that behavior as pass-by-reference.
But mutation through a copied reference value is still different from passing the caller’s variable itself. That distinction is exactly where many equality explanations get blurred.
- The reality: Java copies the bits inside the original variable. Since
pholds a reference value in this example, that value is copied into the new method parameter.
Once framed this way, the phrase “Java is pass-by-value” becomes much less mysterious. The copied value can still lead both variables to the same heap object, but the variables remain separate bindings.
That is also why == on references compares whether the stored reference values identify the
same object, not whether two objects merely look alike.
So in short, Java is pass-by-value, and the values passed are reference values for reference types.
Conclusion on Equality Comparison
Now that we have cleared the concepts around references in Java, we can finally produce a clearer
answer on how == and .equals() differ:
| Operator | Primitive type | Reference type |
|---|---|---|
== | Compares value-by-value | Compares the reference value, which could be the heap address |
.equals() | Primitive types do not have methods | Depends on the specific implementation. For example, |