Topic 28 of 119 3. Operators in Java Interactive Tutorial

Relational Comparisons in Java: Evaluating Greater, Less, and Equal Values

10 mins read Last updated: 22 Aug 2026 19,844 views

Direct Answer & Core Concept

Java has six relational operators, and all of them produce a `boolean` rather than a value of the operand type: `==` for equal, `!=` for not equal, `>` for greater, `>=` for greater or equal, `<` for less, and `<=` for less or equal. What they compare depends entirely on the operand type. For primitives such as `int`, `long` and `char`, they compare values after the usual numeric promotion, so a `byte` or `char` operand widens to `int` first. For objects, `==` compares references and asks only whether both variables point at the same instance, which is why `new String("a") == new String("a")` is false while `.equals` on the same pair is true. String literals are a special case because the compiler pools them, so two literals with identical text do share one instance and compare equal with `==`. Floating point needs special care, because values like `0.1 + 0.2` are not exactly `0.3`, so a direct `==` fails and you should compare against an epsilon with `Math.abs(a - b) < 1e-9`. `NaN` is worse still: every comparison involving it is false, including `NaN == NaN`, so use `Double.isNaN` or `Double.compare`. For ordered objects such as strings, `compareTo` gives the ordering that `compareTo`-based sort relies on, and `Objects.equals(a, b)` is the null-safe form of `equals` for arguments that might be null.

Detailed Explanation & Mechanics

1. The Six Relational Operators

Java has six relational operators. Every one of them returns a boolean, never a value of the operand's own type, which is what makes them safe to drop straight into an if.

OperatorNameMeaning
==Equal toBoth sides are the same
!=Not equal toThe two sides differ
>Greater thanLeft is larger
>=Greater than or equalLeft is larger or the same
<Less thanLeft is smaller
<=Less than or equalLeft is smaller or the same

Two of the six work on any operand, and the other four need numbers or something the language can order.

Main.java
Run Online
int x = 20;
int y = 30;
System.out.println(x < y);
// true
System.out.println(x > y);
// false
System.out.println(x == y);
// false
System.out.println(x != y);
// true
System.out.println(x >= y);
// false
System.out.println(x

The <, >, <= and >= operators are ordering comparisons, so they need operands the language knows how to put in order. String is not one of them, which surprises people who expect any type with compareTo to work directly.

Main.java
Run Online
String a = "apple";
String b = "banana";
System.out.println(a < b);
// error: operator '

The type of both operands must also be compatible. A boolean has no ordering at all, so every ordering comparison on one is rejected.

Main.java
Run Online
boolean flag = true;
System.out.println(flag < false);
// error: bad operand types for binary operator '

3. Numeric Promotion Happens First

Before any comparison happens, both operands go through binary numeric promotion, the same widening rule the arithmetic operators use. That means a byte, short or char becomes an int first, and an int compared with a long becomes a long.

Main.java
Run Online
byte b1 = 10, b2 = 20;
System.out.println(b1 == b2);
// false, both widened to int, then compared
int i = 5;
long l = 5L;
System.out.println(i == l);
// true, i is widened to long, not the other way

Promotion also decides the result of comparing a narrow type against a literal, which is the only place a char surprises anyone.

Main.java
Run Online
char c = 'A';
System.out.println(c == 65);
// true, 'A' is the number 65
System.out.println('B' - 'A');
// 1, arithmetic on chars gives numbers
System.out.println('a' < 'b');
// true, because 97 is less than 98

That last point matters for input validation. A char read with Scanner.nextChar() is a single character, not a one character string, so comparing it against a String will not compile and comparing it against a char is what you actually want.

Main.java
Run Online
"5" is a String, two characters '5' is a char, the number 53 char c = '5';
c == '5' legal, both are chars c == "5" does not compile, char and String are unrelated types c == 53 legal, because '5' is the number 53 a common bug is reading a digit into a String and then comparing it with == instead of .equals, which silently fails for the reason covered in section 4

4. == on Objects Compares References

This is the single most common bug in Java. The == operator on objects asks only whether both variables point at the same instance in memory. It does not look at the contents at all.

Main.java
Run Online
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2);
// false, two separate objects
System.out.println(s1.equals(s2));
// true, same characters
System.out.println(s1.equals(s2));
// true, this is the content comparison

