CoderMusgrove
CoderMusgrove

Reputation: 614

How to use a variable in a lambda, when the variable was assigned as a result of calling the method the lambda was supplied to?

I have a method TaskManager.newRepeatingTask(Runnable r, long delay, long interval) and it returns a UUID. I assign a UUID variable to what that method returned, and I want to use that variable inside of the Runnable. How would I accomplish this effectively, or alternatively, to what I'm trying to accomplish here?

UUID id = TaskManager.newRepeatingTask(() -> {
    for (int i = 0; i < profileButtons.length; i++) {
        GuiButton button = profileButtons[i];
        button.move(-1, 0);
    }
    toMove--;
    if (toMove == 0) {
        // id: "The variable may not have been initialized"
        TaskManager.cancelTask(id);
    }
}, 30L, 0L);

Upvotes: 3

Views: 312

Answers (4)

user2864740
user2864740

Reputation: 61885

My first solution, if I had control over TaskManager, would be to change it such that it also passed a UUID parameter to the callback or had another method of control - then using the result of the method would be moot.

However, if I did not then..


(Edit: I've been informed that the correct way to handle this in Java 8 is with a CompletableFuture - see Stuarts's answer.)

Another approach is to use a "mutable reference wrapper", like Holder<T> (or T[] or make your own), to emulate mutable bindings. Then,

Holder<UUID> idRef = new Holder<UUID>(); // Effectively final

idRef.value = TaskManager.newRepeatingTask(() -> {
    for (int i = 0; i < profileButtons.length; i++) {
        GuiButton button = profileButtons[i];
        button.move(-1, 0);
    }
    toMove--;
    if (toMove == 0) {
        UUID id = idRef.value;
        TaskManager.cancelTask(id);
    }
}, 30L, 0L);

Like the Runnable-with-UUID approach, this also suffers from a potential race condition between the assignment of the ID and the potential usage inside the lambda/Runnable if the task is run on a different thread. (If run later on the same thread then synchronization issues need not apply; and if run immediately on the same thread then the UUID is never observable inside the lambda.)

Applying a shared-synchronization both outside/wrapping the method call itself (and inside around the applicable code) should take care of that unless the Runnable is called immediately. Synchronization, or equivalent, should be done anyway for guaranteed visibility reasons (even if there is no "race condition") if such an approach is taken and the task may be executed on a different thread.

Holder<UUID> idRef = new Holder<UUID>();

synchronized(idRef) {
  idRef.value = TaskManager.newRepeatingTask(() -> {
      for (int i = 0; i < profileButtons.length; i++) {
          GuiButton button = profileButtons[i];
          button.move(-1, 0);
      }
      toMove--;
      if (toMove == 0) {
          // id: "The variable may not have been initialized"
          synchronized(idRef) {
            UUID id = idRef.value;
            TaskManager.cancelTask(id);
          }
      }
  }, 30L, 0L);
}

Upvotes: 2

Stuart Marks
Stuart Marks

Reputation: 132390

Use a java.util.concurrent.CompletableFuture (new in Java 8) to transmit a value between threads or tasks when you're not sure which one will arrive first. Here's how:

CompletableFuture<UUID> id = new CompletableFuture<>();
id.complete(TaskManager.newRepeatingTask(() -> {
    for (int i = 0; i < profileButtons.length; i++) {
        GuiButton button = profileButtons[i];
        button.move(-1, 0);
    }
    toMove--;
    if (toMove == 0) {
        TaskManager.cancelTask(id.join());
    }
}, 30L, 0L));

The join() method will collect and return the value supplied by the complete() method. If complete() hasn't been called yet, join() will block until it is. CompletableFuture handles all synchronization and memory visibility issues internally.

As others have noted, this is a bit contrived. A more conventional approach for a repeating task to cancel itself is to have it return a boolean indicating whether it should be rescheduled or canceled. To do this, change TaskManager.newRepeatingTask() to take a Supplier<Boolean> instead of a Runnable.

Upvotes: 5

AlexanderBrevig
AlexanderBrevig

Reputation: 1987

You could have your TaskManager.newRepeatingTask accept a Consumer<UUID>. Then create a runnable from that, using your then known UUID.

So that you internally go something like this:

//inside newRepeatingTask(consumer:Consumer<UUID> ...)
Runnable r = new Runnable() {
    public UUID uuid;
    @Override
    public void run() {
        consumer.accept(uuid); //calls the lambda
    }
};
r.uuid = getNextUUID(); //do whatever here
//add to your presumed list of runnables

Now you could just do:

UUID id = TaskManager.newRepeatingTask((UUID id) -> {
    TaskManager.cancelTask(id);
    //probably do something better with id
}, 30L, 0L);
//LOOK MA, this code is DRY

Upvotes: 4

Joffrey
Joffrey

Reputation: 37720

I think you'll have to complicate a bit here. The first thing that comes to my mind is the following:

  • create a Runnable (anonym) subclass with a settable (e.g. public) field uuid
  • call newRepeatingTask with your runnable object and get the UUID
  • use the setter to set the UUID on the Runnable

That would be:

Runnable r = new Runnable() {
    public UUID uuid;

    @Override
    public void run() {
        for (int i = 0; i < profileButtons.length; i++) {
            GuiButton button = profileButtons[i];
            button.move(-1, 0);
        }
        toMove--;
        if (toMove == 0) {
            // id: "The variable may not have been initialized"
            TaskManager.cancelTask(uuid);
        }
    }
}
UUID id = TaskManager.newRepeatingTask(r, 30L, 0L);
r.uuid = id;

Sorry but I think you're gonna have to drop the lambda :'(

Important note: as noted by @Dici, if the runnable is run within newRepeatingTask, some synchronization problems might happen. You might consider the option suggested by AlexanderBrevig, which would allow you to set the id before calling run() on the runnable.

Upvotes: 3

Related Questions