Java ArrayList 踩坑记录

  做编程的一个常识:不要在循环过程中删除元素本身(至少是我个人的原则)。否则将发生不可预料的问题。

  而最近,看到一个以前的同学写的一段代码就是在循环过程中删除元素,我很是纳闷啊。然后后来决定给他改掉。然后引发了另外的惨案。

  原来的代码是这样的:

    public List<A> getUserDebitCard(A cond) {
        List<A> list=userService.getCard(cond);
        List<A> result=null;
        if(list!=null && list.size()>0){
            Collections.sort(list, new Comparator<A>(){
              public int compare(A b1, A b2) {
              //按时间排序
                if(Integer.valueOf(b1.getAddTime()) > Integer.valueOf(b2.getAddTime())){
                   return -1;
                  }
                  return 1;
                }
             });
             A bean = getA(cond);
             result=new ArrayList<A>();
             if(bean!=null){
                 for (int i = 0; i < list.size(); i++) {
                    if(list.get(i).getCardNum().equals(bean.getCardNum())){
                        list.get(i).setAs(1);
                        result.add(list.get(i));
                        list.remove(i);
                    }else{
                        list.get(i).setAs(0);
                    }
                 }
             }
            result.addAll(list);
         }
        return result;
    }

  看了如上代码,我很是郁闷,然后给改成如下:

    public List<A> getUserDebitCard(A cond) {
        List<A> list=userService.getCard(cond);
        List<A> result=null;
        if(list!=null && list.size()>0){
            Collections.sort(list, new Comparator<A>(){
              public int compare(A b1, A b2) {
              //按时间排序
                if(Integer.valueOf(b1.getAddTime()) > Integer.valueOf(b2.getAddTime())){
                   return -1;
                  }
                  return 1;
                }
             });
             A bean = getA(cond);
             result=new ArrayList<>();
             if(bean != null){
                 // 将上次的卡放置在第一位
                 Integer lastAIndex = 0;
                 Integer listSize = list.size();
                 if(listSize > 0) {
                     for (int i = 0; i < listSize; i++) {
                         if (list.get(i).getCardNum().equals(bean.getCardNum())) {
                             list.get(i).setAs(1);
                             result.add(list.get(i));        //将排在首位的元素先添加好,并记录下index
                             lastAIndex = i;
//                             list.remove(i);                //循环过程中删除元素是危险的
                         } else {
                             list.get(i).setAs(0);
                         }
                     }
                     list.remove(lastAIndex);
                 }
             }
             result.addAll(list);                            //在循环外删除元素,以为万事大吉,结果悲剧了,这里居然添加了两个元素进来
         }
        return result;
    }

  这下出事了,原本只有一个元素的result,现在变成了两个了,这是为什么呢?妈蛋,我明明已经remove掉了啊。

  也想过百度一下,但是木有搞定啊。然后,拿出看家绝招,断点调试,进入list.remove(Integer) 方法。其源码如下:

    /**
     * Removes the first occurrence of the specified element from this list,
     * if it is present.  If the list does not contain the element, it is
     * unchanged.  More formally, removes the element with the lowest index
     * <tt>i</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (if such an element exists).  Returns <tt>true</tt> if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param o element to be removed from this list, if present
     * @return <tt>true</tt> if this list contained the specified element
     */
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

  原来,由于我使用Integer作为删除元素的条件,这里把Integer当作一个元素去比较了,而并不是平时我们以为的自动拆装箱变为int了。 而进入这个方法的意思,是要找到和元素内容相等的元素,然后删除它。而我给的是一个索引,自然就不相等了,没有删除也是自然了,因为就会看到重复的元素出来了。

  我们再来看一下根据索引删除元素的源码,对比一下:

    /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

  从这里我们可以看到,根据索引删除ArrayList元素的原理是,将原来的数组排除要删除的索引,然后复制到新的数组去,然后将最后一个元素置为null,留给GC回收。并把删除的元素返回。

  这样,一下就明白了删除ArrayList元素到底是咋么一回事了。修复方案就简单了,将Integer替换为int即可:

        // Integer lastAIndex = 0;   //替换成int
        int lastAIndex = 0;

  当然了,最后,我还是换成了另一个更稳妥的方案了,用另一个容器接收最终的结果

    public List<A> getUserDebitCard(A cond) {
        List<A> list=userService.getCard(cond);
        List<A> result=null;
        if(list!=null && list.size()>0){
            Collections.sort(list, new Comparator<A>(){
              public int compare(A b1, A b2) {
              //按时间排序
                if(Integer.valueOf(b1.getAddTime()) > Integer.valueOf(b2.getAddTime())){
                   return -1;
                  }
                  return 1;
                }
             });
             A bean = getA(cond);
             result=new ArrayList<>();
             if(bean != null){
                 // 将上次的卡放置在第一位
                 for (A card1 : list) {
                     if (card1.getCardNum().equals(bean.getCardNum())) {
                         card1.setAs(1);
                         result.add(0, card1);    //直接插入第一位即可
//                         list.remove(i);                //循环过程中删除元素是危险的
                     } else {
                         card1.setAs(0);
                         result.add(card1);
                     }
                 }
             }
         }
        return result;
    }

  虽然自动拆装箱很方便,也很实用,但是有时一不小心就会把自己给埋坑里了,当心了。尤其是针对线上问题。

  毕竟,代码中的一点点小问题,一到线上就会被无限放大,不可掉以轻心啊!

