在 Java 中实现去抖动

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4742210/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-30 07:49:40  来源:igfitidea点击:

implementing debounce in Java

javaalgorithm

提问by levinalex

For some code I'm writing I could use a nice general implementation of debouncein Java.

对于我正在编写的一些代码,我可以debounce在 Java 中使用一个很好的通用实现。

public interface Callback {
  public void call(Object arg);
}

class Debouncer implements Callback {
    public Debouncer(Callback c, int interval) { ... }

    public void call(Object arg) { 
        // should forward calls with the same arguments to the callback c
        // but batch multiple calls inside `interval` to a single one
    }
}

When call()is called multiple times in intervalmilliseconds with the same argument the callback function should be called exactly once.

当使用相同的参数call()interval毫秒为单位多次调用时,回调函数应该只调用一次。

A visualization:

一个可视化:

Debouncer#call  xxx   x xxxxxxx        xxxxxxxxxxxxxxx
Callback#call      x           x                      x  (interval is 2)
  • Does (something like) this exist already in some Java standard library?
  • How would you implement that?
  • 某些Java标准库中是否已经存在(类似的东西)?
  • 你将如何实施?

采纳答案by Eyal Schneider

Please consider the following thread safe solution. Note that the lock granularity is on the key level, so that only calls on the same key block each other. It also handles the case of an expiration on key K which occurs while call(K) is called.

请考虑以下线程安全解决方案。注意锁的粒度是在key级别的,这样只能在同一个key块上互相调用。它还处理在调用 call(K) 时发生的密钥 K 到期的情况。

public class Debouncer <T> {
  private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1);
  private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<T, TimerTask>();
  private final Callback<T> callback;
  private final int interval;

  public Debouncer(Callback<T> c, int interval) { 
    this.callback = c;
    this.interval = interval;
  }

  public void call(T key) {
    TimerTask task = new TimerTask(key);

    TimerTask prev;
    do {
      prev = delayedMap.putIfAbsent(key, task);
      if (prev == null)
        sched.schedule(task, interval, TimeUnit.MILLISECONDS);
    } while (prev != null && !prev.extend()); // Exit only if new task was added to map, or existing task was extended successfully
  }

  public void terminate() {
    sched.shutdownNow();
  }

  // The task that wakes up when the wait time elapses
  private class TimerTask implements Runnable {
    private final T key;
    private long dueTime;    
    private final Object lock = new Object();

    public TimerTask(T key) {        
      this.key = key;
      extend();
    }

    public boolean extend() {
      synchronized (lock) {
        if (dueTime < 0) // Task has been shutdown
          return false;
        dueTime = System.currentTimeMillis() + interval;
        return true;
      }
    }

    public void run() {
      synchronized (lock) {
        long remaining = dueTime - System.currentTimeMillis();
        if (remaining > 0) { // Re-schedule task
          sched.schedule(this, remaining, TimeUnit.MILLISECONDS);
        } else { // Mark as terminated and invoke callback
          dueTime = -1;
          try {
            callback.call(key);
          } finally {
            delayedMap.remove(key);
          }
        }
      }
    }  
  }

回答by simon04

Here's my implementation:

这是我的实现:

public class Debouncer {
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private final ConcurrentHashMap<Object, Future<?>> delayedMap = new ConcurrentHashMap<>();

    /**
     * Debounces {@code callable} by {@code delay}, i.e., schedules it to be executed after {@code delay},
     * or cancels its execution if the method is called with the same key within the {@code delay} again.
     */
    public void debounce(final Object key, final Runnable runnable, long delay, TimeUnit unit) {
        final Future<?> prev = delayedMap.put(key, scheduler.schedule(new Runnable() {
            @Override
            public void run() {
                try {
                    runnable.run();
                } finally {
                    delayedMap.remove(key);
                }
            }
        }, delay, unit));
        if (prev != null) {
            prev.cancel(true);
        }
    }

    public void shutdown() {
        scheduler.shutdownNow();
    }
}

Example usage:

用法示例:

final Debouncer debouncer = new Debouncer();
debouncer.debounce(Void.class, new Runnable() {
    @Override public void run() {
        // ...
    }
}, 300, TimeUnit.MILLISECONDS);

回答by biziclop

I don't know if it exists but it should be simple to implement.

我不知道它是否存在,但它应该很容易实现。

class Debouncer implements Callback {

  private CallBack c;
  private volatile long lastCalled;
  private int interval;

  public Debouncer(Callback c, int interval) {
     //init fields
  }

  public void call(Object arg) { 
      if( lastCalled + interval < System.currentTimeMillis() ) {
        lastCalled = System.currentTimeMillis();
        c.call( arg );
      } 
  }
}

Of course this example oversimplifies it a bit, but this is more or less all you need. If you want to keep separate timeouts for different arguments, you'll need a Map<Object,long>instead of just a longto keep track of the last execution time.

