摘要: 使用Java語言遞歸地將Map里的字段名由駝峰轉下划線。通過此例可以學習如何遞歸地解析任意嵌套的List-Map容器結構。
難度:初級
概述###
在進行多語言混合編程時,由於編程規范的不同, 有時會需要進行字段名的駝峰-下划線轉換。比如 php 語言中,變量偏向於使用下划線,而Java 語言中,變量偏向於駝峰式。當 PHP 調用 java 接口時,就需要將 java 返回數據結構中的駝峰式的字段名稱改為下划線。使用 jackson 解析 json 數據時,如果不指定解析的類,常常會將 json 數據轉化為 LinkedHashMap 。 因此,需要將 LinkedHashMap 里的字段名由駝峰轉為下划線。
這里的難點是, Map 結構可能是非常復雜的嵌套結構,一個 Map 的某個 key 對應的 value 可能是原子的,比如字符串,整數等,可能是嵌套的 Map, 也可能是含有多個 Map 的 List , 而 map , list 內部又可能嵌套任意的 map 或 list . 因此,要使用遞歸的算法來將 value 里的 key 遞歸地轉化為下划線。
算法設計###
首先,要編寫一個基本函數 camelToUnderline,將一個字符串的值從駝峰轉下划線。這個函數不難,逐字符處理,遇到大寫字母就轉成 _ + 小寫字母 ; 或者使用正則表達式替換亦可;
其次,需要使用基本函數 camelToUnderline 對可能多層嵌套的 Map 結構進行轉化。
假設有一個函數 transferKeysFromCamelToUnderline(amap) , 可以將 amap 里的所有 key 從駝峰轉化為下划線,包括 amap 里嵌套的任意 map。返回結果是 resultMap ;
(1) 首先考慮最簡單的情況:沒有任何嵌套的情況,原子類型的 key 對應原子類型的 value ; resultMap.put(newkey, value) 即可 , newkey = camelToUnderline(key);
(2) 其次考慮 Map 含有嵌套 subMap 的情況: 假設 <key, value> 中,value 是一個 subMap, 那么,調用 camelToUnderline(key) 可以得到新的 newkey ,調用 transferKeysFromCamelToUnderline(subMap) 就得到轉換了的 newValue , 得到 <newkey, newValue>; resultMap.put(newkey, newValue)
(3) 其次考慮 Map 含有 List 的情況: List 里通常含有多個 subMap , 只要遍歷里面的 subMap 進行轉換並添加到新的 List, 里面含有所有轉換了的 newValue = map(transferKeysFromCamelToUnderline, List[subMap]); resultMap.put(newkey, newValue) .
遞歸技巧####
當使用遞歸方式思考的時候,有三個技巧值得注意:
(1) 首先,一定從最簡單的情況開始思考。 這是基礎,也是遞歸終結條件;
(2) 其次,要善於從語義上去理解,而不是從細節上。 比如說 Map 含有嵌套 subMap 的時候, 就不要細想 subMap 里面是怎樣復雜的結構,是單純的一個子 map ,還是含有 List 的 Map 的 Map 的 Map; 這樣想會Feng掉滴_ 只需要知道 transferKeysFromCamelToUnderline(amap) 能夠對任意復雜結構的 amap 進行轉換得到所有 key 轉換了的 resultMap , 而在主流程中直接使用這個 subResultMap 即可。這個技巧值得多體會多訓練下才能掌握。
(3) 結果的存放。 既可以放在參數里,在遞歸調用的過程中逐步添加完善,也可以放在返回結果中。代碼實現中展示了兩種的用法。從某種意義來說,遞歸特別需要仔細地設計接口 transferKeysFromCamelToUnderline ,並從接口的語義上去理解和遞歸使用。
代碼實現###
/**
* Created by shuqin on 16/11/3.
* Improved by shuqin on 17/12/31.
*/
package zzz.study.datastructure.map;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
public class TransferUtil {
public static final char UNDERLINE='_';
public static String camelToUnderline(String origin){
return stringProcess(
origin, (prev, c) -> {
if (Character.isLowerCase(prev) && Character.isUpperCase(c)) {
return "" + UNDERLINE + Character.toLowerCase(c);
}
return ""+c;
}
);
}
public static String underlineToCamel(String origin) {
return stringProcess(
origin, (prev, c) -> {
if (prev == '_' && Character.isLowerCase(c)) {
return "" + Character.toUpperCase(c);
}
if (c == '_') {
return "";
}
return ""+c;
}
);
}
public static String stringProcess(String origin, BiFunction<Character, Character, String> convertFunc) {
if (origin==null||"".equals(origin.trim())){
return "";
}
String newOrigin = "0" + origin;
StringBuilder sb=new StringBuilder();
for (int i = 0; i < newOrigin.length()-1; i++) {
char prev = newOrigin.charAt(i);
char c=newOrigin.charAt(i+1);
sb.append(convertFunc.apply(prev,c));
}
return sb.toString();
}
public static void tranferKeyToUnderline(Map<String,Object> map,
Map<String,Object> resultMap,
Set<String> ignoreKeys) {
Set<Map.Entry<String,Object>> entries = map.entrySet();
for (Map.Entry<String, Object> entry: entries) {
String key = entry.getKey();
Object value = entry.getValue();
if (ignoreKeys.contains(key)) {
resultMap.put(key, value);
continue;
}
String newkey = camelToUnderline(key);
if ( (value instanceof List) ) {
List newList = buildValueList(
(List) value, ignoreKeys,
(m, keys) -> {
Map subResultMap = new HashMap();
tranferKeyToUnderline((Map) m, subResultMap, ignoreKeys);
return subResultMap;
});
resultMap.put(newkey, newList);
}
else if (value instanceof Map) {
Map<String, Object> subResultMap = new HashMap<String,Object>();
tranferKeyToUnderline((Map)value, subResultMap, ignoreKeys);
resultMap.put(newkey, subResultMap);
}
else {
resultMap.put(newkey, value);
}
}
}
public static Map<String,Object> tranferKeyToUnderline2(Map<String,Object> map,
Set<String> ignoreKeys) {
Set<Map.Entry<String,Object>> entries = map.entrySet();
Map<String,Object> resultMap = new HashMap<String,Object>();
for (Map.Entry<String, Object> entry: entries) {
String key = entry.getKey();
Object value = entry.getValue();
if (ignoreKeys.contains(key)) {
resultMap.put(key, value);
continue;
}
String newkey = camelToUnderline(key);
if ( (value instanceof List) ) {
List valList = buildValueList((List)value, ignoreKeys,
(m, keys) -> tranferKeyToUnderline2(m, keys));
resultMap.put(newkey, valList);
}
else if (value instanceof Map) {
Map<String, Object> subResultMap = tranferKeyToUnderline2((Map)value, ignoreKeys);
resultMap.put(newkey, subResultMap);
}
else {
resultMap.put(newkey, value);
}
}
return resultMap;
}
public static List buildValueList(List valList, Set<String> ignoreKeys,
BiFunction<Map, Set, Map> transferFunc) {
if (valList == null || valList.size() == 0) {
return valList;
}
Object first = valList.get(0);
if (!(first instanceof List) && !(first instanceof Map)) {
return valList;
}
List newList = new ArrayList();
for (Object val: valList) {
Map<String,Object> subResultMap = transferFunc.apply((Map) val, ignoreKeys);
newList.add(subResultMap);
}
return newList;
}
public static Map<String,Object> generalMapProcess(Map<String,Object> map,
Function<String, String> keyFunc,
Set<String> ignoreKeys) {
Map<String,Object> resultMap = new HashMap<String,Object>();
map.forEach(
(key, value) -> {
if (ignoreKeys.contains(key)) {
resultMap.put(key, value);
}
else {
String newkey = keyFunc.apply(key);
if ( (value instanceof List) ) {
resultMap.put(keyFunc.apply(key),
buildValueList((List) value, ignoreKeys,
(m, keys) -> generalMapProcess(m, keyFunc, ignoreKeys)));
}
else if (value instanceof Map) {
Map<String, Object> subResultMap = generalMapProcess((Map) value, keyFunc, ignoreKeys);
resultMap.put(newkey, subResultMap);
}
else {
resultMap.put(keyFunc.apply(key), value);
}
}
}
);
return resultMap;
}
}
補位技巧####
無論是下划線轉駝峰,還是駝峰轉下划線,需要將兩個字符作為一個組塊進行處理,根據兩個字符的情況來判斷和轉化成特定字符。比如下划線轉駝峰,就是 _x => 到 X, 駝峰轉下划線,就是 xY => x_y。 采用了前面補零的技巧,是的第一個字符與其他字符都可以用相同的算法來處理(如果不補零的話,第一個字符就必須單獨處理)。
字符轉換函數####
為了達到通用性,這里也使用了函數接口 BiFunction<Character, Character, String> convertFunc ,將指定的兩個字符轉換成指定的字符串。流程仍然是相同的:采用逐字符處理。
一個小BUG####
細心的讀者會發現一個小BUG,就是當List里的元素不是Map時,比如 "buyList": ["Food","Dress","Daily"], 程序會拋異常:Cannot cast to java.util.Map。 怎么修復呢? 需要抽離出個函數,專門對 List[E] 的值做處理,這里 E 不是 Map 也不是List。這里不考慮 List 直接嵌套 List 的情況。
public static List buildValueList(List valList, Set<String> ignoreKeys,
BiFunction<Map, Set, Map> transferFunc) {
if (valList == null || valList.size() == 0) {
return valList;
}
Object first = valList.get(0);
if (!(first instanceof List) && !(first instanceof Map)) {
return valList;
}
List newList = new ArrayList();
for (Object val: valList) {
Map<String,Object> subResultMap = transferFunc.apply((Map) val, ignoreKeys);
newList.add(subResultMap);
}
return newList;
}
由於 buildValueList 需要回調tranferKeyToUnderlineX 來生成轉換后的Map,這里使用了BiFunction<Map, Set, Map> transferFunc。相應的, tranferKeyToUnderline 和 tranferKeyToUnderline2 的列表處理要改成:
tranferKeyToUnderline:
if ( (value instanceof List) ) {
List newList = buildValueList(
(List) value, ignoreKeys,
(m, keys) -> {
Map subResultMap = new HashMap();
tranferKeyToUnderline((Map) m, subResultMap, ignoreKeys);
return subResultMap;
});
resultMap.put(newkey, newList);
}
tranferKeyToUnderline2:
if ( (value instanceof List) ) {
List valList = buildValueList((List)value, ignoreKeys,
(m, keys) -> tranferKeyToUnderline2(m, keys));
resultMap.put(newkey, valList);
}
通用化####
對於復雜Map結構的處理,寫一遍不容易,如果要做類似處理,是否可以復用上述處理流程呢? 上述主要的不同在於 key 的處理。只要傳入 key 的處理函數keyFunc即可。這樣,當需要從下划線轉駝峰時,就不需要復制代碼,然后只改動一行了。代碼如下所示,使用了 Java8Map 遍歷方式使得代碼更加簡潔可讀。
public static Map<String,Object> generalMapProcess(Map<String,Object> map,
Function<String, String> keyFunc,
Set<String> ignoreKeys) {
Map<String,Object> resultMap = new HashMap<String,Object>();
map.forEach(
(key, value) -> {
if (ignoreKeys.contains(key)) {
resultMap.put(key, value);
}
else {
String newkey = keyFunc.apply(key);
if ( (value instanceof List) ) {
resultMap.put(keyFunc.apply(key),
buildValueList((List) value, ignoreKeys,
(m, keys) -> generalMapProcess(m, keyFunc, ignoreKeys)));
}
else if (value instanceof Map) {
Map<String, Object> subResultMap = generalMapProcess((Map) value, keyFunc, ignoreKeys);
resultMap.put(newkey, subResultMap);
}
else {
resultMap.put(keyFunc.apply(key), value);
}
}
}
);
return resultMap;
}
單測####
使用Groovy來對上述代碼進行單測。因為Groovy可以提供非常方便的Map構造。單測代碼如下所示:
TransferUtils.groovy
package cc.lovesq.study.test
import zzz.study.datastructure.map.TransferUtil
import static zzz.study.datastructure.map.TransferUtil.*
/**
* Created by shuqin on 17/12/31.
*/
class TransferUtilTest {
static void main(String[] args) {
[null, "", " "].each {
assert "" == camelToUnderline(it)
}
["isBuyGoods": "is_buy_goods", "feeling": "feeling", "G":"G", "GG": "GG"].each {
key, value -> assert camelToUnderline(key) == value
}
[null, "", " "].each {
assert "" == underlineToCamel(it)
}
["is_buy_goods": "isBuyGoods", "feeling": "feeling", "b":"b", "_b":"B"].each {
key, value -> assert underlineToCamel(key) == value
}
def amap = ["code": 200,
"msg": "successful",
"data": [
"total": 2,
"list": [
["isBuyGoods": "a little", "feeling": ["isHappy": "ok"]],
["isBuyGoods": "ee", "feeling": ["isHappy": "haha"]],
],
"extraInfo": [
"totalFee": 1500, "totalTime": "3d",
"nestInfo": [
"travelDestination": "xiada",
"isIgnored": true
],
"buyList": ["Food","Dress","Daily"]
]
],
"extendInfo": [
"involveNumber": "40",
]
]
def resultMap = [:]
def ignoreSets = new HashSet()
ignoreSets.add("isIgnored")
tranferKeyToUnderline(amap, resultMap, ignoreSets)
println(resultMap)
def resultMap2 = tranferKeyToUnderline2(amap, ignoreSets)
println(resultMap2)
def resultMap3 = generalMapProcess(amap, TransferUtil.&camelToUnderline, ignoreSets)
println(resultMap3)
def resultMap4 = generalMapProcess(resultMap3, TransferUtil.&underlineToCamel, ignoreSets)
println(resultMap4)
}
}
小結###
本文使用Java語言遞歸地將復雜的嵌套Map里的字段名由駝峰轉下划線,並給出了更通用的代碼形式,同時展示了如何處理復雜的嵌套結構。復雜的結構總是由簡單的子結構通過組合和嵌套而構成,通過對子結構分而治之,然后使用遞歸技術來組合結果,從而實現對復雜結構的處理。
通過此例,學習了:
- 設計遞歸程序來解析任意嵌套的List-Map容器結構;
- 使用函數接口使代碼更加通用化;
- 使用Groovy來進行單測。