【程式碼分享】關於List按V的某個屬性分組的通用程式碼實現
阿新 • • 發佈:2019-02-07
背景是這樣的:我們的專案中,定義了各種各樣的和表對應的實體類。我們的邏輯中,經常會查出某個表的資料,然後按照這個表的某個欄位進行分組。例如,A表,有屬性ID和姓名name及其它屬性,我們查出一批資料後,想按照name進行分組,生成Map<name,List<A>>這樣結構的map。於是,我們寫了一段如下的分組程式碼:
<span style="font-size:18px;"> /** * 按name分組方法 * @param list A表實體的列表 * @param map 分組後的儲存map */ public static void groupA(List<AEntity> list, Map<String, List<AEntity>> map) { if (null == list || null == map) { return; } // 按name開始分組 String key; List<AEntity> listTmp; for (AEntity val : list) { key = val.getName(); listTmp = map.get(key); if (null == listTmp) { listTmp = new ArrayList<AEntity>(); map.put(key, listTmp); } listTmp.add(val); } }</span>
其中,map是呼叫方new好的HashMap,透傳給方法用來承載分組結果的。程式碼很精簡,自我感覺良好。後來又碰到B表,同樣的需要查出來,按照某個欄位分組,於是就又寫了一段程式碼:
<span style="font-size:18px;"> /** * 按age分組方法 * @param list B表實體的列表 * @param map 分組後的儲存map */ public static void groupB(List<BEntity> list, Map<String, List<BEntity>> map) { if (null == list || null == map) { return; } // 按age開始分組 String key; List<AEntity> listTmp; for (AEntity val : list) { key = val.getAge(); listTmp = map.get(key); if (null == listTmp) { listTmp = new ArrayList<AEntity>(); map.put(key, listTmp); } listTmp.add(val); } }</span>
程式碼依然精簡,但是感覺不太好了。這幾乎和前面一個方法一樣,能不能精簡一下呢?
於是就各種思考,總結出了幾個特點:
1. 入參泛型不同
2. 分組的維度(屬性方法)不同
如果能把這兩個不同點統一起來,是不是就可以提取一個共同的工具類方法了?
思路也簡單:入參泛型不同,那方法就使用泛型;分組使用的方法不同,就用反射機制,獲取方法。於是有了初版的通用方法:
<span style="font-size:18px;"> /** * 將List<V>按照V的某個方法返回值(返回值必須為K型別)分組,合入到Map<K, List<V>>中<br> * 要保證入參的method必須為V的某一個有返回值的方法,並且該返回值必須為K型別 * * @param list 待分組的列表 * @param map 存放分組後的map * @param method 方法 */ @SuppressWarnings("unchecked") public static <K, V> void listGroup2Map(List<V> list, Map<K, List<V>> map, Method method) { // 入參非法行校驗 if (null == list || null == map || null == method) { LOGGER.error("CommonUtils.listGroup2Map 入參錯誤,list:" + list + " ;map:" + map + " ;method:" + method); return; } try { // 開始分組 Object key; List<V> listTmp; for (V val : list) { key = method.invoke(val); listTmp = map.get(key); if (null == listTmp) { listTmp = new ArrayList<V>(); map.put((K) key, listTmp); } listTmp.add(val); } } catch (Exception e) { LOGGER.error("分組失敗!", e); } } /** * 根據類和方法名,獲取方法物件 * * @param clazz * @param methodName * @return */ public static Method getMethodByName(Class<?> clazz, String methodName) { Method method = null; // 入參不能為空 if (null == clazz || StringUtils.isBlank(methodName)) { LOGGER.error("CommonUtils.getMethodByName 入參錯誤,clazz:" + clazz + " ;methodName:" + methodName); return method; } try { method = clazz.getDeclaredMethod(methodName); } catch (Exception e) { LOGGER.error("類獲取方法失敗!", e); } return method; }</span>
這兩個方法,第二個是為了獲取類似getName、getAge之類的方法物件,然後傳遞給第一個方法即可。(如果大家不想依賴log包之類的,可以將LOGGER處刪掉,StringUtils.isBlank方法替換成字串非空判斷即可)
到這裡,我想分享的程式碼主體思路已經出來了。考慮到讓呼叫者每次都呼叫兩個方法,不太友好,就又改了一版,又補充增加了一個方法:
<span style="font-size:18px;"> /**
* 將List<V>按照V的methodName方法返回值(返回值必須為K型別)分組,合入到Map<K, List<V>>中<br>
* 要保證入參的method必須為V的某一個有返回值的方法,並且該返回值必須為K型別
*
* @param list 待分組的列表
* @param map 存放分組後的map
* @param clazz 泛型V的型別
* @param methodName 方法名
*/
public static <K, V> void listGroup2Map(List<V> list, Map<K, List<V>> map, Class<V> clazz, String methodName) {
// 入參非法行校驗
if (null == list || null == map || null == clazz || StringUtils.isBlank(methodName)) {
LOGGER.error("CommonUtils.listGroup2Map 入參錯誤,list:" + list + " ;map:" + map
+ " ;clazz:" + clazz + " ;methodName:" + methodName);
return;
}
// 獲取方法
Method method = getMethodByName(clazz, methodName);
// 非空判斷
if (null == method) {
return;
}
// 正式分組
listGroup2Map(list, map, method);
}</span>
測試方法如下:
<span style="font-size:18px;"> @Test
public void testGroup() {
AEntity a1 = new AEntity();
a1.setId("111");
a1.setName("name1");
AEntity a2 = new AEntity();
a2.setId("222");
a2.setName("name");
AEntity a3 = new AEntity();
a3.setId("111");
a3.setName("name3");
AEntity a4 = new AEntity();
a4.setId("222");
a4.setName("name");
List<AEntity> list = new ArrayList<AEntity>();
list.add(a1);
list.add(a2);
list.add(a3);
list.add(a4);
list.add(a5);
System.out.println("list分組前為:" + list);
Map<String, List<AEntity>> map = new HashMap<String, List<AEntity>>();
CommonUtils.listGroup2Map(list, map, AEntity.class, "getName");// 輸入方法名
System.out.println("分組完成,分組後的map為:" + map);
}</span>
至此,我想分享的程式碼就出來了。關於效能,我也做了迴圈10次、100次、1000次、10000次的對比。1000次以下的,耗時差不多,這種通用方式會稍微慢那麼一點點(幾毫秒)。10000次的差別就有點大了,傳統方式耗時3到9ms,通用方式耗時25~78ms不等(畢竟用到反射了)。當然,這個耗時也跟測試樣本規模有關,沒有深究了。因此,對效能要求非常高的專案,要慎重考慮。
也許某些開源的工具類中已經有過這樣的方法了,不過我沒看到,就自己總結了一把,希望對大家有所幫助。
最後再碎碎念一把:泛型不支援類似V.class這樣的呼叫,不然還能省掉Class<V> clazz這個入參呢。這都是Java向下相容導致的不便吧!