list遍历陷阱分析原理

Posted lishiqi-blog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了list遍历陷阱分析原理相关的知识,希望对你有一定的参考价值。

35.Arraylist 的动态扩容机制是如何自动增加的?简单说说你理解的增加流程!

解析:

当在 ArrayList 中增加一个对象时 Java 会去检查 Arraylist 以确保已存在的数组中有足够的容量来存储这个新对象,如果没有足够容量就新建一个长度更长的数组(原来的1.5倍),旧的数组就会使用 Arrays.copyOf 方法被复制到新的数组中去,现有的数组引用指向了新的数组。下面代码展示为 Java 1.8 中通过 ArrayList.add 方法添加元素时,内部会自动扩容,扩容流程如下:

//确保容量够用,内部会尝试扩容,如果需要
ensureCapacityInternal(size + 1)

//在未指定容量的情况下,容量为DEFAULT_CAPACITY = 10
//并且在第一次使用时创建容器数组,在存储过一次数据后,数组的真实容量至少DEFAULT_CAPACITY
 private void ensureCapacityInternal(int minCapacity) 
    //判断当前的元素容器是否是初始的空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) 
        //如果是默认的空数组,则 minCapacity 至少为DEFAULT_CAPACITY
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    

    ensureExplicitCapacity(minCapacity);


//通过该方法进行真实准确扩容尝试的操作
 private void ensureExplicitCapacity(int minCapacity) 
        modCount++;//记录List的结构修改的次数

        //需要扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);


//扩容操作
 private void grow(int minCapacity) 
    //原来的容量
    int oldCapacity = elementData.length;
    
    //新的容量 = 原来的容量 + (原来的容量的一半)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    //如果计算的新的容量比指定的扩容容量小,那么就使用指定的容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    //如果新的容量大于MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)
    //那么就使用hugeCapacity进行容量分配
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
    
    //创建长度为newCapacity的数组,并复制原来的元素到新的容器,完成ArrayList的内部扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
 

36.下面这些方法可以正常运行吗?为什么?

public void remove1(ArrayList<Integer> list) 
    for(Integer a : list)
        if(a <= 10)
            list.remove(a);
        
    


public static void remove2(ArrayList<Integer> list) 
    Iterator<Integer> it = list.iterator();
    while(it.hasNext())
        if(it.next() <= 10) 
            it.remove();
        
    


public static void remove3(ArrayList<Integer> list) 
    Iterator<Integer> it = list.iterator();
    while(it.hasNext()) 
        it.remove();    
    


public static void remove4(ArrayList<Integer> list) 
    Iterator<Integer> it = list.iterator();
    while(it.hasNext()) 
        it.next();
        it.remove();
        it.remove();
    

解析:

remove1 方法会抛出 ConcurrentModificationException 异常,这是迭代器的一个陷阱,foreach 遍历编译后实质会替换为迭代器实现(普通for循环不会抛这个异常,因为list.size方法一般不会变,所以只会漏删除),因为迭代器内部会维护一些索引位置数据,要求在迭代过程中容器不能发生结构性变化(添加、插入、删除,修改数据不算),否则这些索引位置数据就失效了,避免的方式就是使用迭代器的 remove 方法。

remove2 方法可以正常运行,无任何错误。

remove3 方法会抛出 IllegalStateException 异常,因为使用迭代器的 remove 方法前必须先调用 next 方法,next 方法会检测容器是否发生了结构性变化,然后更新 cursor 和 lastRet 值,直接不调用 next 而 remove 会导致相关值不正确。

remove4 方法会抛出 IllegalStateException 异常,理由同 remove3,remove 调用一次后 lastRet 值会重置为 -1,没有调用 next 去设置 lastRet 的情况下再直接调一次 remove 自然就状态异常了。

当然了,上面四个写法的具体官方解答可参见 ArrayList 中迭代器部分源码,如下:

private class Itr implements Iterator<E> 
    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 hasNext() 
        return cursor != size;
    

    @SuppressWarnings("unchecked")
    public E next() 
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    

    public void remove() 
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try 
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
         catch (IndexOutOfBoundsException ex) 
            throw new ConcurrentModificationException();
        
    

    final void checkForComodification() 
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    

