2

I am searching for a modern way to execute a given method at a given date/time (ZonedDateTime in particular).

I am aware of the Timer class and the Quartz library, as shown here (the threads include full solutions):

But those threads are rather old and do not utilize the new Java features and library elements since then. In particular, it would be very handy to get hands on any kind of Future object, since they provide a simple mechanism to cancel them.

So please do not suggest solutions involving Timer or Quartz. Also, I'd like to have a vanilla solution, not using any external libraries. But feel free to also suggest those for the sake of Q&A.

Youcef LAIDANI
  • 55,661
  • 15
  • 90
  • 140
Zabuzard
  • 25,064
  • 8
  • 58
  • 82

1 Answers1

4

ScheduledExecutorService

You can use the ScheduledExecutorService (documentation) class, which is available since Java 5. It will yield a ScheduledFuture (documentation) which can be used to monitor the execution and also cancel it.

In particular, the method:

ScheduledFuture<?> schedule​(Runnable command, long delay, TimeUnit unit)

which

Submits a one-shot task that becomes enabled after the given delay.

But you can also look into the other methods, depending on the actual use case (scheduleAtFixedRate and versions accepting Callable instead of Runnable).

Since Java 8 (Streams, Lambdas, ...) this class becomes even more handy, due to the availability of easy conversion methods between the old TimeUnit and the newer ChronoUnit (for your ZonedDateTime), as well as the ability to provide the Runnable command as lambda or method reference (because it is a FunctionalInterface).


Example

Let's have a look at an example doing what you ask for:

// Somewhere before the method, as field for example
// Use other pool sizes if desired
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

public static ScheduledFuture<?> scheduleFor(Runnable runnable, ZonedDateTime when) {
    Instant now = Instant.now();
    // Use a different resolution if desired
    long secondsUntil = ChronoUnit.SECONDS.between(now, when.toInstant());

    return scheduler.schedule(runnable, secondsUntil, TimeUnit.of(ChronoUnit.SECONDS));
}

A call is simple:

ZonedDateTime when = ...
ScheduledFuture<?> job = scheduleFor(YourClass::yourMethod, when);

You can then use the job to monitor the execution and also cancel it, if desired. Example:

if (!job.isCancelled()) {
    job.cancel(false);
}

Notes

You can exchange the ZonedDateTime parameter in the method for Temporal, then it also accepts other date/time formats.

Do not forget to shutdown the ScheduledExecutorService when you are done. Else you will have a thread running, even if your main program has finished already.

scheduler.shutdown();

Note that we use Instant instead of ZonedDateTime, since zone information is irrelevant to us, as long as the time difference is computed correctly. Instant always represents the time in UTC, without any weird phenomena like DST. (Although it does not really matter for this application, it is just cleaner).

Zabuzard
  • 25,064
  • 8
  • 58
  • 82
  • 1
    Good Answer. For clarity, I suggest that inside your `scheduleFor` method you convert to using `Instant` rather than `ZonedDateTime` by calling `toInstant`. While it works here in this context, showing `ZonedDateTime.now()` without an explicit time zone sets a bad example to show people learning about date-time handling. Being unaware of the time zone is a common source of trouble in programming. The `Instant` is always in UTC, so its use removes that ambiguity. – Basil Bourque Mar 06 '19 at 16:55
  • @BasilBourque Thanks for the comment, I have a question though. I thought `ChronoUnit#between` is aware of the timezone set in `ZonedDateTime` and will thus compare it correctly, no matter which zone is set in `now()` (which is systems default zone). I.e. it would not matter, the outcome of `between` would always be correct. Is this the case, or am I wrong here? – Zabuzard Mar 06 '19 at 19:52
  • 1
    I believe **your code *is* correct**, with proper results. As I mentioned in my comment, within the context of your code, your use of `ZonedDateTime` would work successfully. My concern is for other people learning date-time handling, seeing `ZonedDateTime.now()`, and then going on to call that method in their own work, *sans* optional zone argument, without understanding the implications of the JVM’s current default time zone being implicitly applied at runtime. Also, using `Instant` in your method makes clear that we are counting seconds without any specific relevance to time zone issues. – Basil Bourque Mar 06 '19 at 20:53