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 equalOutput:
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 equalFor 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 charactersEven 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")) { ... } // ✅ negatedWhen == 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 != nullcrashes whenbodyis 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. Inifconditions, 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.
- Create
AssertResponse.java. - In
main, declare four variables:int statusCode = 200;,long responseTimeMs = 1850;,String body = "{\"id\":42}";, andlong slaMs = 2000;. - Build a single
boolean isPassing = ...expression that combines:statusCodeis between 200 and 299 (use&&).responseTimeMsis less thanslaMs.bodyis not null and not empty (!body.isEmpty()).
- Print
"✅ PASS"or"❌ FAIL"with a ternary. - Now mutate the inputs to break each condition in turn (e.g.,
statusCode = 500, thenbody = null) and re-run. Confirm the message flips and that the null case doesn't crash thanks to your null-first ordering. - Add a single line at the top:
String env = null;. Compare it safely against"production"using"production".equals(env). Print the result. Confirm it printsfalseand doesn't throw. - Stretch: rewrite the equality check
if (statusCode == 200)using a comparison range —statusCode >= 200 && statusCode < 300. Run withstatusCode = 204and 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.