Walk through the full WatchService usage lifecycle, from creating the service to processing events. Why is calling reset() essential?
answer
- create → register → take → drain → reset
- Key states: ready / signalled / invalid
- take() blocks, poll() doesn't, poll(timeout) bounded
- reset() re-arms; false means key invalid
- pollEvents() drains; forgetting reset() = silence
basics
~20 sYou create a WatchService, register a directory for the event kinds you want, then loop: block on take() until a key fires, drain its events with pollEvents(), handle each one, and call key.reset() to keep watching. Without reset() the key stops delivering new events.
solid answer
~50 sThe lifecycle is: (1) obtain the service via FileSystems.getDefault().newWatchService(); (2) register a directory Path with the event kinds (ENTRY_CREATE/MODIFY/DELETE), which returns a WatchKey; (3) enter a loop where you call ws.take() to block until a key is signalled (or poll()/poll(timeout) for non-blocking/bounded waits); (4) drain pending changes with key.pollEvents(), inspecting each WatchEvent's kind() and context(); (5) call key.reset() to re-arm the key. reset() is essential because after a key becomes signalled it stays in the signalled state and delivers no further events until reset returns it to the ready state; forgetting it is the classic bug where you get exactly one batch of events and then silence. reset() also returns false when the key is no longer valid (directory deleted or the service closed), which is your signal to stop processing that key. You should also handle OVERFLOW and close the service to release native resources.
code
java · 21 linesimport java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
public class Watcher {
public static void main(String[] args) throws Exception {
Path dir = Paths.get("/var/inbox");
try (WatchService ws = FileSystems.getDefault().newWatchService()) {
dir.register(ws, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
for (;;) {
WatchKey key = ws.take(); // block until signalled
for (WatchEvent<?> e : key.pollEvents()) {
if (e.kind() == OVERFLOW) continue; // events were lost; re-scan if needed
Path name = (Path) e.context(); // relative to dir
Path full = dir.resolve(name);
System.out.println(e.kind() + ": " + full);
}
if (!key.reset()) break; // re-arm, or exit if invalid
}
}
}
}go deeper
Can recite the basic loop order: register, wait, handle events, reset, and knows reset keeps it going.
Explains the key state machine (ready/signalled/invalid), why reset() is mandatory, the take vs poll vs poll(timeout) distinction, and that context() is relative.
Discusses resource management (close/cancel, ClosedWatchServiceException), draining semantics of pollEvents, and structuring the loop on a dedicated thread with interruption handling.
Frames reset() as an ack-style backpressure mechanism, reasons about thread ownership and lifecycle in a service, and designs robust shutdown and error-recovery around invalid keys and OVERFLOW.
## The lifecycle in detail WatchService follows a deliberate **state machine** on its keys, and getting the loop right is the heart of using it correctly. ### Step 1 — Create the service ```java WatchService ws = FileSystems.getDefault().newWatchService(); ``` A `WatchService` is a closeable resource backed by native OS handles, so it belongs in a try-with-resources or must be explicitly `close()`d. ### Step 2 — Register a directory ```java Path dir = Paths.get("/var/inbox"); WatchKey key = dir.register(ws, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); ``` `register` is a method on **Path**, taking the service and a varargs of **event kinds**. It returns a **WatchKey** representing this registration. Re-registering the same directory updates the existing key's event set rather than creating a duplicate. ### The WatchKey state machine A WatchKey is always in one of three states: - **Ready** — registered and waiting; events accumulate but the key is not yet queued to the service. - **Signalled** — at least one event is pending; the key has been queued so `take()`/`poll()` can return it. Crucially, **once signalled it stays signalled** and will not be re-queued for new events until you reset it. - **Invalid** — no longer watching (directory deleted, key cancelled, or service closed). ### Step 3 — Retrieve a signalled key Three retrieval methods on the service: - `take()` — **blocks** until a key is signalled (like a blocking queue's take). Throws `InterruptedException`. - `poll()` — returns a signalled key immediately, or `null` if none. - `poll(long timeout, TimeUnit unit)` — waits up to the timeout, then returns `null`. ### Step 4 — Drain the events ```java for (WatchEvent<?> event : key.pollEvents()) { WatchEvent.Kind<?> kind = event.kind(); if (kind == StandardWatchEventKinds.OVERFLOW) { /* re-scan, continue */ } Path name = (Path) event.context(); // file name relative to dir Path full = dir.resolve(name); // absolute/resolved path } ``` `pollEvents()` **retrieves and removes** the pending events for that key (it drains the queue). Each `WatchEvent` carries a `kind()`, a `count()` (repeat count for coalesced events), and a `context()` (the entry name as a Path **relative to the watched directory** — resolve it against the dir to get the real path). ### Step 5 — reset() (the critical step) ```java boolean valid = key.reset(); if (!valid) { // directory inaccessible / key no longer valid → stop watching it } ``` `reset()` transitions the key from **signalled** back to **ready**, so it can be queued again for future events. **This is the single most common mistake:** if you do not call reset(), the key stays signalled, never gets re-queued, and your loop blocks forever on the next `take()` even though files keep changing. reset() returns `false` if the key is no longer valid — that is your cue to remove it from any map of watched directories and stop processing it. ### The canonical loop ```java for (;;) { WatchKey key = ws.take(); // 3: block for (WatchEvent<?> e : key.pollEvents()) { /* 4: handle */ } if (!key.reset()) break; // 5: re-arm or exit } ``` ### Cleanup When done, `ws.close()` releases the native resources and invalidates all keys; an active `take()` will then throw `ClosedWatchServiceException`. You can also stop watching one directory with `key.cancel()` without closing the whole service. ## Why reset() exists at all The signalled-until-reset design lets you safely drain a key's events on one thread while the service keeps accumulating new events behind it; resetting is your explicit acknowledgement that you have consumed the batch and are ready for the next. It mirrors the ack pattern of message queues.
- What happens if you never call reset() on a signalled key?The key stays in the signalled state and is never re-queued, so the service delivers no further events for that directory and your take() loop blocks forever even though files keep changing.
- How do you turn a WatchEvent's context into a usable absolute path?The context() is the entry name relative to the watched directory, so you resolve it against that directory: watchedDir.resolve((Path) event.context()).
- What's the difference between key.cancel() and ws.close()?cancel() stops watching just that one directory and invalidates that single key; close() shuts down the entire service, invalidating all keys and causing a blocked take() to throw ClosedWatchServiceException.
reset() is like flipping a pager back to 'ready' after you've read the message — until you do, it won't beep again even if new pages pile up.
saying these in an interview costs you the question
- Forgetting key.reset() and then claiming WatchService 'only fires once' as if that's a bug in the API
- Treating context() as an absolute path instead of a name relative to the watched directory
- Calling pollEvents() repeatedly expecting the same events (it drains them)
- Not closing the service / leaking native handles
- Assuming take() returns events directly rather than a WatchKey