当然,这个例子有点过于简化了,但这或多或少是你所需要的。如果您想为不同的参数保留单独的超时,则需要 aMap<Object,long>而不仅仅是 along来跟踪上次执行时间。

回答by levinalex

This looks like it could work:

这看起来可以工作:

class Debouncer implements Callback {
    private Callback callback;
    private Map<Integer, Timer> scheduled = new HashMap<Integer, Timer>();
    private int delay;

    public Debouncer(Callback c, int delay) {
        this.callback = c;
        this.delay = delay;
    }

    public void call(final Object arg) {
        final int h = arg.hashCode();
        Timer task = scheduled.remove(h);
        if (task != null) { task.cancel(); }

        task = new Timer();
        scheduled.put(h, task);

        task.schedule(new TimerTask() {
            @Override
            public void run() {
                callback.call(arg);
                scheduled.remove(h);
            }
        }, this.delay);
    }
}

回答by Neromancer

The following implementation works on Handler based threads (e.g. the main UI thread, or in an IntentService). It expects only to be called from the thread on which it is created, and it will also run it's action on this thread.

以下实现适用于基于处理程序的线程(例如主 UI 线程,或在 IntentService 中)。它只希望从创建它的线程调用,并且它也会在该线程上运行它的操作。

public class Debouncer
{
    private CountDownTimer debounceTimer;
    private Runnable pendingRunnable;

    public Debouncer() {

    }

    public void debounce(Runnable runnable, long delayMs) {
        pendingRunnable = runnable;
        cancelTimer();
        startTimer(delayMs);
    }

    public void cancel() {
        cancelTimer();
        pendingRunnable = null;
    }

    private void startTimer(final long updateIntervalMs) {

        if (updateIntervalMs > 0) {

            // Debounce timer
            debounceTimer = new CountDownTimer(updateIntervalMs, updateIntervalMs) {

                @Override
                public void onTick(long millisUntilFinished) {
                    // Do nothing
                }

                @Override
                public void onFinish() {
                    execute();
                }
            };
            debounceTimer.start();
        }
        else {

            // Do immediately
            execute();
        }
    }

    private void cancelTimer() {
        if (debounceTimer != null) {
            debounceTimer.cancel();
            debounceTimer = null;
        }
    }

    private void execute() {
        if (pendingRunnable != null) {
            pendingRunnable.run();
            pendingRunnable = null;
        }
    }
}

回答by benbai123

My implementation, very easy to use, 2 util methods for debounce and throttle, pass your runnable into it to get the debounce/throttle runnable

我的实现,非常易于使用,2 个用于去抖动和节流的 util 方法,将您的可运行对象传递给它以获得可运行的去抖动/节流

package basic.thread.utils;

public class ThreadUtils {
    /** Make a runnable become debounce
     * 
     * usage: to reduce the real processing for some task
     * 
     * example: the stock price sometimes probably changes 1000 times in 1 second,
     *  but you just want redraw the candlestick of k-line chart after last change+"delay ms"
     * 
     * @param realRunner Runnable that has something real to do
     * @param delay milliseconds that realRunner should wait since last call
     * @return
     */
    public static Runnable debounce (Runnable realRunner, long delay) {
        Runnable debounceRunner = new Runnable() {
            // whether is waiting to run
            private boolean _isWaiting = false;
            // target time to run realRunner
            private long _timeToRun;
            // specified delay time to wait
            private long _delay = delay;
            // Runnable that has the real task to run
            private Runnable _realRunner = realRunner;
            @Override
            public void run() {
                // current time
                long now;
                synchronized (this) {
                    now = System.currentTimeMillis();
                    // update time to run each time
                    _timeToRun = now+_delay;
                    // another thread is waiting, skip
                    if (_isWaiting) return;
                    // set waiting status
                    _isWaiting = true;
                }
                try {
                    // wait until target time
                    while (now < _timeToRun) {
                        Thread.sleep(_timeToRun-now);
                        now = System.currentTimeMillis();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // clear waiting status before run
                    _isWaiting = false;
                    // do the real task
                    _realRunner.run();
                }
            }};
        return debounceRunner;
    }
    /** Make a runnable become throttle
     * 
     * usage: to smoothly reduce running times of some task
     * 
     * example: assume the price of a stock often updated 1000 times per second
     * but you want to redraw the candlestick of k-line at most once per 300ms
     * 
     * @param realRunner
     * @param delay
     * @return
     */
    public static Runnable throttle (Runnable realRunner, long delay) {
        Runnable throttleRunner = new Runnable() {
            // whether is waiting to run
            private boolean _isWaiting = false;
            // target time to run realRunner
            private long _timeToRun;
            // specified delay time to wait
            private long _delay = delay;
            // Runnable that has the real task to run
            private Runnable _realRunner = realRunner;
            @Override
            public void run() {
                // current time
                long now;
                synchronized (this) {
                    // another thread is waiting, skip
                    if (_isWaiting) return;
                    now = System.currentTimeMillis();
                    // update time to run
                    // do not update it each time since
                    // you do not want to postpone it unlimited
                    _timeToRun = now+_delay;
                    // set waiting status
                    _isWaiting = true;
                }
                try {
                    Thread.sleep(_timeToRun-now);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // clear waiting status before run
                    _isWaiting = false;
                    // do the real task
                    _realRunner.run();
                }
            }};
        return throttleRunner;
    }
}

回答by Naz_Jnr

Here is my working implementation:

这是我的工作实现:

Execution Callback:

执行回调:

public interface cbDebounce {

void execute();

}

Debouncer:

去抖器:

public class Debouncer {

private Timer timer;
private ConcurrentHashMap<String, TimerTask> delayedTaskMap;

public Debouncer() {
    this.timer = new Timer(true); //run as daemon
    this.delayedTaskMap = new ConcurrentHashMap<>();
}

public void debounce(final String key, final cbDebounce debounceCallback, final long delay) {
    if (key == null || key.isEmpty() || key.trim().length() < 1 || delay < 0) return;

    cancelPreviousTasks(); //if any

    TimerTask timerTask = new TimerTask() {
        @Override
        public void run() {
            debounceCallback.execute();
            cancelPreviousTasks();
            delayedTaskMap.clear();
            if (timer != null) timer.cancel();
        }
    };

    scheduleNewTask(key, timerTask, delay);
}

private void cancelPreviousTasks() {
    if (delayedTaskMap == null) return;

    if (!delayedTaskMap.isEmpty()) delayedTaskMap
            .forEachEntry(1000, entry -> entry.getValue().cancel());

    delayedTaskMap.clear();
}

private void scheduleNewTask(String key, TimerTask timerTask, long delay) {
    if (key == null || key.isEmpty() || key.trim().length() < 1 || timerTask == null || delay < 0) return;

    if (delayedTaskMap.containsKey(key)) return;

    timer.schedule(timerTask, delay);

    delayedTaskMap.put(key, timerTask);
}

}

}