Two new expressions always produce two distinct objects, so the reference check is always false no matter how identical the text is.

Main.java
Run Online
System.out.println(new String("a") == new String("a"));
// always false
System.out.println(new String("a").equals(new String("a")));
// always true

To compare contents, call equals. Every value-carrying type in the standard library implements it, and it is defined by the class rather than by the operator.

Type== meansequals means
intValues are equalNot applicable, primitives have no methods
StringSame instanceSame characters in the same order
IntegerSame wrapper instanceSame numeric value
ListSame list objectSame elements in the same order
ArraySame array objectNot supported, arrays need Arrays.equals

The wrapper row catches experienced developers too, because small values are cached and the comparison appears to work until the value leaves that range.

Main.java
Run Online
Integer i1 = 100, i2 = 100;
System.out.println(i1 == i2);
// true, both are the cached 100
Integer i3 = 1000, i4 = 1000;
System.out.println(i3 == i4);
// false, outside the cache range of -128 to 127
System.out.println(i3.equals(i4));
// true, the reliable comparison

And the XOR trick from the bitwise topic is not a workaround here, because it compares the numbers and ignores the object identity completely.

Main.java
Run Online
System.out.println((i3 ^ i4) == 0);
// true, even though i3 == i4 is false

5. String Literals Are Pooled

String literals are not subject to the rule above, which is exactly why the bug survives code review. The compiler puts every literal in a shared pool, so two literals with identical text are the same object and == on them is true.

Main.java
Run Online
String a = "hello";
String b = "hello";
System.out.println(a == b);
// true, both refer to the pooled literal
System.out.println("hello" == "hello");
// true

Once a string comes from anywhere else, the pooling no longer applies.

Main.java
Run Online
String s = "hello";
String u = new String("hello");
String v = s.intern();
System.out.println(s == u);
// false, u is a fresh object
System.out.println(s == v);
// true, v is the pooled literal
System.out.println(s.equals(u));
// true

The trap is that a variable's provenance depends on where it came from, which is not visible at the comparison site. That is why the rule stays simple: use equals for content and never rely on == for strings.

Main.java
Run Online
where the string came from == two such strings equals ---------------------------- ------------------- ------------------- string literals true true new String(...) false true read from a file or network usually false true built with concat or a builder usually false true a constant in another class true if interned true only the first row is predictable, so never rely on it

Even concatenation behaves unintuitively. The compiler folds a concatenation of compile time constants back into a pooled literal, but any runtime concatenation allocates a brand new object.

Main.java
Run Online
String s = "ab";
System.out.println(s ==(s + ""));
// false, the concat ran at runtime
System.out.println(s.equals(s + ""));
// true

6. The Null-Safe Comparison

Objects.equals(a, b) handles both sides being null, which equals cannot, since calling equals on a null reference throws.

Main.java
Run Online
String n1 = null;
String n2 = null;
System.out.println(Objects.equals(n1, n2));
// true
System.out.println(n1.equals(n2));
// throws NullPointerException

The reason is that equals is an instance method, so the receiver has to exist before the call can start. Objects.equals is a static helper that checks for null on both sides before delegating.

Main.java
Run Online
a.equals(b) b must be non null, and a must be non null to receive the call Objects.equals(a, b) either may be null, checked first null null -> true null "abc" -> false "abc" null -> false "abc" "abc" -> true

There is a subtlety about which side you null-check. a.equals(b) throws if a is null, while Objects.equals(b, a) never throws regardless of order.

Main.java
Run Online
String maybe = readUserInput();
// could be null
if (maybe != null && maybe.length() > 5) {
    // guard first, then compare, and this is fine
}if (maybe != null && Objects.equals(maybe, "admin")) {
    // also fine, the guard is what protects the call
}

The rule is that the guard belongs to the receiver, so write a != null && a.equals(b) rather than relying on Objects.equals when the first check is what makes the second safe. Reach for Objects.equals when either side may be null and you do not want to think about which one it is.

