如何在lambda表达式中使用一个变量,当这个变量是调用lambda表达式的方法的结果时?

3

我有一个方法TaskManager.newRepeatingTask(Runnable r, long delay, long interval),它返回一个UUID。我将一个UUID变量分配给该方法返回的内容,并希望在Runnable内部使用该变量。如何有效地实现这一点,或者说,我想要达到的目的是什么?

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);

TaskManager 属于哪个包? - PM 77-1
TaskManager 在我的工具类中,我完全掌控它。我不知道这些信息是为了什么而需要的。问题现在已经正式解决。 - CoderMusgrove
4个回答

5

使用Java 8中新增的java.util.concurrent.CompletableFuture来在线程或任务之间传递值,当你不确定哪个先到达时。以下是方法:

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));
join()方法将收集并返回complete()方法提供的值。如果complete()尚未被调用,join()将阻塞直到其被调用。 CompletableFuture会在内部处理所有同步和内存可见性问题。
正如其他人所指出的那样,这有点牵强。一个更常规的方法是让重复任务返回一个布尔值,指示它是否应该被重新安排或取消。为此,请将TaskManager.newRepeatingTask()更改为使用Supplier<Boolean>而不是Runnable

4
你可以让 TaskManager.newRepeatingTask 接受一个 Consumer<UUID>。然后,从中创建一个可运行的任务,并使用你已知的 UUID
因此,在内部,你应该这样做:
//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

现在你可以直接这样做:
UUID id = TaskManager.newRepeatingTask((UUID id) -> {
    TaskManager.cancelTask(id);
    //probably do something better with id
}, 30L, 0L);
//LOOK MA, this code is DRY

3
newRepeatingTask方法的声明中,将第一个参数声明为Consumer<UUID>而不是Runnable。然后,您就可以提供一个以UUID为参数的lambda表达式,如此处第二个代码片段所示。 - Joffrey
3
关于lambda表达式:简而言之,(UUID id) -> {...} 是一个实现了 Consumer<UUID> 接口的lambda表达式,而 () -> {...} 则是一个实现了 Runnable 接口的lambda表达式。这就是为什么你可以在调用期望一个 Consumer<UUID> 参数或一个 Runnable 参数的方法时使用这些lambda表达式。 - Joffrey
2
我是一个C#程序员,所以直到现在我才知道Java也有lambda表达式。我发现随着使用和理解,使用lambda表达式学习它们比阅读一些教程更好。这只是一种使您的功能成为程序中数据的方法。也就是说,您可以像传递int、string等一样传递功能。这有助于实现函数式编程风格。如果您想了解更多关于lambda表达式的信息,这篇文章还不错:http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html - 祝你好运! - AlexanderBrevig
2
@AlexanderBrevig Java中的lambda表达式是相当新的:它们随着2014年3月发布的Java 8一起推出。这可能就是你不知道它的原因^^ - Joffrey
1
啊,我明白了!我在公司使用第七版的Java开发了一些内部工具。我知道Lambda表达式即将推出,但从未意识到它们已经可用了。我<3 λ。 - AlexanderBrevig
显示剩余5条评论

3

我认为你需要稍微复杂一些。我首先想到的是以下内容:

  • 创建一个可设置(例如public)字段uuidRunnable(匿名)子类
  • 使用你的runnable对象调用newRepeatingTask并获取UUID
  • 使用setter在Runnable上设置UUID

这将是:

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;

非常抱歉,我认为你需要放弃lambda :'(

重要提示:如@Dici所示,如果可运行对象在newRepeatingTask中运行,则可能会出现一些同步问题。您可以考虑AlexanderBrevig提出的选项,在调用可运行对象的run()之前设置id。


或者让newRepeatingTask仅接受新子类,并自动设置UUID。 - AlexanderBrevig
是的,那也是一个解决方案,但可能有点过于限制了。 - Joffrey
1
@Joffrey:如果在r.uuid = id;之前调用了TaskManager.cancelTask(id);,那怎么办?难道不需要一些同步吗? - Dici
确实需要,除非在“newRepeatingTask”中没有调用“run()”。 - Joffrey
@CoderMusgrove:好的! - Dici

2

如果我能控制TaskManager,我的第一个解决方案是更改它,使其在回调时也传递UUID参数或具有另一种控制方法 - 然后使用方法的结果将是无意义的。

然而,如果我没有这样的控制权,则可以采用另一种方法。


(编辑:我被告知在Java 8中处理此问题的正确方法是使用CompletableFuture - 请参见Stuarts的答案。)

另一种方法是使用“可变引用包装器”,例如Holder<T>(或T[]或自己创建),以模拟可变绑定。然后,

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);

与使用带有UUID的Runnable方法相似,这种方法也存在潜在的竞态条件问题,即如果任务在不同的线程上运行,则ID的分配和潜在的lambda/Runnable内部使用之间可能存在竞争条件。(如果稍后在同一线程上运行,则同步问题不适用;如果立即在同一线程上运行,则lambda中永远不会观察到UUID。)
将共享同步应用于方法调用本身的外部/包装(以及适用代码周围的内部)应该可以解决这个问题,除非Runnable被立即调用。无论是否存在“竞态条件”,为了保证可见性,都应该进行同步或等效操作,如果采取这种方法并且任务可能在不同的线程上执行。
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);
}

1
newRepeatingTask()的代码不受控制时,对于同步操作加1。 - Joffrey
1
在Java 8中,“Holder”被拼写为“CompletableFuture”,并处理同步和内存可见性。关闭,+1。 - Stuart Marks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接