37.简要解释下面程序的执行现象和结果?

ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
Integer[] array1 = new Integer[3];
list.toArray(array1);
Integer[] array2 = list.toArray(new Integer[0]);
System.out.println(Arrays.equals(array1, array2));  //1 结果是什么?为什么?

Integer[] array = 1, 2, 3;
List<Integer> list = Arrays.asList(array);
list.add(4);    //2 结果是什么?为什么?

Integer[] array = 1, 2, 3;
List<Integer> list = new ArrayList<Integer>(Arrays.asList(array));
list.add(4);    //3 结果是什么?为什么?

解析:

1 输出为 true,因为 ArrayList 有两个方法可以返回数组Object[] toArray()<T> T[] toArray(T[] a),第一个方法返回的数组是通过 Arrays.copyOf 实现的,第二个方法如果参数数组长度足以容纳所有元素就使用参数数组,否则新建一个数组返回,所以结果为 true。

2 会抛出 UnsupportedOperationException 异常,因为 Arrays 的 asList 方法返回的是一个 Arrays 内部类的 ArrayList 对象,这个对象没有实现 add、remove 等方法,只实现了 set 等方法,所以通过 Arrays.asList 转换的列表不具备结构可变性。

3 当然可以正常运行咯,不可变结构的 Arrays 的 ArrayList 通过构造放入了真正的万能 ArrayList,自然就可以操作咯。

38.简单解释一下 Collection 和 Collections 的区别?

解析:

java.util.Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,在 Java 类库中有很多具体的实现,意义是为各种具体的集合提供最大化的统一操作方式。 譬如 Collection 的实现类有 List、Set 等,List 的实现类有 LinkedList、ArrayList、Vector 等,Vector 的实现类有 Stack 等,不过切记 Map 是自立门户的,其提供了转换为 Collection 的方法,但是自己不是 Collection 的子类。

java.util.Collections 是一个包装类,它包含有各种有关集合操作的静态多态方法,此类构造 private 不能实例化,就像一个工具类,服务于 Java 的 Collection 框架,其提供的方法大概可以分为对容器接口对象进行操作类(查找和替换、排序和调整顺序、添加和修改)和返回一个容器接口对象类(适配器将其他类型的数据转换为容器接口对象、装饰器修饰一个给定容器接口对象增加某种性质)。

39.解释一下 ArrayList、Vector、Stack、LinkedList 的实现和区别及特点和适用场景?

解析:

首先他们都是 List 家族的儿子,List 又是 Collection 的子接口,Collection 又是 Iterable 的子接口,所以他们都具备 Iterable 和 Collection 和 List 的基本特性。

ArrayList 是一个动态数组队列,随机访问效率高,随机插入、删除效率低。LinkedList 是一个双向链表,它也可以被当作堆栈、队列或双端队列进行操作,随机访问效率低,但随机插入、随机删除效率略好。Vector 是矢量队列,和 ArrayList 一样是一个动态数组,但是 Vector 是线程安全的。Stack 继承于 Vector,特性是先进后出(FILO, FirstIn Last Out)。

从线程安全角度看 Vector、Stack 是线程安全的,ArrayList、LinkedList 是非线程安全的。

从实现角度看 LinkedList 是双向链表结构,ArrayList、Vector、Stack 是内存数组结构。

从动态扩容角度看由于 ArrayList 和 Vector(Stack 继承自 Vector,只在 Vector 的基础上添加了几个 Stack 相关的方法,故之后不再对 Stack 做特别的说明)使用数组实现,当数组长度不够时,其内部会创建一个更大的数组,然后将原数组中的数据拷贝至新数组中,而 LinkedList 是双向链表结构,内存不用连续,所以用多少申请多少。

