https://github.com/apache/kafka/pull/17313#discussion_r1781495806
背景故事
今天延續上一篇的文章,不過來討論一個離kafka
比較遠的題目,但卻是在Kafka
不斷被提到的東西,那就是Scala
與Java
之間的各種轉換。為什麼會有這個議題?有讀過前面文章的朋友一定馬上能猜到原因,沒錯,那就是Kafka
目前只剩下伺服器核心元件還是Scala
,其他都已經是Java
了。因此在許多邏輯操作上就必須面對資料結構兩邊的互相轉換,當然可能有人會說為什麼不統一都用Java
資料結構?這是歷史的關係Scala
是先出現而後來才開始轉而用Java
,所以要一次全部改用Java
的資料結構會需要很長很長的一段時間。因此在Kafka
內部採用了大量Java
結構轉換成Scala
的寫法,而這也帶出今天的主題:Java
資料結構直接轉成Scala
之後的語義是否一致?而這次要討論的主角就是Scala
的getOrElseUpdate(key: K, defaultValue: => V)
getOrElseUpdate(key: K, defaultValue: => V)
說文解字就是當key
存在時則回傳對應的值,反之則會執行defaultValue
取得新的值後放入資料結構裡面後回傳。這聽起來是一個很常見的故事,甚至大家會直接聯想到Java
版本的computeIfAbsent
,對吧?我們來看一下Scala
如何包裝Java
的ConcurrentHashMap
override def getOrElseUpdate(key: K, op: => V): V =
underlying.computeIfAbsent(key, _ => op) match {
case null => super/*[concurrent.Map]*/.getOrElseUpdate(key, op)
case v => v
}
是不是有一種「阿果然如此」,就是直接呼叫computeIfAbsent
來完成getOrElseUpdate
的需求 ... 欸等等,為什麼null
的時候還要另外呼叫一次super.getOrElseUpdate
? 原因很簡單,那就是computeIfAbsent
和getOrElseUpdate
兩者對於null
的態度不一樣。computeIfAbsent
當計算結果爲null
的時候不會放進資料結構裡面,類似下方的邏輯:
if (map. get(key) == null) {
V newValue = mappingFunction. apply(key);
if (newValue != null) map. put(key, newValue);
}
然而getOrElseUpdate
在語義上則是計算出來的值一定會放到資料結構,就算該值爲null
,當然如果實作上是不允許null
的一樣會噴出錯誤。而這兩者在語義上的差異自然就導致Scala
在包裝Java
時需要額外處理null
的問題,畢竟對於Scala
而言包裝後的物件一定要符合Scala
的語義,所以當computeIfAbsent
最後結果爲null
時,Scala
就必須額外想辦法把該值放到資料結構裡面。看到這裏大家可能會有一個疑問,那就直接再呼叫put
不就好了?但很可惜在併發的世界裡這樣會失去原子性,這也是爲何Scala
是選擇呼叫getOrElseUpdate
來嘗試把null
放到資料結構裡面。
等等,如果又呼叫一次getOrElseUpdate
,那麼用來產生資料的方法不就可能會被呼叫很多次?沒錯,這就是一個需要注意的地方,ConcurrentHashMap
在實作上是有保證用來產生資料的方法一定只會呼叫一次,但如果你今天為了要轉換成Scala
而重新包裝,那麼在上述null
的情況下用來產生資料的方法就可能會被反覆呼叫很多次!
解決辦法
如果你今天很確定使用情境不會有產生null
的狀況,自然就不用擔心這個問題。反之,則必須要注意產生資料的方法本身不能具有副作用,例如改變某些狀態之類的。又或者乾脆一點,別再用Scala
的包裝了,就直接大方的使用Java
的界面吧!
廣告
歡迎訂閱全臺最鬆散的開源社群源來適你,上面不定期會有各種開源的廢文。也歡迎參加全臺最鬆散的開源討論頻道,上面有一群網友一起在刷開源技術