时间: 2024-10-04 10:57:36

Java ArrayList 踩坑记录的相关文章

unionId突然不能获取的踩坑记录

昨天(2016-2-2日),突然发现系统的一个微信接口使用不了了.后来经查发现,是在网页授权获取用户基本信息的时候,unionid获取失败导致的. 在网页授权获取用户基本信息的介绍中(http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html),unionid可以在第二步(https://api.weixin.qq.com/sns/oauth2/access_token)和第四步的2个接口中获得.其中,第四步的接口是

IDFA踩坑记录

IDFA踩坑记录: 1.iOS10.0 以下,即使打开"限制广告跟踪",依然可以读取idfa: 2.打开"限制广告跟踪",然后再关闭"限制广告跟踪",idfa会改变: 3.越狱机器安装开发证书打的包,读取的idfa正常: 4.越狱机器安装本地打的发布证书的包,读取idfa异常,第一次打开app读取的是一个错误的idfa,第二次打开会变成另外一个错误的idfa: 5.越狱机器安装Testflight的包,读取idfa异常,第一次打开app读取的是一

【Pysc2】Deepmind Pysc2 环境配置及其踩坑记录

1. 下载星际争霸II游戏 可以直接下暴雪战网,然后在战网内下载. 2. 下载Pysc2 cmd后在命令行输入 pip install pysc2 3. 下载sc2 cmd后在命令行输入 pip install sc2 4. 下载地图 https://github.com/ClausewitzCPU0/SC2AI 解压密码: iagreetotheeula (表示自己同意最终用户许可协议) 解压在Maps的文件夹,我是使用的暴雪战网下载的游戏,发现游戏文件夹里并没有Maps, 需要自己手动创建.

Linux Git 踩坑记录

Linux Git 踩坑记录 git cherry-pick 冲突解决 出现: error: could not apply xxxxxx(commit ID)... ***** hint: after resolving the conflicts, mark the corrected paths hint: with 'git add ' or 'git rm ' hint: and commit the result with 'git commit' 此时使用git status命令查

java用毫秒数做日期计算的一个踩坑记录

错误示例: Date today = new Date(); Date nextMonth = new Date(today.getTime() + 30* 1000*60*60*24); println(today); println(nextMonth); Result:  Sat Sep 30 11:18:24 CST 2017 Sun Sep 10 18:15:37 CST 2017 代码说明:上面代码的目的是计算一个月后的日期,从结果发现明显是错误的(回到上个月去了) 原因分析:30*

微信小程序之蓝牙 BLE 踩坑记录

前言 前段时间接手了一个微信小程序的开发,主要使用了小程序在今年 3 月开放的蓝牙 API ,此过程踩坑无数,特此记录一下跳坑过程.顺便开了另一个相关的小项目,欢迎 start 和 fork: BLE_MiniProgram API简介 微信小程序目前有蓝牙 API 共 18 个,其中操作蓝牙适配器的共有 4 个,分别是 wx.openBluetoothAdapter 初始化蓝牙适配器 wx.closeBluetoothAdapter 关闭蓝牙模块 wx.getBluetoothAdapterS

.NET CORE2.2 下 Ocelot+Consul服务发现踩坑记录

历史原因,笔者所在公司的项目目前还在使用 .NET CORE 2.2版本,在所有业务应用升级完成服务注册发现之后,最后剩下 Ocelot 网关服务升级.在升级过程中,遇到一些问题,记录此文,以便有相同情况的同学参考. 1. Ocelot 升级服务发现 根据官方文档 ,通过简单的添加配置,既可以将原有配置方式改为服务发现: 安装插件 Install-Package Ocelot.Provider.Consul 13.5.2,.Net Core 2.x 最后一个版本 配置服务 s.AddOcelot

vue2.0 踩坑记录之组件

- did you register the component correctly? For recursive components, make sure to provide the "name" option. 出现如上错误的原因大致有以下三点(我踩坑只踩到了这四种情况,如果还有 之后会更新) 1.未能正确的拼好components单词. 这是个低级错误,应该作为第一步排查的可能 2.定义子组件时没有在父组件的内部定义 子组件一定要在父组件中局部注册 3.局部注册的定义写在了实

CISCO配置文件的导出和导入(Timed out)问题 踩坑记录

起因: 我:这台交换机我要用. 武哥:不行,在测试,上面很多配置项都不能动! 我..... 后来武哥找了台3560交换机让我自己玩去:"拷贝一下交换机配置,就能用了" 使用工具:3CDaemon 大致流程: 1 开启3CDaemon,这样你的电脑就能作为一个tftp服务器了,监控udp 69端口 2 交换机配置文件拷贝到tftp上 #copy   源   目的: 输入tftp服务器地址 确定文件名(如果是交换机从tftp下载要输入文件名) ok 简单so 3 将tftp服务器上的配置文