Comparison and Logical Operators

7 min read

The condition inside an if is an expression that has to evaluate to a boolean — true or false. To build those expressions you combine comparison operators (==, !=, >, <, >=, <=) and logical operators (&&, ||, !). The mechanics are the same as JavaScript, with one critical exception that we touched on in lesson 1 and will nail down here: object equality.

Comparison operators

Six operators, all returning a boolean:

int statusCode = 200;
int responseTime = 1450;
 
System.out.println(statusCode == 200);      // true   — equal
System.out.println(statusCode != 500);      // true   — not equal
System.out.println(responseTime > 1000);    // true   — greater than
System.out.println(responseTime < 2000);    // true   — less than
System.out.println(statusCode >= 200);      // true   — greater than or equal
System.out.println(responseTime <= 1500);   // true   — less than or equal

Output:

true
true
true
true
true
true

For numbers (int, double, long, float), char, and boolean, these all do exactly what you'd expect. The trap is what == means for objects.

== vs .equals() — the rule that defines Java

For primitives (int, boolean, char, etc.), == compares the value:

int a = 200;
int b = 200;
System.out.println(a == b);  // true — values are equal

For objects (String, arrays, custom classes), == compares the reference — i.e., "are these two variables pointing at the same object in memory?" That's almost never the question you actually want to ask.

String x = new String("staging");
String y = new String("staging");
 
System.out.println(x == y);          // false — two different String objects
System.out.println(x.equals(y));     // true  — same characters

Even without new String(...), the moment a String comes from an HTTP response, a CSV file, or String.valueOf, you get a fresh object whose reference doesn't match a literal. The reliable rule:

  • Primitives: use == and !=.
  • Objects (Strings included): use .equals() and .equals() negated with !.
String env = readFromConfig();          // pretend this returns "staging"
 
if (env.equals("staging")) { ... }      // ✅ comparing values
if ("staging".equals(env)) { ... }      // ✅ also null-safe
if (!env.equals("production")) { ... }  // ✅ negated

When == and equals() each apply

== vs .equals() — when each is correct

== (values for primitives, references for objects)

  • int a = 200; int b = 200; → a == b is true ✅

  • boolean p = true; if (p == true) { ... } ✅

  • char g = 'A'; if (g == 'A') { ... } ✅

  • String x = "staging"; if (x == "staging") sometimes-true ⚠️

  • two arrays with the same contents — false ❌

  • two custom objects with the same fields — false ❌

.equals(...) (always values, for any object)

  • env.equals("staging") — compares characters ✅

  • "staging".equals(env) — null-safe (no NPE) ✅

  • env.equalsIgnoreCase("STAGING") — case-insensitive ✅

  • Arrays.equals(arr1, arr2) — element-by-element ✅

  • user.equals(other) — your class can define what equality means ✅

Print this rule on a sticky note for your monitor: primitives ==, objects .equals(). Internalise it now and you skip an entire category of test bugs.

Logical operators

Three operators combine boolean expressions:

int statusCode = 200;
int responseTime = 1200;
String body = "{\"ok\":true}";
 
boolean passed = statusCode == 200 && responseTime < 2000 && body != null;
boolean serverIssue = statusCode >= 500 || responseTime > 5000;
boolean stillTrying = !serverIssue;
 
System.out.println("Test passed:    " + passed);
System.out.println("Server issue:   " + serverIssue);
System.out.println("Keep retrying:  " + stillTrying);

Output:

Test passed:    true
Server issue:   false
Keep retrying:  true
  • && — AND. True only when both sides are true.
  • || — OR. True when either side is true.
  • ! — NOT. Flips true to false and vice versa.

These are identical to JavaScript and Python.

Short-circuit evaluation

&& and || are short-circuit: Java evaluates the left side first and stops as soon as the result is determined.

String env = null;
 
if (env != null && env.equals("staging")) {  // safe — second part never runs
    System.out.println("On staging");
}