7. Floating Point Needs an Epsilon

Comparing double or float with == looks like it should work and quietly does not, because values such as 0.1 cannot be represented exactly in binary.

Main.java
Run Online
double p = 0.1 + 0.2;
System.out.println(p);
// 0.30000000000000004
System.out.println(p == 0.3);
// false
System.out.println(0.5 + 0.25 == 0.75);
// true, these are exact in binary

The second line is the point. 0.5, 0.25 and 0.75 are all exact binary fractions, so they compare fine, and that is why the bug survives testing with round numbers. 0.1 is not exact, the sum lands one unit in the last place away from 0.3, and the equality fails.

The fix is to compare with a tolerance rather than exactly.

Main.java
Run Online
if (Math.abs(a - b) < 1e-9) {
    // treat as equal
}System.out.println(Math.abs(p - 0.3) < 1e-9);
// true

Choose the epsilon for the problem rather than pasting the same number everywhere. 1e-9 suits typical business arithmetic, while values measured from sensors or accumulated over many operations may need a much wider window, or a different approach entirely.

SituationRecommended approach
Exact arithmetic on moneyUse BigDecimal, not double
Approximate physics or measurementCompare with Math.abs(a - b) < epsilon
Sort or store valuesUse Double.compare, which defines a total order
Compare to zeroUse a tolerance, never == 0.0

BigDecimal deserves special care because equals on it is not a value comparison. It also compares the scale, so 0.1 and 0.10 are unequal by equals and equal by compareTo.

Main.java
Run Online
BigDecimal b1 = new BigDecimal("0.1");
BigDecimal b2 = new BigDecimal("0.10");
System.out.println(b1.equals(b2));
// false, different scale
System.out.println(b1.compareTo(b2) == 0);
// true, same numeric value

8. NaN Breaks Every Comparison

NaN, the result of dividing zero by zero or taking the square root of a negative number, is not equal to anything, including itself. That single fact makes it behave differently from every other value in the language.

Main.java
Run Online
double nan = 0.0 / 0.0;
System.out.println(nan == nan);
// false
System.out.println(nan != nan);
// true, even this one
System.out.println(nan > 1.0);
// false
System.out.println(nan < 1.0);
// false
System.out.println(nan >= nan);
// false

Note that nan != nan is true. That is not a contradiction, it is the definition: NaN means not a number, so it compares unequal to everything. The consequence for ordinary code is that NaN can never be found by testing equality.

Main.java
Run Online
if (value == Double.NaN) never true, so this check never fires if (Double.isNaN(value)) the correct test and for sorting, == finds nothing while compareTo gives a total order Double.compare(nan, nan) == 0 true Double.compare(nan, 1.0) > 0 true, NaN sorts above everything

Double.compare is the tool when you need ordering, because it defines NaN as greater than every other value and equal to itself, which makes it usable as a sort key where == is not.

Main.java
Run Online
System.out.println(Double.isNaN(nan));
// true
System.out.println(Double.compare(nan, nan) == 0);
// true
System.out.println(Double.compare(nan, 1.0) > 0);
// true

Infinity, by contrast, compares normally, so only NaN needs special handling.

Main.java
Run Online
System.out.println(1.0 / 0.0 == Double.POSITIVE_INFINITY);
// true
System.out.println(Double.MAX_VALUE * 2 > Double.MAX_VALUE);
// true, overflow to infinity

9. Comparing Objects That Have an Order

Many objects define a natural ordering through the Comparable interface, which supplies compareTo. This is what sorting uses under the hood, and it is also how you get > and < behaviour for a type that does not support them directly.

Main.java
Run Online
String apple = "apple";
String banana = "banana";
System.out.println(apple.compareTo(banana));
// negative, apple sorts first
System.out.println(apple.compareTo(banana) < 0);
// true, the idiomatic form

The contract has three parts, and all three matter: a negative result means the receiver sorts first, zero means the two are equivalent by ordering, and a positive result means the receiver sorts last. Only the sign is specified, never the magnitude, so never write code that expects a difference of exactly one.

For strings the ordering is by UTF-16 code unit, which puts every uppercase letter before every lowercase one.

