IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> list大坑请注意 -> 正文阅读

[数据结构与算法]list大坑请注意

问题一:Arrays.asList()返回的list不支持增删操作

问题复现

package com.geekmice.onetomany.list;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ListTest {
    public static void main(String[] args) {
        t1();
    }

    /**
     * @description 数组转换list
     */
    public static void t1() {
        ArrayList<Object> objects = new ArrayList<>();
        String[] array = {"张三", "李四", "王五"};
        List<String> list = Arrays.asList(array);
        // Exception in thread "main" java.lang.UnsupportedOperationException
        list.add("小三");
    }
}

错误提示

Exception in thread “main” java.lang.UnsupportedOperationException

在这里插入图片描述

分析:上面这几行代码,主要是将数组数组转换list,转换后list,进行添加操作提示错误 UnsupportedOperationException,刚开始很不解,Arrays#asList 返回明明也是一个 ArrayList,为什么添加一个元素就会报错?这以后还能好好新增元素吗;后面看了一下java.util.ArrayList和Arrays#asList 内部情况,其实不一样的;
对于Arrays#asList 而言

    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

发现这个Arrays#asList 返回的 ArrayList 其实是个赝品,仅仅只是 Arrays 一个内部类,并非真正的 java.util.ArrayList
在这里插入图片描述
从上图我们发现,add/remove 等方法实际都来自 AbstractList,而 java.util.Arrays$ArrayList 并没有重写父类的方法。而父类方法恰恰都会抛出 UnsupportedOperationException。

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

真正ArrayList,重写add,delete方法,可以正常使用
在这里插入图片描述

问题二:List,为什么却还互相影响

问题复现

    public static void t2() {
        String[] array = {"张三", "李四", "王五"};
        List<String> list = Arrays.asList(array);
        list.set(0, "001");
        array[1] = "002";
        System.out.println("array:" + Arrays.toString(array));
        System.out.println("list:" + list);
    }

效果展示

array:[001, 002, 王五]
list:[001, 002, 王五]

从日志输出可以看到,不管我们是修改原数组,还是新 List 集合,两者都会互相影响。
查看 java.util.Arrays$ArrayList 实现,我们可以发现底层实际使用了原始数组

        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }

知道了实际原因,修复的办法也很简单,套娃一层 ArrayList 呗!

 List<String> list = new ArrayList<>(Arrays.asList(arrays));
    public static void t2() {
        String[] array = {"张三", "李四", "王五"};
        List<String> list = new ArrayList<>(Arrays.asList(array));
        list.set(0, "001");
        array[1] = "002";
        System.out.println("array:" + Arrays.toString(array));
        System.out.println("list:" + list);
    }

array:[张三, 002, 王五]
list:[001, 李四, 王五]
不过这么写感觉十分繁琐,推荐使用 Guava Lists 提供的方法。

 List<String> list = Lists.newArrayList(arrays);

引入依赖

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>22.0</version>
        </dependency>

完善后代码

   public static void t2() {
        String[] array = {"张三", "李四", "王五"};
        ArrayList<String> list = Lists.newArrayList(array);
        list.set(0, "001");
        array[1] = "002";
        System.out.println("array:" + Arrays.toString(array));
        System.out.println("list:" + list);
    }

最终效果

array:[张三, 002, 王五]
list:[001, 李四, 王五]

问题复现二

除了 Arrays#asList产生新集合与原始数组互相影响之外,JDK 另一个方法 List#subList 生成新集合也会与原始 List 互相影响;

 public static void t2() {
      
        ArrayList<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);
        integerList.add(3);
        // Returns a view of the portion of this list between the specified
        List<Integer> subList = integerList.subList(0, 2);
        subList.set(0, 10);
        integerList.set(1, 20);
        System.out.println("integerList:" + integerList);
        System.out.println("subList:" + subList);
    }

效果展示

integerList:[10, 20, 3]
subList:[10, 20]

查看 List#subList 实现方式,可以发现这个 SubList 内部有一个 parent 字段保存保存最原始 List

   public List<E> subList(int fromIndex, int toIndex) {
       subListRangeCheck(fromIndex, toIndex, size);
       return new SubList(this, 0, fromIndex, toIndex);
   }

   SubList(AbstractList<E> parent,
           int offset, int fromIndex, int toIndex) {
       this.parent = parent;
       this.parentOffset = fromIndex;
       this.offset = offset + fromIndex;
       this.size = toIndex - fromIndex;
       this.modCount = ArrayList.this.modCount;
   }

所有外部读写动作看起来是在操作 SubList ,实际上底层动作却都发生在原始 List 中,比如 add 方法

出现OOM问题场景

private static List<List<Integer>> data = new ArrayList<>();

private static void oom() {
    for (int i = 0; i < 1000; i++) {
        List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());
        data.add(rawList.subList(0, 1));
    }
}

data 看起来最终保存的只是 1000 个具有 1 个元素的 List,不会占用很大空间。但是程序很快就会 OOM。