Because env != null is false, Java doesn't bother evaluating env.equals("staging") — which would throw a NullPointerException. Short-circuiting is what lets you safely null-check before calling a method on the same line. The same logic for ||: if the left side is true, the right side is skipped.

There are also non-short-circuit operators & and | — they always evaluate both sides. They exist for bitwise operations and very specific rare cases. In test code, always use && and ||.

The ternary operator

A compact one-line if/else that returns a value:

int statusCode = 200;
String label = statusCode == 200 ? "✅ PASS" : "❌ FAIL";
System.out.println(label);

Output:

✅ PASS

Read it as: "if statusCode == 200, the value is ✅ PASS, otherwise ❌ FAIL." Identical to JavaScript. Useful for short formatting; avoid nesting more than one ternary, which gets unreadable fast.

instanceof — Java's "is this object a …?"

instanceof checks whether a reference points at an object of a given type. You'll meet it more in chapter 5 when we cover polymorphism, but it's worth previewing:

Object response = "200 OK";
 
if (response instanceof String) {
    System.out.println("Got a string response");
}

Java 16+ added pattern matching so you can capture the value at the same time:

Object response = "200 OK";
 
if (response instanceof String s) {
    System.out.println("Length is " + s.length());
}

The s is automatically a String inside the if. This pattern is useful when test data arrives typed as Object and you need to dispatch on its real type.

Putting it together — validating an API response

public class ResponseValidator {
    public static void main(String[] args) {
        int statusCode = 200;
        long responseTimeMs = 1450;
        String body = "{\"ok\":true}";
        long slaMs = 2000;
 
        boolean isPassing =
                statusCode == 200
                && body != null
                && !body.isEmpty()
                && responseTimeMs < slaMs;
 
        String label = isPassing ? "✅ PASS" : "❌ FAIL";
        System.out.println(label + " — status " + statusCode + " in " + responseTimeMs + "ms");
    }
}

Output:

✅ PASS — status 200 in 1450ms

Four conditions, joined with &&, evaluated left-to-right. The null check (body != null) sits before body.isEmpty() — the order matters because of short-circuiting. Reverse them and a null body crashes the test with NullPointerException instead of returning false cleanly.

⚠️ Common mistakes

  • Using == to compare Strings. Reviewed in lesson 1; reviewed again here because it's the single most common Java bug. Use .equals(). Always.
  • Wrong null-check order with &&. body.isEmpty() && body != null crashes when body is null because the left side runs first. Always null-check first: body != null && !body.isEmpty(). This is short-circuit evaluation working for you.
  • Confusing & with && and | with ||. The single-character versions don't short-circuit and exist mainly for bitwise work. In if conditions, use the doubled form. The compiler accepts the single form silently for booleans, so this is a quiet bug source.

🎯 Practice task

Write a real assertion helper. 20-25 minutes.

  1. Create AssertResponse.java.
  2. In main, declare four variables: int statusCode = 200;, long responseTimeMs = 1850;, String body = "{\"id\":42}";, and long slaMs = 2000;.
  3. Build a single boolean isPassing = ... expression that combines:
    • statusCode is between 200 and 299 (use &&).
    • responseTimeMs is less than slaMs.
    • body is not null and not empty (!body.isEmpty()).
  4. Print "✅ PASS" or "❌ FAIL" with a ternary.
  5. Now mutate the inputs to break each condition in turn (e.g., statusCode = 500, then body = null) and re-run. Confirm the message flips and that the null case doesn't crash thanks to your null-first ordering.
  6. Add a single line at the top: String env = null;. Compare it safely against "production" using "production".equals(env). Print the result. Confirm it prints false and doesn't throw.
  7. Stretch: rewrite the equality check if (statusCode == 200) using a comparison range — statusCode >= 200 && statusCode < 300. Run with statusCode = 204 and confirm both styles agree. Reasoning about ranges with && is the everyday work of a test framework.

You can now build any condition the next two chapters will throw at you. Lesson 3 introduces the loop constructs that walk over arrays and lists.

// tip to track lessons you complete and pick up where you left off across devices.