Java Relational Operators: Syntax and Pitfalls
java relational operators: Understand Java's six relational operators, how they behave with primitives and objects, and where developers commonly make comparison mista...
Java relational operators are the six binary operators that compare two values and produce a boolean result: ==, !=, <, <=, >, and >=. They appear in nearly every conditional expression, loop guard, and filter predicate, yet their behavior differs significantly between primitive types and reference types. Understanding where these operators compare values versus references is the difference between code that works reliably and code that fails intermittently in production.
The Six Relational Operators
Java defines six relational operators, each producing a boolean result:
| Operator | Meaning | Example |
|---|---|---|
== | equal to | a == b |
!= | not equal to | a != b |
< | less than | a < b |
<= | less than or equal to | a <= b |
> | greater than | a > b |
>= | greater than or equal to | a >= b |
All six are binary operators: they take two operands and evaluate to true or false. The ordered comparison operators (<, <=, >, >=) require numeric operands. The equality operators (==, !=) accept any types, but their meaning depends entirely on whether the operands are primitives or references.
Behavior with Primitive Numeric Types
For numeric primitives, the relational operators compare the actual numeric values. When the two operands have different primitive types, Java applies binary numeric promotion, widening the smaller type to the larger type before performing the comparison.
int a = 10; long b = 20L; boolean result = a < b; // true; a is widened to long before comparison double x = 0.5; float y = 0.25f; boolean greater = x > y; // true; y is widened to double
The ordered comparison operators work with all numeric primitives: byte, short, int, long, float, and double. They do not work with boolean or with reference types. The equality operators == and != work with all primitive types, including boolean.
Reference Equality vs Value Equality
The most common source of confusion with Java relational operators is the distinction between == and the equals() method. For reference types, == compares references, not object content. Two distinct objects with identical field values compare as not equal.
String first = new String("hello"); String second = new String("hello"); System.out.println(first == second); // false, different objects System.out.println(first.equals(second)); // true, same content
The equals() method must be overridden by the class to provide value-based equality. The default implementation in Object behaves like ==, so any class that does not override equals() retains reference semantics. This is why records and value-based classes override equals() explicitly.
Numeric Edge Cases: NaN and Floating-Point Comparison
Floating-point comparison has a specific edge case that catches developers: NaN (Not a Number). The relational operators have precisely defined behavior with NaN values:
NaN == anythingis alwaysfalse, includingNaN == NaNNaN != anythingis alwaystrue- All ordered comparisons (
<,<=,>,>=) with NaN arefalse
double value = Double.NaN; System.out.println(value == Double.NaN); // false System.out.println(value != Double.NaN); // true System.out.println(value < 1.0); // false
You cannot detect NaN using ==. The correct approach is Double.isNaN(value) or Float.isNaN(value). This behavior also affects any calculation that produces NaN, such as 0.0 / 0.0 or subtracting infinity from infinity.
Comparing Objects with Comparable
When you need ordered comparison of objects, the relational operators do not apply directly. Java provides the Comparable interface for this purpose. The compareTo() method returns a negative integer, zero, or a positive integer depending on whether the current object is less than, equal to, or greater than the argument.
Integer a = Integer.valueOf(5); Integer b = Integer.valueOf(10); int comparison = a.compareTo(b); // negative value, meaning a < b
For wrapper types like Integer, the ordered comparison operators work through unboxing. This is convenient but introduces a risk: if the reference is null, unboxing throws NullPointerException.
Common Mistakes and Their Consequences
A frequent mistake is using == to compare Integer values outside the cached range. Java caches Integer instances from -128 to 127, so == may return true for small values but false for larger ones.
Integer a = 100; Integer b = 100; System.out.println(a == b); // true, both reference cached instances Integer c = 200; Integer d = 200; System.out.println(c == d); // false, distinct objects
The cache range is guaranteed for -128 to 127 but may be extended by the JVM. The reliable approach is to use equals() or compareTo() for wrapper types.
A related mistake is comparing strings with == after assuming that string interning guarantees reference equality. String literals are interned, so two literals with identical content may share a reference, but strings created at runtime do not.
Runtime Cost and Maintainability
The relational operators themselves are inexpensive. For primitives, each comparison compiles to a single bytecode instruction. For reference types, == is also a single reference comparison. The cost of equals() depends on the implementation; a well-written equals() for a simple class performs a few field comparisons.
The maintainability concern is more significant than the performance cost. Using == where value equality is intended creates bugs that are difficult to trace because the code compiles and runs without error. The failure appears only at runtime when specific values are compared. Code reviews should focus on whether == is used with reference types and whether the class in question overrides equals().
For ordered comparison of custom objects, implementing Comparable and using compareTo() keeps the ordering logic in one place. This prevents the same rules from being duplicated across sorting, searching, and validation code.
Using Relational Operators in Streams and Lambdas
Relational operators appear frequently in stream pipelines and lambda expressions. The operators themselves behave the same way, but the surrounding context can introduce subtle issues.
List<Integer> values = List.of(3, 1, 4, 1, 5); long count = values.stream() .filter(v -> v > 2) .count();
The lambda parameter v is an Integer object, and the > operator triggers unboxing. If any value in the list were null, the unboxing would throw NullPointerException. This is a common production failure when data comes from external sources.
Where Relational Operators Cannot Be Used
The ordered comparison operators (<, <=, >, >=) require numeric operands. They cannot be used with boolean values or arbitrary objects. The equality operators == and != work with any types, but for reference types they compare references unless equals() is involved.
For enums, == is safe and recommended because enum constants are singletons. Comparing enum values with == is both correct and more efficient than calling equals().
enum Status { ACTIVE, INACTIVE } Status status = Status.ACTIVE; if (status == Status.ACTIVE) { // reliable, enum constants are singletons }
This is one case where reference equality is exactly the right semantics, and the JVM guarantees that each enum constant exists only once per class loader.