从效率方面来说 Vector、ArrayList、Stack 是基于数组实现的,是根据索引来访问元素,Vector(Stack)和 ArrayList 最大的区别就是 synchronization 同步的使用,抛开两个只在序列化过程中使用的方法不说,没有一个 ArrayList 的方法是同步的,相反,绝大多数 Vector(Stack)的方法法都是直接或者间接的同步的,因此就造成 ArrayList 比 Vector(Stack)更快些,不过在最新的 JVM 中,这两个类的速度差别是很小的,几乎可以忽略不计;而 LinkedList 是双向链表实现,根据索引访问元素时需要遍历寻找,性能略差。所以 ArrayList 适合大量随机访问,LinkList 适合频繁删除插入操作。

从差异角度看 LinkedList 还具备 Deque 双端队列的特性,其实现了 Deque 接口,Deque 继承自 Queue 队列接口,其实也挺好理解,因为 LinkedList 是的实现是双向链表结构,所以实现队列特性实在是太容易了。

40.简单介绍下 List 、Map、Set、Queue 的区别和关系?

解析:

List、Set、Queue 都继承自 Collection 接口,而 Map 则不是(继承自 Object),所以容器类有两个根接口,分别是 Collection 和 Map,Collection 表示单个元素的集合,Map 表示键值对的集合。

List 的主要特点就是有序性和元素可空性,他维护了元素的特定顺序,其主要实现类有 ArrayList 和 LinkList。ArrayList 底层由数组实现,允许元素随机访问,但是向 ArrayList 列表中间插入删除元素需要移位复制速度略慢;LinkList 底层由双向链表实现,适合频繁向列表中插入删除元素,随机访问需要遍历所以速度略慢,适合当做堆栈、队列、双向队列使用。

Set 的主要特性就是唯一性和元素可空性,存入 Set 的每个元素都必须唯一,加入 Set 的元素都必须确保对象的唯一性,Set 不保证维护元素的有序性,其主要实现类有 HashSet、LinkHashSet、TreeSet。HashSet 是为快速查找元素而设计,存入 HashSet 的元素必须定义 hashCode 方法,其实质可以理解为是 HashMap 的包装类,所以 HashSet 的值还具备可 null 性;LinkHashSet 具备 HashSet 的查找速度且通过链表保证了元素的插入顺序(实质为 HashSet 的子类),迭代时是有序的,同理存入 LinkHashSet 的元素必须定义 hashCode 方法;TreeSet 实质是 TreeMap 的包装类,所以 TreeSet 的值不备可 null 性,其保证了元素的有序性,底层为红黑树结构,存入 TreeSet 的元素必须实现 Comparable 接口;不过特别注意 EnumSet 的实现和 EnumMap 没有一点关系。

Queue 的主要特性就是队列和元素不可空性,其主要的实现类有 LinkedList、PriorityQueue。LinkedList 保证了按照元素的插入顺序进行操作;PriorityQueue 按照优先级进行插入抽取操作,元素可以通过实现 Comparable 接口来保证优先顺序。Deque 是 Queue 的子接口,表示更为通用的双端队列,有明确的在头或尾进行查看、添加和删除的方法,ArrayDeque 基于循环数组实现,效率更高一些。

Map 自立门户,但是也提供了嫁接到 Collection 相关方法,其主要特性就是维护键值对关联和查找特性,其主要实现类有 HashTab、HashMap、LinkedHashMap、TreeMap。HashTab 类似 HashMap,但是不允许键为 null 和值为 null,比 HashMap 慢,因为为同步操作;HashMap 是基于散列列表的实现,其键和值都可以为 null;LinkedHashMap 类似 HashMap,其键和值都可以为 null,其有序性为插入顺序或者最近最少使用的次序(LRU 算法的核心就是这个),之所以能有序,是因为每个元素还加入到了一个双向链表中;TreeMap 是基于红黑树算法实现的,查看键值对时会被排序,存入的元素必须实现 Comparable 接口,但是不允许键为 null,值可以为 null;如果键为枚举类型可以使用专门的实现类 EnumMap,它使用效率更高的数组实现。

从数据结构角度看集合的区别有如下:

动态数组:ArrayList 内部是动态数组,HashMap 内部的链表数组也是动态扩展的,ArrayDeque 和 PriorityQueue 内部也都是动态扩展的数组。

