在 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
implementing debounce in Java
提问by levinalex
For some code I'm writing I could use a nice general implementation of debounce
in 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 interval
milliseconds 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 long
to 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);
}
}
}
}
}
}