Main.java
Run Online
System.out.println("Z".compareTo("a"));
// -7, because 'Z' is 90 and 'a' is 97

That is worth knowing when you sort user-visible text, since a case-sensitive sort will not put lowercase names in the position a reader expects.

Main.java
Run Online
System.out.println("ABC".compareToIgnoreCase("abc"));
// 0
System.out.println("ABC".equalsIgnoreCase("abc"));
// true

Sorting a list uses the same ordering.

Main.java
Run Online
List words = new ArrayList(List.of("pear", "apple", "fig"));
Collections.sort(words);
System.out.println(words);
// [apple, fig, pear]

Note that compareTo on strings returns 1 for a prefix, which surprises people who expect shorter to be less than longer in a numeric sense. It is lexicographic, so "a" comes before "ab".

Main.java
Run Online
System.out.println("a".compareTo("ab"));
// -1
System.out.println("ab".compareTo("a"));
//  1

And the wrapper trap has a clean solution here too, since compareTo compares values rather than references.

Main.java
Run Online
Integer i3 = 1000, i4 = 1000;
System.out.println(i3 == i4);
// false
System.out.println(i3.compareTo(i4) == 0);
// true

10. Arrays Are a Special Case

Arrays inherit Object.equals, so == on two arrays compares references and the inherited equals gives the same answer. Neither does what a newcomer expects.

Main.java
Run Online
int[] a = {
    1, 2, 3
};
int[] b = {
    1, 2, 3
};
System.out.println(a == b);
// false
System.out.println(a.equals(b));
// false, inherited from Object
System.out.println(Arrays.equals(a, b));
// true, the correct answer

Arrays.equals walks both arrays element by element, and there is a matching family for the other cases.

MethodPurpose
Arrays.equals(a, b)Element by element, works for primitives and objects
Arrays.deepEquals(a, b)For nested arrays of arrays
Arrays.compare(a, b)Ordering, for sorting arrays

The same distinction shows up in collections. List.equals compares contents element by element, which is why two separately built lists with the same items are equal.

Main.java
Run Online
List l1 = List.of("x");
List l2 = List.of("x");
System.out.println(l1.equals(l2));
// true, contents compared

11. Chaining Comparisons Does Not Work

Java has no combined less-than-or-equal operator the way some languages do. Writing a range check by chaining produces something that compiles but means something else entirely.

