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.

Syntaxwhile (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 Clock or System.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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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: 2

Retry 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.

Example · java
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 3

Draining 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.

Example · java
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

  • Be the first to comment on this lesson.