Main (to test)

主要(测试)

public class Main {

private static Debouncer debouncer;

public static void main(String[] args) throws IOException, InterruptedException {
    debouncer = new Debouncer();
    search("H");
    search("HE");
    search("HEL");
    System.out.println("Waiting for user to finish typing");
    Thread.sleep(2000);
    search("HELL");
    search("HELLO");
}

private static void search(String searchPhrase) {
    System.out.println("Search for: " + searchPhrase);
    cbDebounce debounceCallback = () -> System.out.println("Now Executing search for: "+searchPhrase);
    debouncer.debounce(searchPhrase, debounceCallback, 4000); //wait 4 seconds after user's last keystroke
}

}

Output

输出

  • Search for: H
  • Search for: HE
  • Search for: HEL
  • Waiting for user to finish typing
  • Search for: HELL
  • Search for: HELLO
  • Now Executing search for: HELLO
  • 搜索: H
  • 搜索: HE
  • 搜索: HEL
  • 等待用户完成输入
  • 搜索: 地狱
  • 搜索: 你好
  • 正在执行搜索:HELLO

回答by Pei

I've updated @Eyal's answer to be able to configure debouncing time in each call, and use runnable code block instead of callback:

我更新了@Eyal 的答案,以便能够在每次调用中配置去抖动时间,并使用可运行代码块而不是回调:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Debouncer<T> {

    private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1);
    private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<T, TimerTask>();

    public Debouncer() {
    }

    public void call(T key, Runnable runnable, int interval, TimeUnit timeUnit) {
        TimerTask task = new TimerTask(key, runnable, interval, timeUnit);

        TimerTask prev;
        do {
            prev = delayedMap.putIfAbsent(key, task);
            if (prev == null)
                sched.schedule(task, interval, timeUnit);
        } while (prev != null && !prev.extend());
    }

    public void terminate() {
        sched.shutdownNow();
    }

    private class TimerTask implements Runnable {
        private final T key;
        private final Runnable runnable;
        private final int interval;
        private final TimeUnit timeUnit;
        private long dueTime;
        private final Object lock = new Object();

        public TimerTask(T key, Runnable runnable, int interval, TimeUnit timeUnit) {
            this.key = key;
            this.runnable = runnable;
            this.interval = interval;
            this.timeUnit = timeUnit;
            extend();
        }

        public boolean extend() {
            synchronized (lock) {
                if (dueTime < 0)
                    return false;
                dueTime = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(interval, timeUnit);
                return true;
            }
        }

        public void run() {
            synchronized (lock) {
                long remaining = dueTime - System.currentTimeMillis();
                if (remaining > 0) { // Re-schedule task
                    sched.schedule(this, remaining, TimeUnit.MILLISECONDS);
                } else { // Mark as terminated and invoke callback
                    dueTime = -1;
                    try {
                        runnable.run();
                    } finally {
                        delayedMap.remove(key);
                    }
                }
            }
        }
    }
}