1. 程式人生 > >【程式碼分享】關於List按V的某個屬性分組的通用程式碼實現

【程式碼分享】關於List按V的某個屬性分組的通用程式碼實現

        背景是這樣的:我們的專案中,定義了各種各樣的和表對應的實體類。我們的邏輯中,經常會查出某個表的資料,然後按照這個表的某個欄位進行分組。例如,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向下相容導致的不便吧!