java 中counter什么意思_方便适用的计数器Counter
為什么要使用計數器?
在游戲程序的開發中,我們會遇到很多跟計數相關的需求,比如玩家領取了多少次獎勵、成就的任務進度、一場比賽中的得分等等。然而在很多的API里,很少提供我們不用關心邊界值或中間操作的計數器,特別是對于服務器來講,基本會使用有鍵值對的計數器,因為我們管理的是一群玩家,并不是一個,當然可能還會有更多的層級,比如一群玩家的任務進度計數,就是一個3維數組。要實現這種看似簡單的功能,我們就會想到Java里Map這個東西,但是很遺憾的是,Map的處理太過多余了,他并不是為計數而生,我們需要改造一下,這里先從簡單計數器說起。
AtomicInteger
這個AtomicInteger好啊,又是線程安全的,又有方便的計數API,我們就從他出發吧。
計數器的API總共就幾個,獲取當前值(get),直接設置當前值(set),增加多少值(getAndAdd和addAndGet),其實就相當于操作符++,一個是a++,一個是++a。在增加值的基礎上再提供++1、- -1、1- -、1++這種操作,基本上就夠用了,對于AtomicInteger的原理這里就不贅述。
IntCounter
既然有線程安全的了,為啥要有一個非安全的,嘛。。畢竟線程安全的有那么點點以犧牲性能為代價。這里提一下游戲服務器里的線程模型,假設是一個以地圖為基礎的RPG游戲,通常來講,我們會以地圖來分線程,保證同一個地圖的玩家是在同一個線程上的(這就是為什么有些游戲交易必須同地圖,甚至更老的游戲,功能都綁定在了NPC身上,題外話不多說)。如果該功能對于玩家是單機性質(自己的操作不影響他人)的或者說即使交互(與其他玩家發生行為)也是同地圖玩家的交互,計數操作是沒必要線程安全的。RPG游戲是高反饋低延時的游戲,所以扣點性能也沒什么大驚小怪的(雖然現在硬件已經很變態了)。
IntCounter的實現其實非常簡單,就是對int的再次封裝(LongCounter同理),想必我這里不說,大家都明白怎么寫這個代碼了。舉個栗子,代碼沒什么好講的,大家可以實現更多方便的API,雖然這些API總共就沒幾行,但是積少成多,對于項目的開大有著很大的幫助,不要小看他了。
private int count;
/*** 歸零*/
public void zero() {
setCount(0);
}
/*** 設置為最大值*/
public void setHigh() {
setCount(high());
}
/*** 設置為最小值*/
public void setLow() {
setCount(low());
}
/*** 最大限制** @return*/
public int high() {
return Integer.MAX_VALUE;
}
/*** 最小限制** @return*/
public int low() {
return 0;
}
/*** 獲取當前數值** @return*/
public int getCount() {
return this.count;
}
/*** 直接設置當前值** @param value* @return*/
protected int setCount(int value) {
return this.count = GameMathUtil.fixedBetween(value, low(), high());
}
/*** +1并獲取** @return*/
public int incrementAndGet() {
return addAndGet(1);
}
/*** -1并獲取** @return*/
public int decrementAndGet() {
return addAndGet(-1);
}
/*** 增加并獲取** @param delta* @return*/
public int addAndGet(int delta) {
return setCount(getCount() + delta);
}
/*** 獲取并+1** @return*/
public int getAndIncrement() {
return getAndAdd(1);
}
/*** 獲取并-1** @return*/
public int getAndDecrement() {
return getAndAdd(-1);
}
/*** 獲取并增加** @param delta* @return*/
public int getAndAdd(int delta) {
int old = getCount();
setCount(GameMathUtil.safeAdd(old, delta));
return old;
}
鍵值對Map類型計數器的包裝
剛才說了,不管是對于服務器本身的性質也好還是對于需求本身也好,都會存在同類型復數個計數器,我們就自然想到了Map(2維映射)類型甚至Table(3維映射)來處理這個事情。由于原本自帶的Map并不是專門做這種事情的,所以我們針對計數器的需求特別優化一下API的友好度。
如果我們直接使用Map的話,我們必須要處理
1.不管在放入計數或者是獲取計數的時候是否存在一個鍵值對,如果不存在我們會初始化他
2.如果大部分的計數在常規狀態下都為初始值(這里假設為0),那么我們會初始化一堆沒有用的數據
3.每次計數改變的操作,都會先取出數據(取出的時候還要做第1步的檢查),然后更改數值再放回,這些代碼重復太多會讓寫代碼的人不能直接關注需求本身而產生混亂從而導致很多BUG。
下面的代碼簡單的演示下上面的痛處
//聲明一個MapMap tasks = new HashMap<>();
//現在獲取任務"kill monster"的進度String taskName = "kill monster";
Integer taskProccess = tasks.get(taskName);
//如果任務進度為空則初始化任務進度為0if(taskProccess == null){
taskProccess = 0;
tasks.put(taskName,taskProccess);
}
//任務進度+1taskProccess+=1;
//這里由于慣性思維,在寫很多復雜邏輯的時候很有可能會不做put操作而導致bugtasks.put(taskName,taskProccess);
上面的操作簡直讓人蛋疼無比!那么我們先看看經過優化過的IntMap是怎么寫代碼的吧!
IntMap tasksNew = IntMap.empty();
tasksNew.incrementAndGet(taskName);
兩行代碼解決,是不是輕松多了,不算上聲明,1行代碼解決,本來計數這種簡單操作就應該是一行代碼操作的事情,對吧。
針對上面的傷痛,我們看看是怎么實現一個自己的IntMap。計數器的API都是大同小異的
/*** 獲取計數** @param key* @return*/
int getCount(K key);
/*** 設置計數** @param key* @param newValue* @return*/
int putCount(K key, int newValue);
/*** 總數** @return*/
int sum();
/*** 加1并獲取** @param key* @return*/
int incrementAndGet(K key);
/*** 減1并獲取** @param key* @return*/
int decrementAndGet(K key);
/*** 增加并獲取** @param key* @param delta* @return*/
int addAndGet(K key, int delta);
/*** 獲取并加1** @param key* @return*/
int getAndIncrement(K key);
/*** 獲取并減1** @param key* @return*/
int getAndDecrement(K key);
/*** 獲取并增加** @param key* @param delta* @return*/
int getAndAdd(K key, int delta);
我們使用Java8中Map的新API(Map.compute)可以非常方便的實現剛才Map中冗余的操作
/*** 獲取并更新** @param key* @param updaterFunction* @return*/
private int getAndUpdate(K key, IntUnaryOperator updaterFunction) {
AtomicInteger holder = new AtomicInteger();
map.compute(key, (k, value) -> {
// 如果獲取key的value為空,則直接返回0 int oldValue = (value == null) ? 0 : value;
holder.set(oldValue);
return updaterFunction.applyAsInt(oldValue);
});
return holder.get();
}
獲取并更新實現了,更新并獲取就大同小異了,其余的API也只是對這個基礎方法進行包裝而已
鍵值對更特殊的優化-枚舉計數器EnumIntCounter
枚舉是個非常好的東西,讓代碼看起來非常簡潔,不混亂,有明確定義,主要是有一種限定作用,避免產生參數值的錯誤。我們來想象一個需求,統計星期1-7當中,哪個星期玩家殺怪的數量最多,這里我們就可以把星期1-7做成一個枚舉
public enum WEEK{
W_1,
W_2,
W_3,
W_4,
W_5,
W_6,
W_7,
}
這里不用星期的英文是因為我懶得去查了(屬于說得出來拼不出來,看見又認識的那種,哈哈,野生英語水平)。既然枚舉叫枚舉,那在代碼運行期間,他的數量肯定是一定的,所以我們在表示這種結構的時候不會像Map那樣復雜,單純的用一個int數組(int[])就行了,至于大小,剛才不是說了嗎,枚舉是固定的,所以我們就這樣聲明
private int[] counts;
public static > EnumIntCounter create(Class enumClass) {
return new EnumIntCounter<>(enumClass);
}
public EnumIntCounter(Class enumClass) {
counts = new int[EnumUtil.length(enumClass)];
}
這里獲取枚舉長度用的EnumUtil.length其實就是c.getEnumConstants().length,c是枚舉Class。
實現get和put對于數組來說就非常簡單了,只需要提供數據的index去做更改就行了。至于默認值為0,數據本身new出來就全部默認是0了。有時候還是需要通過枚舉的編號去獲取計數的,所以我們還得為get和put分別提供一個傳int編號過來查找計數的重載方法
/*** 獲取計數** @param e* @return*/
public int getCount(E e) {
return getCount(e.ordinal());
}
private int getCount(int ordinal) {
if (ordinal > counts.length - 1 || ordinal < 0) {
return 0;
}
return counts[ordinal];
}
/*** 放置計數** @param e* @param value* @return*/
public int putCount(E e, int value) {
return putCount(e.ordinal(), value);
}
private int putCount(int ordinal, int value) {
// 容錯 if (ordinal > counts.length - 1) {
int[] temp = new int[ordinal + 1];
System.arraycopy(counts, 0, temp, 0, counts.length);
counts = temp;
}
int old = counts[ordinal];
counts[ordinal] = value;
return old;
}
同樣的,實現了get和put,什么增加、減少、加一、減一我相信你自己就能搞定,就不贅述了??傊?#xff0c;這些東西雖然看起來非常簡單,感覺人人都能想到,但是真正跑過去優化的人不多,自己總是抱怨寫起煩躁,但是就是不動手搞一搞。我在項目上用到的這3種計數器讓我寫復雜邏輯的時候不再關心如何計數,身心健康,心曠神怡,再也不再想錘策劃人員一頓了~好處就是這么多。
如果你也實現完了,我們來看看怎么用吧,巨簡單!
EnumIntCounter tasksNewAgain = new EnumIntCounter<>(WEEK.class);
tasksNewAgain.incrementAndGet(WEEK.W_1);
哎呀!我去,舒服!
告辭!
總結
以上是生活随笔為你收集整理的java 中counter什么意思_方便适用的计数器Counter的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c语言水解猴子吃桃问题
- 下一篇: 全国气象预报业务产品grib2数据格式解