List接口常用实现类有ArrayList、LinkedList。
ArrayList
内部有一个Object数组用于存储元素。假如是无参构造器生成的ArrayList实例,则一开始数组是个空数组。在第一次add时,会扩容至10。假如创建实例时指定了初始容量,则扩容时会在初始容量的基础上扩容。扩容规则是:int newCapacity = oldCapacity + (oldCapacity >> 1),即扩容至原来的1.5倍,不是整数就向下取整。这个问题在面试中信银行信用卡中心时有被问过。
ArrayList有个int型的size变量,父类AbstractList有个int型的modCount变量。add方法会使得size加1,modCount加1。remove方法会使得size减1,modCount加1。
remove方法需要详细讲下:每次remove都有数组拷贝,如果删除的不是最后一个元素的话。System.arrayCopy方法。
E remove(int index):把数组指定位置之后的数据都往前拷贝一个位移,原来最后面的位置赋值为null。
boolean remove(Object o):找到第一个o,然后把该位置之后的数据都往前拷贝一个位移,原来最后面的位置赋值为null。寻找o索引实现:
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; }
如果o是null,则遍历数组,找到第一个null。如果o不是null,则遍历数组,找到第一个o.equals(elementData[index])返回为true的。
int indexOf():和remove方法根据元素找索引的代码是一样的。需要遍历数组。
public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; }
boolean contains(Object o):内部是调用indexOf(o)方法,也需要遍历数组。
public boolean contains(Object o) { return indexOf(o) >= 0; }
iterator():返回的Iterator的next()方法内部也是判断了当前修改次数是否等于预期修改次数,如果不相等,会抛ConcurrentModificationException异常。就跟在遍历map的迭代器时修改map会抛并发修改异常是一样样的。
LinkedList
内部数据结构是链表,链表元素是Node实例。Node是LinkedList的内部类,有三个成员变量:E item、Node<E> next、Node<E> prev,只有一个全参构造器Node(Node<E> prev, E element, Node<E> next)。
LinkedList有一个Node类型的first变量,标识第一个元素,有一个Node类型的last变量,标识最后一个元素。同ArrayList一样,也有一个int类型的size变量,标识当前元素个数。
add方法会在链表的最后添加一个Node实例,把last变量的的next属性赋值为当前add的Node实例,当前add的Node实例的prev属性赋值为last。
remove()、removeFirst()都是去掉第一个元素。
E remove(int index):根据索引index如何找到对应Node元素呢?调用的是node(index)方法,实现是:
Node<E> node(int index) { if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
即先判断index和size/2谁大,如果index小于size/2,则从前往后找,这样比较快,否则从后往前找。从前往后找的话,根据first.next.next.next。。。next操作index-1次,就找到了。如果从后往前找,则根据last.prev.prev。。。prev操作size-1-index次,就找到了。找到后,调用unlink方法。对比ArrayList的remove(int index)方法,多了遍历操作,少了数组copy操作。
boolean remove(Object o):根据o如何找到对应Node元素呢?从前往后遍历,如果o为null,则找第一个item为null的Node实例,如果o不为null,则找第一个o.equals item返回为true的Node实例。找到后,调用unlink方法。
get(int index):内部调用node(index)方法。
contains(Object o):内部还是调用indexOf(o)方法,从前往后遍历链表,就像remove(o)一样。
size():返回size变量值
clear():从first开始,利用next往下遍历,设置每个Node实例的item为null,prev为null,next为null,并把size置为0,modCount加1。
Set接口常用实现类有HashSet、LinkedHashSet、TreeSet。
HashSet
内部有一个HashMap类型的变量map。在执行HashSet的构造方法时,map初始化为一个新的HashMap实例。可以选择调用HashSet的有参构造器,这样会把初始容量、负载因子透传给map。如果不传这些的话,则map的初始容量为16,负载因子为0.75。
add():内部调用map的put()方法,put的key是add方法的入参,put的value是一个固定的Object实例。此实例是一个Object类型的静态常量,在类加载的时候赋值。
add方法是如何去重的呢?
从内部实现上可以看出,add判断两个元素是否一样,是依赖map的put方法的(所以如果Set中要放自定义类型元素的话,自定义类型必须重写hashCode方法和equals方法),如果add的元素一样,则put方法中只会用新value值替换旧value值,不会增加一个Node节点。
boolean contains(Object o):内部是调用map的containsKey(key)方法。会根据key的hash值和key去找对应的Node实例,时间复杂度是O(1),不像ArrayList那样要遍历数组,也不像LinkedList那样要遍历链表。
size():内部调用map的size()方法。
clear():内部调用map的clear()方法。
iterator():内部调用map.keySet().iterator()。
LinkedHashSet
是如何维护元素顺序的呢?LinkedHashSet内部有一个LinkedHashMap实例。在构造LinkedHashSet实例时,先构造父类HashSet实例,这时候HashMap类型的变量map会赋值为一个LinkedHashMap实例。LinkedHashMap可维护Node顺序,取各Node实例的key属性值,即LinkedHashSet的各元素,故也是有序的。
TreeSet
内部有一个NavigableMap类型的变量,在构造TreeSet时会被赋值为一个TreeMap实例。
add方法同样是调用TreeMap的put方法,put的key是add的入参,put的value仍是一个Object类型的静态常量。
itetator()方法同样是调用TreeMap实例的keySet的iterator()方法。