list遍历陷阱分析原理

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

解析:javascript

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

//确保容量够用,内部会尝试扩容,若是须要
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 结果是什么?为何?

解析:ide

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; } } 

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

相关文章
相关标签/搜索