【代码分享】关于List<V>按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向下兼容导致的不便吧!

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值