運用Map merge來加總多種統計資料
前陣子在Java的推特看到一篇文章 Map merge and compute, hidden API diamonds,討論Java API的Map功能中一組比較常被忽略的工具compute與merge,而這陣子因為備課的關係常撈取各國COVID-19病毒相關的統計資料,剛好也用到merge這支程式,因此就順勢來做一下進階應用案例探討。
Map是一種由key-value pairs組成的資料結構,傳統上要創建一個這樣的結構大致上是這樣:
在Java 9時,Map的功能被增強了不少,因此可以使用像下面的語法產生上面的資料:
不過有點不同的是,Map.of創建的資料是immutable的,因此內容無法再做任何變動。而在撈取一串統計數據時,資料可能長得像這樣:
以上代表同一個國家"US"的不同省份(province)"A"、"B",後面則是每日的統計資料(以這裡為例是COVID-19每日的確診人數)。撈取之後可能會把它存在像下面這樣的結構中:
假定還有另一組每日死亡人數的資料,結構很類似,像這樣:
這裡的List.of()同樣也是Java 9對List所做的增強。
假定我們已將上述的國家/省份/確診及死亡等數據匯整成一個資料集合,接下來想把這些數字加總成各國的全國性數據,存在以國家為key的Map結構中,其對應的value為另一個Map,以"confirmed"(確診)與"deaths"(死亡)為key、分別對應以每日統計人數的List為value的資料結構:
例如:
因為這裡只是要得到全國加總,因此已略去省份名稱,也就是過程中同一國家可能對應多筆不同省份的資料。對於原本空的變數country_updated來說,遇到第一筆資料時是新增進去,而第二筆以後就要加總進去,因此這裡使用merge較為合適。
merge的簽章有些複雜,如下:
其特性就是當這裡的key, value關係尚不存在時,就將它們直接存進去;若存在的話則將舊值取出,連同待加進去的value一起導入remappingFunction,來算出一個新的值以存進去,並且覆蓋舊值。這裡的remappingFunction是一個BiFunction,也就是須傳入兩個參數的函數,而傳入的順序為取出的舊值在先、待加入的值在後。
為了簡化程式碼,我們還需要一段小程式,將兩個Map中同key值的List取出,並將所有的元素相加,包成一道Map Entry回傳:
假設我們將預備加入國名為country的省份資料存在變數province中:
(留意其結構與目的地相同,即country_updated外層Map的值結構)
那麼將這個省份的資料加進country_updated中的程式碼就可以寫成:
若country尚未存在country_update中,則會直接將province的內容存進去;若存在的話則會以(o, n)導入BiFunction;而回傳值則是透過sum將兩個導入值的內容一一相加所得到的Map。
以上這個關於Map merge的進階應用案例,不知道是否能幫助你更加了解它的妙用呢?
*使用hilite.me做為code beautifier
Map是一種由key-value pairs組成的資料結構,傳統上要創建一個這樣的結構大致上是這樣:
Map<String, Integer> myMap = new HashMap<>(); myMap.put("John", 32); myMap.put("Mary", 40);
在Java 9時,Map的功能被增強了不少,因此可以使用像下面的語法產生上面的資料:
Map<String, Integer> myMap = Map.of("John", 32, "Mary", 40);
不過有點不同的是,Map.of創建的資料是immutable的,因此內容無法再做任何變動。而在撈取一串統計數據時,資料可能長得像這樣:
"A","US",1,1,1,4,4,5,7,7,7,11,16,21,22,22,22,24, ... "B","US",0,0,0,0,2,10,12,23,33,38,42,51,55,59,64,70, ... ...
以上代表同一個國家"US"的不同省份(province)"A"、"B",後面則是每日的統計資料(以這裡為例是COVID-19每日的確診人數)。撈取之後可能會把它存在像下面這樣的結構中:
Map<String, List<Integer>> confirmed = Map.of( "A", List.of(1, 1, 1, 4, 4, 5, 7, 7, 7, 11, 16, 21, 22, 22, 22, 24, ...), "B", List.of(0, 0, 0, 0, 2, 10, 12, 23, 33, 38, 42, 51, 55, 59, 64, 70, ...));
假定還有另一組每日死亡人數的資料,結構很類似,像這樣:
Map<String, List<Integer>> deaths = Map.of( "A", List.of(0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, ...), "B", List.of(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...));
假定我們已將上述的國家/省份/確診及死亡等數據匯整成一個資料集合,接下來想把這些數字加總成各國的全國性數據,存在以國家為key的Map結構中,其對應的value為另一個Map,以"confirmed"(確診)與"deaths"(死亡)為key、分別對應以每日統計人數的List為value的資料結構:
Map<String, Map<String, List<Integer>>> country_updated
例如:
"US":{"confirmed":[1, 1, 1, 4, 6, 15, 19, 30, ...], "deaths":[0, 0, 0, 0, 0, 0, 1, 1, ...]}
因為這裡只是要得到全國加總,因此已略去省份名稱,也就是過程中同一國家可能對應多筆不同省份的資料。對於原本空的變數country_updated來說,遇到第一筆資料時是新增進去,而第二筆以後就要加總進去,因此這裡使用merge較為合適。
merge的簽章有些複雜,如下:
default V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)
其特性就是當這裡的key, value關係尚不存在時,就將它們直接存進去;若存在的話則將舊值取出,連同待加進去的value一起導入remappingFunction,來算出一個新的值以存進去,並且覆蓋舊值。這裡的remappingFunction是一個BiFunction,也就是須傳入兩個參數的函數,而傳入的順序為取出的舊值在先、待加入的值在後。
為了簡化程式碼,我們還需要一段小程式,將兩個Map中同key值的List取出,並將所有的元素相加,包成一道Map Entry回傳:
<K> Entry<K, List<Integer>> sum(K key, Map<K, List<Integer>> m1, Map<K, List<Integer>> m2) { return Map.entry(key, IntStream.range(0, m1.get(key).size()) .map(i -> m1.get(key).get(i) + m2.get(key).get(i)) .boxed() .collect(Collectors.toList())); }
假設我們將預備加入國名為country的省份資料存在變數province中:
Map<String, List<Integer>> province = Map.of("confirmed", confirmed, "deaths", deaths);
那麼將這個省份的資料加進country_updated中的程式碼就可以寫成:
country_updated.merge(country, province, (o, n)->Map.ofEntries( sum("confirmed", o, n), sum("deaths", o, n)));
若country尚未存在country_update中,則會直接將province的內容存進去;若存在的話則會以(o, n)導入BiFunction;而回傳值則是透過sum將兩個導入值的內容一一相加所得到的Map。
以上這個關於Map merge的進階應用案例,不知道是否能幫助你更加了解它的妙用呢?
*使用hilite.me做為code beautifier
留言