Main.java
Run Online
int age = 25;
System.out.println(age >= 18 && age

The chained form is rejected because the first comparison produces a boolean, and a boolean cannot be compared with an int. This is a good instinct, since even if it compiled it would be wrong: the parentheses in (18 <= age) <= 65 would compare a boolean to 65.

The rule is to use two separate comparisons joined by &&, or || when you want either end to be out of range.

Main.java
Run Online
if (age >= 18 && age 65) {
    System.out.println("out of range");
}

The same applies to any two-sided bound, and it is worth internalising because the chained form looks like it should work and the failure is a compile error rather than a silent bug.

12. Relational Operators Cheat Sheet

QuestionAnswer
Relational operators== != > >= < <=
Result typeboolean, always
On primitivesCompares values after promotion
On objects, ==Compares references, not contents
Compare object contentsequals, or Objects.equals
new String("a") == new String("a")false
"a" == "a"true, literals are pooled
Integer 1000 == Integer 1000false, cache covers only -128 to 127
Null safe comparisonObjects.equals(a, b)
null.equals(x)Throws NullPointerException
0.1 + 0.2 == 0.3false, 0.30000000000000004
Compare doublesMath.abs(a - b) < epsilon
NaN == NaNfalse
Test for NaNDouble.isNaN(x)
Total order for doublesDouble.compare(a, b)
Money arithmeticBigDecimal, and compare with compareTo
BigDecimal.equalsCompares scale too, so 0.1 is not 0.10
Compare ordered objectscompareTo(a, b) < 0
String orderingBy UTF-16 code unit, uppercase before lowercase
Compare arraysArrays.equals(a, b)
Range testa >= lo && a <= hi, chaining does not compile
'A' == 65true, a char is a number
int == longLegal, the int widens to long

The rule that covers most cases is short: use == for primitives, and equals for objects. The exceptions worth remembering are string literals, which are pooled so == appears to work, and the wrapper cache, which does the same for small numbers. Both are implementation details that should never be relied on, because a string that arrived from a file or a network will not be pooled and the comparison that passed in your test will fail in production.

Runnable Code Example: Relational Comparisons in Java

Java 17+
Main.java
Run Online
import java.util.Objects;
public class RelationalDemo {
    public static void main(String[] args) {
        // ---------- the six operators on primitives ----------
        int x = 20;
        int y = 30;
        System.out.println("x < y   " +(x < y));
        System.out.println("x > y   " +(x > y));
        System.out.println("x == y  " +(x == y));
        System.out.println("x != y  " +(x != y));
        System.out.println("x >= y  " +(x >= y));
        System.out.println("x <= y  " +(x <= y));
        // ---------- chars are numbers, not one character strings ----------
        System.out.println("'a' < 'b'       " +('a' < 'b'));
        System.out.println("'B' - 'A'       " +('B' - 'A'));
        System.out.println("'A' == 65       " +('A' == 65));
        // ---------- on objects, == compares references ----------
        String s1 = new String("hello");
        String s2 = new String("hello");
        System.out.println("s1 == s2        " +(s1 == s2));
        System.out.println("s1.equals(s2)   " + s1.equals(s2));
        // literals are pooled, so these are the same instance
        System.out.println("\"hi\" == \"hi\"     " +("hi" == "hi"));
        // ---------- the null safe comparison ----------
        System.out.println("Objects.equals   " + Objects.equals(s1, s2));
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

Why does new String("abc") == new String("abc") return false?

Because == on objects compares references, not contents, and two separate new expressions always create two distinct objects with different heap addresses. Calling equals on the same pair returns true because String overrides it to compare characters. The reason the bug survives review is that string literals are pooled by the compiler, so "abc" == "abc" is true and the difference is invisible until a string arrives from a file, a network call or a concatenation.
Q2

When is == correct for comparing objects?

When you genuinely mean identity, meaning you want to know whether both variables point at the very same instance. That matters for operations such as synchronized blocks, where the lock is the identity of the object. It is never correct when you mean the contents are the same, which is nearly always what people intend. The dangerous middle ground is the wrapper classes, since Integer caches values from -128 to 127 so Integer 100 == Integer 100 appears to work while Integer 1000 == Integer 1000 is false.
Q3

What does Objects.equals add over equals ?

Null safety on both sides. Calling a.equals(b) throws a NullPointerException if a is null, since the receiver must exist before the call can start. Objects.equals(a, b) is a static helper that checks both arguments for null before delegating, so it returns true for two nulls and false for one null and one non null. Use it whenever either side might be null, and note that the ordering of the arguments to a direct equals call still matters.
Q4

Why does 0.1 + 0.2 == 0.3 evaluate to false?

Because 0.1 and 0.2 have no exact binary representation, so the sum lands one unit in the last place away from 0.3, printing as 0.30000000000000004. Compare with a tolerance instead, such as Math.abs(a - b) < 1e-9, choosing the epsilon to suit the problem. Note that 0.5 and 0.25 are exact in binary, so round numbers compare fine and the bug survives testing with them. For money use BigDecimal rather than double.
Q5

What is special about NaN in comparisons?

NaN compares unequal to everything, including itself, so nan == nan is false and even nan != nan is true. That also means every ordering comparison such as nan > 1.0 is false, so you can never detect NaN with an equality test. Use Double.isNaN for detection and Double.compare when you need a total order, since Double.compare treats NaN as equal to itself and greater than every other value, which makes it usable as a sort key. Infinity, unlike NaN, compares normally.
Q6

How do you compare objects for ordering, such as strings?

Use compareTo, which comes from the Comparable interface and is what sorting uses internally. A negative result means the receiver sorts first, zero means equivalent, and positive means last. Only the sign is specified, never the magnitude, so do not expect a difference of exactly one. For strings the order is by UTF-16 code unit, which puts every uppercase letter before every lowercase one, so "Z".compareTo("a") is negative.
Q7

Can you write 0 < x < 10 in Java?

No, it does not compile, reporting bad operand types for binary operator '<'. The first comparison produces a boolean, and a boolean cannot be ordered against an integer, so the second comparison is rejected. Even if it compiled it would be wrong, since the grouping would be (0 < x) < 10. Write the range test as x >= 0 && x <= 10, and use || with the inverted bounds when you want to detect a value outside the range.
Q8

Why does comparing arrays with equals not work?

Arrays inherit equals from Object, which compares references, so two arrays with identical contents but different identities both fail an equals check and fail ==. Use Arrays.equals for element by element comparison, Arrays.deepEquals for nested arrays, and Arrays.compare when you need an ordering for sorting. Collection types do not have this problem because List and the other collection classes override equals to compare contents, which is why two separately built lists of the same items are equal.
Q9

What happens when you compare a byte, char or short with an int?

Binary numeric promotion runs first, so the narrow type widens to int and the comparison happens on ints. This is why 'A' == 65 is true, since a char is the number 65, and why 'B' - 'A' is 1. A char is a single character and not a one character string, so comparing it to a String does not compile. The same promotion applies when mixing int with long, where the int widens rather than narrowing, so int == long is perfectly legal.
Q10

How do you compare strings case-insensitively?

Use equalsIgnoreCase for the boolean test, or compareToIgnoreCase when you need an ordering. Both are instance methods on String. Be aware that this is not the same as lowercasing first, because it does not handle every Unicode case mapping, so for user-facing text beyond ASCII a proper locale-aware approach such as toLowerCase with a Locale is more reliable.
Q11

Why does BigDecimal.equals give a surprising result?

BigDecimal.equals also compares the scale, the number of digits after the decimal point, not just the numeric value. So new BigDecimal("0.1").equals(new BigDecimal("0.10")) is false even though the two represent the same number, while compareTo returns 0. When you mean value equality use compareTo(a, b) == 0 rather than equals, and be aware that using BigDecimal as a key in a HashMap or HashSet suffers the same problem, because hashCode is consistent with equals.
Q12

When would you legitimately use == on an object?

When identity is exactly what you need, most notably in a synchronized block or method, where the JVM locks on the identity of the monitor object and two equal but distinct objects would be separate locks. It is also acceptable for enums and for checking against the special null literal. Beyond those cases, if you are reaching for == on an object you almost certainly want equals, and the practical rule is that == is for primitives and identity while equals is for objects and contents.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Use == for primitives and equals for objects, and treat that as the default rather than something to decide per comparison. The reason is that == on objects asks about identity while equals asks about content, so the wrong one usually compiles, usually runs, and usually fails only in production once a string arrives from a file, a network call or a concatenation. The two traps that make the rule feel optional are both implementation details you should never lean on: string literals are pooled by the compiler, so "abc" == "abc" is true, and the wrapper classes cache values from -128 to 127, so Integer 100 == Integer 100 appears to work while Integer 1000 == Integer 1000 does not. Where an argument may be null, use Objects.equals(a, b), which checks both sides before delegating and never throws where a direct equals call would. For ordering rather than equality use compareTo, and remember that only its sign is meaningful, so never test for a specific difference. Never compare double or float with ==: 0.1 + 0.2 prints as 0.30000000000000004 and the equality fails, so use Math.abs(a - b) < epsilon with a tolerance chosen for the problem, or BigDecimal when the values are money, comparing those with compareTo rather than equals since equals also inspects the scale. Watch for NaN in any numeric comparison, because it is unequal to everything including itself, so detect it with Double.isNaN and use Double.compare when you need a total order for sorting. Arrays need Arrays.equals rather than equals, and a range test needs two comparisons joined by &&, since Java has no chained comparison operator. Finally, when you write an equals method yourself, override both equals and hashCode together, make the method consistent with your compareTo, and be explicit about null and type checks so the contract holds for every caller.
We value your privacy & transparency

OperateTools uses cookies and local storage to ensure basic tool functionality, preserve preferences, and analyze aggregated traffic. We never sell your personal data or index your document files. Learn more in our Privacy Policy.