OOM 的原因正是因为每个 SubList 都强引用个一个 10 万个元素的原始 List,导致 GC 无法回收。

这里修复的办法也很简单,跟上面一样,也来个套娃呗,加一层 ArrayList

问题三:不可变集合,说好不变,你怎么就变了

为了防止 List 集合被误操作,我们可以使用 Collections#unmodifiableList 生成一个不可变(immutable)集合,进行防御性编程。

这个不可变集合只能被读取,不能做任何修改,包括增加,删除,修改,从而保护不可变集合的安全

问题复现

 public static void t3() {
        ArrayList<String> list = new ArrayList<>(Arrays.asList("one", "two", "three"));
        List<String> unmodifiableList = Collections.unmodifiableList(list);
        // Exception in thread "main" java.lang.UnsupportedOperationException
        // 看起来没什么问题
        unmodifiableList.add("1");
        unmodifiableList.remove(1);
        unmodifiableList.set(0, "t");
        // 以下进行测试
        // list.set(0, "first_modify");
        // Assertions.assertEquals(list.get(0), unmodifiableList.get(0));
        // list.add("fourth");
        // Assertions.assertEquals(list.get(3), unmodifiableList.get(3));
        // Assertions.assertEquals(list.size(), unmodifiableList.size());
    }

效果展示

在这里插入图片描述
上面单元测试结果将会全部通过,这就代表 Collections#unmodifiableList 产生不可变集合将会被原始 List 所影响。

分析

查看 Collections#unmodifiableList 底层实现

        UnmodifiableList(List<? extends E> list) {
            super(list);
            // 这里面引入原始list
            this.list = list;
        }

可以看到这跟上面 SubList 其实是同一个问题,新集合底层实际使用了原始 List。

解决方案

使用 JDK9 List#of 方法。

List<String> list = new ArrayList<>(Arrays.asList("one", "two", "three"));
List<String> unmodifiableList = List.of(list.toArray(new String[]{}));

使用 Guava immutable list

List<String> list = new ArrayList<>(Arrays.asList("one", "two", "three"));
List<String> unmodifiableList = ImmutableList.copyOf(list);

相比而言 Guava 方式比较清爽,使用也比较简单,推荐使用 Guava 这种方式生成不可变集合。

问题四:foreach 增加/删除元素大坑

问题复现

    public static void t4(){
        String[] array = {"1","2","3"};
        ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
        for (String s : list) {
            if("1".equals(s)){
                // Exception in thread "main" java.util.ConcurrentModificationException
                list.remove(s);
            }

        }
    }

上面代码我们使用foreach方式遍历list集合,如果符合条件,将会从集合中删除该元素;
这个程序编译正常,但是运行时候,程序异常,日志如下

效果展示

Exception in thread “main” java.util.ConcurrentModificationException
at java.util.ArrayList I t r . c h e c k F o r C o m o d i f i c a t i o n ( A r r a y L i s t . j a v a : 901 ) a t j a v a . u t i l . A r r a y L i s t Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList Itr.checkForComodification(ArrayList.java:901)atjava.util.ArrayListItr.next(ArrayList.java:851)
at com.geekmice.onetomany.list.ListTest.t4(ListTest.java:67)
at com.geekmice.onetomany.list.ListTest.main(ListTest.java:14)

在这里插入图片描述

分析

可以看到最终错误是由ArrayList$Itr.next处代码抛出,但是代码中我们并没有调用,为什么呢?
实际上这是foreach方式给java提供语法糖,编译后编程另外一种方式,反编译看一下

    public static void t4() {
        String[] array = new String[]{"1", "2", "3"};
        ArrayList<String> list = new ArrayList(Arrays.asList(array));
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String s = (String)var2.next();
            if ("1".equals(s)) {
                list.remove(s);
            }
        }

    }

可以看到foreach这种方式实际就是迭代器Iterator实现的,这也就是foreach被遍历的类需要实现Iterator接口的原因
在这里插入图片描述
看到modCount 和expectedModCount不同才错误,再看看这两个属性什么含义

        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
    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;
    }
        /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        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
    }

在这里插入图片描述
modCount 来源于 ArrayList 的父类 AbstractList,可以用来记录 List 集合被修改的次数
modCount 计数操作将会交子类自己操作,ArrayList 每次修改操作(增、删)都会使 modCount 加 1

解决方案

使用迭代器删除

String[] array = {"1","2","3"};
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
	String str = iterator.next();
	if(str.equals("1"){
		iterator.remove();
	}
}

使用removeIf删除

String[] array = {"1","2","3"};
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
list.removeIf(str->str.equals("1"));

总结

第一、Arrays.asList和List.subList就是一个普通独立的ArrayList

如果没有办法,使用了Arrays.asList和List.sublist,返回给其他方法时候,一定要嵌套真正的ArrayList

第二、jdk提供的不可变集合非常笨重,低效,不安全,推荐使用guava不可变集合
第三、不要随便在foreach增加/删除元素

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 19:31:09  更:2022-08-19 19:32:28 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/28 18:58:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计