链表:LinkedList 是用双向链表实现的,HashMap 中映射到同一个链表数组的键值对是通过单向链表链接起来的,LinkedHashMap 中每个元素还加入到了一个双向链表中以维护插入或访问顺序。

哈希表:HashMap 是用哈希表实现的,HashSet, LinkedHashSet 和 LinkedHashMap 基于 HashMap,内部当然也是哈希表。

排序二叉树:TreeMap 是用红黑树(基于排序二叉树)实现的,TreeSet 内部使用 TreeMap,当然也是红黑树,红黑树能保持元素的顺序且综合性能很高。

堆:PriorityQueue 是用堆实现的,堆逻辑上是树,物理上是动态数组,堆可以高效地解决一些其他数据结构难以解决的问题。

循环数组:ArrayDeque 是用循环数组实现的,通过对头尾变量的维护,实现了高效的队列操作。

位向量:EnumSet 是用位向量实现的,对于只有两种状态且需要进行集合运算的数据使用位向量进行表示、位运算进行处理,精简且高效。

41.简单说说 HashMap 的底层原理?

答案:

当我们往 HashMap 中 put 元素时,先根据 key 的 hash 值得到这个元素在数组中的位置(即下标),然后把这个元素放到对应的位置中,如果这个元素所在的位子上已经存放有其他元素就在同一个位子上的元素以链表的形式存放,新加入的放在链头,从 HashMap 中 get 元素时先计算 key 的 hashcode,找到数组中对应位置的某一元素,然后通过 key 的 equals 方法在对应位置的链表中找到需要的元素,所以 HashMap 的数据结构是数组和链表的结合。

解析:

HashMap 底层是基于哈希表的 Map 接口的非同步实现,实际是一个链表散列数据结构(即数组和链表的结合体)。 首先由于数组存储区间是连续的,占用内存严重,故空间复杂度大,但二分查找时间复杂度小(O(1)),所以寻址容易,插入和删除困难。而链表存储区间离散,占用内存比较宽松,故空间复杂度小,但时间复杂度大(O(N)),所以寻址困难,插入和删除容易。 所以就产生了一种新的数据结构------哈希表,哈希表既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便,哈希表有多种不同的实现方法,HashMap 采用的是链表的数组实现方式,具体如下:

首先 HashMap 里面实现了一个静态内部类 Entry(key、value、next),HashMap 的基础就是一个 Entry[] 线性数组,Map 的内容都保存在 Entry[] 里面,而 HashMap 用的线性数组却是随机存储的原因如下:

// 存储时
int hash = key.hashCode(); //每个 key 的 hash 是一个固定的 int 值 
int index = hash % Entry[].length; 
Entry[index] = value;

// 取值时
int hash = key.hashCode(); 
int index = hash % Entry[].length; 
return Entry[index];

当我们通过 put 向 HashMap 添加多个元素时会遇到两个 key 通过hash % Entry[].length计算得到相同 index 的情况,这时具有相同 index 的元素就会被放在线性数组 index 位置,然后其 next 属性指向上个同 index 的 Entry 元素形成链表结构(譬如第一个键值对 A 进来,通过计算其 key 的 hash 得到的 index = 0,记做 Entry[0] = A,接着第二个键值对 B 进来,通过计算其 index 也等于 0,这时候 B.next = A, Entry[0] = B,如果又进来 C 且 index 也等于 0 则 C.next = B, Entry[0] = C)。 当我们通过 get 从 HashMap 获取元素时首先会定位到数组元素,接着再遍历该元素处的链表获取真实元素。 当 key 为 null 时 HashMap 特殊处理总是放在 Entry[] 数组的第一个元素。 HashMap 使用 Key 对象的 hashCode() 和 equals() 方法去决定 key-value 对的索引,当我们试着从 HashMap 中获取值的时候,这些方法也会被用到,所以 equals() 和 hashCode() 的实现应该遵循以下规则: 如果o1.equals(o2)o1.hashCode() == o2.hashCode()必须为 true,或者如果o1.hashCode() == o2.hashCode()则不意味着o1.equals(o2)会为true。

