List在使用中存在的这些问题你都知道吗?

2023年 10月 8日 113.9k 0

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜

众所周知,Java为开发者提供了多种集合类的实现。而Java的集合类通常被分为两大类:MapCollection。进一步,Collection又分为三个子类,包括List、Set和Queue。其中,可以几乎所有业务代码都需要用到List,因此其也被认为是最为是集合中最重要的一个结构。

List的错误使用也会导致诸多问题,今天我们就来看一看几个错误使用List的场景。

前言

在日常的Java面试中,ArrayList绝对可以算得上是Java后端面试中的一个常客。为此网上也有很多文章来对ArrayList的源码进行分析,其实重点归结下来无非以下几点

  • ArrayList中元素增、删操作的原理;
  • ArrayList中的扩容机制,元素拷贝原理;
  • ArrayList线程安全性以及fast-fail机制等。
  • 在面试中可能你对于上述问题可以做到对答如流,但是真正落实到实际开发中你能保证写出没有Bug的代码吗?对于这个问题不同的开发者可能有着不同的答案,但无论你的答案是什么,笔者还是希望你能花几分钟读一读后续的内容,相信读完你一定会有所收获~~~

    删除元素时传递的参数到底是什么?

    在开始分析之前,我们先来看一段代码:

    @Test
    public void testStrList() {
        ArrayList strs = new ArrayList();
    
        strs.add("hello");
        strs.add("coding");
        strs.add("word");
    
        strs.remove(1);
        log.info("Arrays  after is [{}] ", Arrays.toString(strs.toArray()));
    }
    
    

    上述代码逻辑很简单,无非就是向strs中添加三个字符串信息,然后调用remove方法进行删除,那此时上述代码会打印出什么内容呢?此时,相信你肯定不假思索的回答出肯定是Arrays after is [[hello, word]]。但是当我拿出如下这段代码,阁下又该如何应对呢?

    @Test
    public void testList() {
       ArrayList nums = new ArrayList();
    
       for (int i = 0 ; i < 5 ; i ++) {
           nums.add(5-i);
       }
    
       
        //  nums.remove(new Integer(3));
        nums.remove(3);
        log.info("Arrays  after is [{}] ", Arrays.toString(nums.toArray()));
    }
    

    对于上述代码,笔者有两个疑问:

  • 上述代码会输出什么呢?
  • 如果将上述代码nums.remove(3)注释掉,而将处代码注释打开,其输出结果又是什么呢?
  • 这个问题归根到底本质就是对于ArrayListremove方法的理解,可能面试时你对于ArrayListremove方法也能侃侃而谈,但面对如此场景你还能不假思索的回答出来吗?

    有疑惑也别慌,接下来我们就来看看List中关于remove的方法有什么样的定义:

    List # remove

    public interface List extends Collection {
    
    	......
    	boolean remove(Object o);
    	......
    	E remove(int index);
    	......
    }
    

    可以看到,在List接口中,对于remove方法进行了重载,其支持两种类型的传参:一种是传入基本类型;另一种则需要传入一个对象。进一步,ArrayList中对于不同参数下remove方法的执行逻辑也是有差异的。具体来看:

  • remove中传入的参数为int时,则会删除参数对应索引中的元素;
  • remove中传入的参数为Object时,则会遍历列表中的元素,进而删除列表中的对应元素
  • 至此,上述代码执行后的结果其实已经很明显了。当执行nums.remove(3)时,其会删除对应索引下的的元素,即删除列表中的元素2;而当执行nums.remove(new Integer(3))时,则会删除列表中的元素3

    可能你会觉得,这里无非就是remove方法有一个重载逻辑罢了,有何难?你无非就是比我看的时候细心了一点罢了,这又能算的上什么呢?

    如果你有这样的想法,那不妨来看看如下这段代码,想一想下列代码最终输出的size是多少呢?

    public void testStrList() {
        ArrayList strs = new ArrayList();
    
        strs.add("hello");
        strs.add("coding");
        strs.add("word");
        
        // 定义匹配规则
        ArrayList mapStrs = new ArrayList(Arrays.asList("hello"));
    
        // 存储适配信息坐标
        ArrayList index = new ArrayList();
        for (int i = 0 ; i < strs.size() ; i ++) {
            if (mapStrs.contains(strs.get(i))) {
                index.add(i);
            }
        }
        
        // 删除匹配信息
        for (int i = 0 ; i < index.size() ; i ++) {
            strs.remove(index.get(i));
        }
        log.info("strs removed size is [{}] ", strs.size());
    }
    

    上述代码最终会输出strs removed size is 3,不知你的答案是否是这样的?至于为何是这样,笔者在此就不进行分析了。这算是笔者留的一个思考题,欢迎你在评论区留言。

    Arrays.asList你用对了吗?

    众所周知,Arrays.asListJava 中的一个静态方法,它的主要作用是将一个数组转换成一个固定大小的 List。 那下述两种方式构建的List有什么差异呢?

    int[] arr = {1, 2, 3}; 
    
    //  传入数组构建List
    List listArray = Arrays.asList(arr);
    
    //  通过参数构建List
    List list = Arrays.asList(1,2,3);
    
    

    执行代码你会发现,方法这样初始化的 List 并不是我们期望的包含 3 个数字的 List。而其会创建一个元素类型为[]intList。换言之,其最终会生成的 List是一个元素个数为 1的整型数组。

    那为什么会这样?我们来看下其源码:

     public static List asList(T... a) { 
        return new ArrayList(a); 
     }
    

    进一步,其在构建ArrayList时的逻辑如下所示:

    image.png

    通过追踪源码可以看到,Arrays.asList 方法传入的是一个泛型T类型可变参数,而我们传入的int[]最终作为了一个对象成为了泛型类型 T。当然,这个问题更深层的原因在于:Jdk在对象的封箱/拆箱只支持将 int 装箱为 Integer,却不能把 int 数组装箱为 Integer 数组。进一步,如下代码则不会存在此问题。

    Integer[] arr2 = {1, 2, 3}; 
    List list = Arrays.asList(arr2);
    

    总之, Arrays.asList 不能直接使用来转换基本类型数组。 进一步,既然Arrays.asList可以得到一个List,那对其进行add、remove等操作是不是没啥问题?。例如如下这样

    Integer[] arr2 = {1, 2, 3}; 
    List list = Arrays.asList(arr2);
    
    list.add(3,4)
    

    如果运行上述代码你会喜提一个UnsupportedOperationException。换言之,为list 中的3号位置新增整型 4 的操作失败了。造成这一问题的在于:Arrays.asList 返回的List不支持增删操作。即Arrays.asList 返回的 List 并不是 我们期望的 java.util.ArrayList。其最终会返回 Arrays 的内部类 ArrayList。而该类并没有覆写父类AbstractListadd方法,而父类中add方法的实现,就是抛出 UnsupportedOperationException。相关代码如下:

    AbstractList # add

    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    

    总结

    至此,我们抛砖引玉的介绍了使用List时可能遇到的一些小问题。事实上,除了上述讨论的问题外List在使用切片功能也可能导致一些意想不到的OOM异常,在此笔者就不逐一进行分析了,感兴趣的读者可翻阅相关资料进行了解。

    或许,List使用过程中的这些小细节在你看来无关紧要,但细节决定成败。当然,这也是一个仁者见仁智者的问题,笔者在此就不赘述了。

    最后的最后,还是希望本文能对你日常开发有所启发,这是也是笔者写这篇文章的初衷。同时,如果觉得文章还可以,不妨点个关注,不错过笔者的每一次更新!

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论