while and do-while
Loop until a condition changes with while and do-while: reading lines, draining queues, following pagination cursors and retrying with a cap.
while (condition) { ... }
do { ... } while (condition);
while ((line = reader.readLine()) != null) { ... }A while loop checks its condition before each pass and keeps going while it is true. Use it when you do not know in advance how many passes you need: read until the input ends, retry until a call succeeds, take jobs until the queue is empty.
while (condition) {
...
}
do {
...
} while (condition);
do-while runs at least once
do/while checks the condition after the body, so the body always runs once. That fits cursor pagination: you must fetch the first page before you know whether there is a next one. Note the semicolon after the closing while (...); leaving it out is a compile error.
Read-until-null
Many Java APIs signal "no more" with null: BufferedReader.readLine() at end of input, Queue.poll() on an empty queue. The usual idiom assigns and tests in one condition:
String line;
while ((line = reader.readLine()) != null) {
...
}
The inner parentheses matter: without them line would be assigned the result of the comparison, which does not compile.
Always have a way out
A while loop has no built-in counter, so the most common bug is a loop that never ends. In a server that means a request thread stuck at full CPU. Every loop that waits on something outside your code needs a cap:
- a maximum number of attempts for retries;
- a deadline (compare against a
ClockorSystem.nanoTime()) for polling; - a page limit when following cursors from an API you do not control, in case it keeps returning the same cursor.
Make sure something inside the body changes the condition. If the condition reads a field that another thread sets, that field needs proper visibility (for example volatile or an atomic), otherwise the loop may never see the change.
Retries and polling
Do not spin in a tight loop while waiting. Sleep between attempts and grow the delay (exponential backoff) with an upper bound and a little random jitter, so a struggling downstream service is not flooded by clients retrying in step. Sleeping is cheap on a virtual thread, but for scheduled work a ScheduledExecutorService is usually a better fit than a loop.
If you know the number of passes, a for loop states it more clearly. If you are walking a collection, use for-each.
Example
When to use it
- Follow next-page cursors from a third-party API until it returns no cursor, with a page limit.
- Process an uploaded log or CSV file line by line without holding it all in memory.
- Retry a flaky downstream call with exponential backoff and a maximum number of attempts.
More examples
Reading lines until the input ends
readLine returns null at the end of the input, so the loop processes one line at a time without loading a whole file into memory. The same loop works for a file, a socket or a request body.
void main() throws IOException {
String log = """
GET /orders 200
GET /orders/7 404
POST /orders 201
GET /health 500
""";
int errors = 0;
try (BufferedReader reader = new BufferedReader(new StringReader(log))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.endsWith(" 404") || line.endsWith(" 500")) {
errors++;
IO.println("error: " + line);
}
}
}
IO.println("error lines: " + errors);
}
// Output:
// error: GET /orders/7 404
// error: GET /health 500
// error lines: 2Retry with backoff and a cap
The condition combines success and an attempt limit, so the loop always ends. Doubling the delay up to a maximum gives a failing downstream service room to recover.
boolean callService(int attempt) {
return attempt >= 3; // fails twice, then succeeds
}
void main() {
int attempt = 0;
int maxAttempts = 5;
long delayMs = 100;
boolean ok = false;
while (!ok && attempt < maxAttempts) {
attempt++;
ok = callService(attempt);
if (!ok) {
IO.println("attempt " + attempt + " failed, waiting " + delayMs + " ms");
// A real client would sleep here: Thread.sleep(delayMs);
delayMs = Math.min(delayMs * 2, 1000);
}
}
IO.println(ok ? "succeeded on attempt " + attempt : "gave up after " + attempt);
}
// Output:
// attempt 1 failed, waiting 100 ms
// attempt 2 failed, waiting 200 ms
// succeeded on attempt 3Draining a work queue with poll()
poll() returns null instead of throwing when the queue is empty, which makes it a natural loop condition. Because the loop adds work while it runs, a for-each over the queue would not work here.
record Job(String name, int attempt) {}
void main() {
Deque<Job> queue = new ArrayDeque<>(List.of(new Job("email", 1), new Job("resize", 1)));
Job job;
while ((job = queue.poll()) != null) {
boolean ok = !(job.name().equals("resize") && job.attempt() == 1);
if (ok) {
IO.println("done " + job.name() + " on attempt " + job.attempt());
} else {
IO.println("requeue " + job.name());
queue.offer(new Job(job.name(), job.attempt() + 1));
}
}
IO.println("queue empty");
}
// Output:
// done email on attempt 1
// requeue resize
// done resize on attempt 2
// queue empty
Discussion