关于 HashMap 的 hash 函数算法巧妙之处可以参见本文链接:http://pengranxiang.iteye.com/blog/543893

42.简单解释一下 Comparable 和 Comparator 的区别和场景?

解析:

Comparable 对实现它的每个类的对象进行整体排序,这个接口需要类本身去实现,若一个类实现了 Comparable 接口,实现 Comparable 接口的类的对象的 List 列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序,此外实现 Comparable 接口的类的对象可以用作有序映射(如TreeMap)中的键或有序集合(如TreeSet)中的元素,而不需要指定比较器, 实现 Comparable 接口必须修改自身的类(即在自身类中实现接口中相应的方法),如果我们使用的类无法修改(如SDK中一个没有实现Comparable的类),我们又想排序,就得用到 Comparator 这个接口了(策略模式)。 所以如果你正在编写一个值类,它具有非常明显的内在排序关系,比如按字母顺序、按数值顺序或者按年代顺序,那你就应该坚决考虑实现 Comparable 这个接口, 若一个类实现了 Comparable 接口就意味着该类支持排序,而 Comparator 是比较器,我们若需要控制某个类的次序,可以建立一个该类的比较器来进行排序。 Comparable 比较固定,和一个具体类相绑定,而 Comparator 比较灵活,可以被用于各个需要比较功能的类使用。

43.简单说说 Iterator 和 ListIterator 的区别?

解析:

ListIterator 有 add() 方法,可以向 List 中添加对象,而 Iterator 不能。

ListIterator 和 Iterator 都有 hasNext() 和 next() 方法,可以实现顺序向后遍历,但是 ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向(顺序向前)遍历,Iterator 就不可以。

ListIterator 可以定位当前的索引位置,通过 nextIndex() 和 previousIndex() 可以实现,Iterator 没有此功能。

都可实现删除对象,但是 ListIterator 可以实现对象的修改,通过 set() 方法可以实现,Iierator 仅能遍历,不能修改。

容器类提供的迭代器都会在迭代中间进行结构性变化检测,如果容器发生了结构性变化,就会抛出 ConcurrentModificationException,所以不能在迭代中间直接调用容器类提供的 add、remove 方法,如需添加和删除,应调用迭代器的相关方法。

44.请实现一个极简 LRU 算法容器?

解析:

看起来是一道很难的题目,其实静下来你会发现想考察的其实就是 LRU 的原理和 LinkedHashMap 容器知识,当然,你要是厉害不依赖 LinkedHashMap 自己纯手写撸一个也不介意。 LinkedHashMap 支持插入顺序或者访问顺序,LRU 算法其实就要用到它访问顺序的特性,即对一个键执行 get、put 操作后其对应的键值对会移到链表末尾,所以最末尾的是最近访问的,最开始的最久没被访问的。 LRU 是一种流行的替换算法,它的全称是 Least Recently Used,最近最少使用,它的思路是最近刚被使用的很快再次被用的可能性最高,而最久没被访问的很快再次被用的可能性最低,所以被优先清理。 下面给出极简 LRU 缓存算法容器:

public class LRUCache<K, V> extends LinkedHashMap<K, V> 
    private int maxEntries;
    
    //maxEntries 最大缓存个数
    public LRUCache(int maxEntries)
        super(16, 0.75f, true);
        this.maxEntries = maxEntries;
    
    
    //在添加元素到 LinkedHashMap 后会调用这个方法,传递的参数是最久没被访问的键值对,如果这个方法返回 true 则这个最久的键值对就会被删除,LinkedHashMap 的实现总是返回 false,所有容量没有限制。
    @Override
    protected boolean removeEldestEntry(Entry<K, V> eldest) 
        return size() > maxEntries;
    
   

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

以上是关于list遍历陷阱分析原理的主要内容,如果未能解决你的问题,请参考以下文章

stl的erase()陷阱--迭代器失效总结

避坑手册 | JAVA编码中容易踩坑的十大陷阱

iterator迭代器遍历hashmap的使用陷阱

java中DelayQueue的一个使用陷阱分析

Linux共享内存使用常见陷阱与分析

List中remove()方法的陷阱