实习面试准备——List
寒假一直在家学习Java基础知识、数据结构与算法、多线程、Redis、MySQL优化等,准备开学后投出实习简历,下面是我的实习准备(根据虎牙校招的面经来总结的)
1.Java集合List详解
集合框架:
在Collection中,List集合是有序的,可对其中每个元素的插入位置进行精确地控制,可以通过索引来访问元素,遍历元素。在List集合中,我们常用到ArrayList和LinkedList这两个类。
在List集合中,常用到ArrayList和LinkedList
ArrayList底层通过数组实现,随着元素的增加而动态扩容
LinkedList底层通过链表来实现,随着元素的增加不断向链表的后端增加节点。
ArrayList:
它继承于AbstractList,实现了List, RandomAccess, Cloneable, Serializable接口。
(1)ArrayList实现List,得到了List集合框架基础功能;
(2)ArrayList实现RandomAccess,获得了快速随机访问存储元素的功能,RandomAccess是一个标记接口,没有任何方法;
(3)ArrayList实现Cloneable,得到了clone()方法,可以实现克隆功能;
(4)ArrayList实现Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是hessian协议。
特点:
(1)容量不固定,随着容量的增加而动态扩容(阈值基本不会达到)
(2)有序集合(插入的顺序==输出的顺序)
(3)插入的元素可以为null
(4)随机访问的效率高,增删改效率较低(相对于LinkedList来说)
(5)线程不安全
LinkedList
它继承AbstractSequentialList,实现了List, Deque, Cloneable, Serializable接口。
(1)LinkedList实现List,得到了List集合框架基础功能;
(2)LinkedList实现Deque,Deque 是一个双向队列,也就是既可以先入先出,又可以先入后出,说简单些就是既可以在头部添加元素,也可以在尾部添加元素;
(3)LinkedList实现Cloneable,得到了clone()方法,可以实现克隆功能;
(4)LinkedList实现Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是hessian协议。
特点:
(1)是一个双向链表,每一个节点都拥有指向前后节点的引用
(2)相比于ArrayList来说,LinkedList的随机访问效率更低。
List接口中的常用方法
A:添加功能 boolean add(E e):向集合中添加一个元素 void add(int index, E element):在指定位置添加元素 boolean addAll(Collection<? extends E> c):向集合中添加一个集合的元素。 B:删除功能 void clear():删除集合中的所有元素 E remove(int index):根据指定索引删除元素,并把删除的元素返回 boolean remove(Object o):从集合中删除指定的元素 boolean removeAll(Collection<?> c):从集合中删除一个指定的集合元素。 C:修改功能 E set(int index, E element):把指定索引位置的元素修改为指定的值,返回修改前的值。 D:获取功能 E get(int index):获取指定位置的元素 Iterator iterator():就是用来获取集合中每一个元素。 E:判断功能 boolean isEmpty():判断集合是否为空。 boolean contains(Object o):判断集合中是否存在指定的元素。 boolean containsAll(Collection<?> c):判断集合中是否存在指定的一个集合中的元素。 F:长度功能 int size():获取集合中的元素个数 G:把集合转换成数组 Object[] toArray():把集合变成数组。
ArrayList具体实现List接口方法就不一一列出了,因为JDK源码里面已经非常细致和完整
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { //实现Serializable接口,生成的序列版本号: private static final long serialVersionUID = 8683452581122892189L; //ArrayList初始容量大小:在无参构造中不使用了 private static final int DEFAULT_CAPACITY = 10; //空数组对象:初始化中默认赋值给elementData private static final Object[] EMPTY_ELEMENTDATA = {}; //空数组对象 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList中实际存储元素的数组,非私有是为了简化嵌套类的访问 transient Object[] elementData; //集合实际存储元素长度: private int size; //ArrayList有参构造:容量大小 public ArrayList(int initialCapacity) { //initialCapacity大于0,初始化一个大小为initialCapacity的Object数组 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //initialCapacity等于0,初始化一个空数组 this.elementData = EMPTY_ELEMENTDATA; } else { //否则,抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } //ArrayList无参构造 public ArrayList() { //初始化数组:空数组,容量为0 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //ArrayList有参构造:Java集合 public ArrayList(Collection<? extends E> c) { //将集合转换为数组 elementData = c.toArray(); if ((size = elementData.length) != 0) { //如果elementData的长度不为0 if (elementData.getClass() != Object[].class) // 官方的说法:c.toArray might (incorrectly) not return Object[] (see 6260652) // 这是因为实现 Collection接口的类可能会重写toArray()方法,可能返回的就不是Object 数组了 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 否则,初始化一个空数组 this.elementData = EMPTY_ELEMENTDATA; } }
用于添加元素—add()方法:
//往ArrayList添加元素 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! //索引后移,将对应角标下的元素赋值为e: elementData[size++] = e; return true; } //得到最小扩容量 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } //扩展时modCount值改变 private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code //如果扩展的容量比原有容量大,才扩展。 if (minCapacity - elementData.length > 0) grow(minCapacity); } private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //如果数组为空,数组的扩容量为10 return Math.max(DEFAULT_CAPACITY, minCapacity); } //如果数组非空,数组的扩容量为size+1 return minCapacity; } private void grow(int minCapacity) { int oldCapacity = elementData.length;//原来数组的长度 int newCapacity = oldCapacity + (oldCapacity >> 1);//oldCapacity >> 1,右移1位,即除以了2,即newCapacity =1.5*oldCapacity if (newCapacity - minCapacity < 0)//如果想要扩展的容量比新容量大,那么将新容量置为作者要求的大小。 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0)//如果想要扩展的容量大于Integer.MAX_VALUE - 8,则取最大容量值和指定值之间较小的一个作为新容量 newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity);//调用Arrays.copyOf()方法,该方法的底层就是数组的拷贝,实现数组扩容 }
用于删除元素:
remove(int index)是针对于角标来进行删除,不需要去遍历整个集合,效率更高;
remove(Object o)是针对于对象来进行删除,需要遍历整个集合进行equals()方法比对,所以效率较低;
无论是哪种形式的删除,最终都会调用System.arraycopy()方法进行数组复制操作,所以效率都会受到影响;
//在ArrayList的移除index位置的元素 public E remove(int index) { //检查角标是否合法:不合法抛异常 rangeCheck(index); //AbstractList中的成员变量protected transient int modCount = 0,操作数+1: modCount++; //获取当前角标的value: E oldValue = elementData(index); //获取需要删除元素 到最后一个元素的长度,也就是删除元素后,后续元素移动的个数; int numMoved = size - index - 1; //如果移动元素个数大于0 ,也就是说删除的不是最后一个元素 if (numMoved > 0) // 将elementData数组index+1位置开始拷贝到elementData从index开始的空间 System.arraycopy(elementData, index+1, elementData, index, numMoved); //size减1,并将最后一个元素置为null, elementData[--size] = null; // GC,垃圾收集 //返回被删除的元素 return oldValue; } //在ArrayList的移除对象为O的元素,不返回被删除的元素: public boolean remove(Object o) { //如果o==null,则遍历集合,判断哪个元素为null: if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { //快速删除,和前面的remove(index)一样的逻辑 fastRemove(index); return true; } } else { //同理: for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } //快速删除: private void fastRemove(int index) { //操作数+1 modCount++; //获取需要删除元素 到最后一个元素的长度,也就是删除元素后,后续元素移动的个数; int numMoved = size - index - 1; //如果移动元素个数大于0 ,也就是说删除的不是最后一个元素: if (numMoved > 0) // 将elementData数组index+1位置开始拷贝到elementData从index开始的空间 System.arraycopy(elementData, index+1, elementData, index,numMoved); //size减1,并将最后一个元素置为null elementData[--size] = null; // clear to let GC do its work }
用于改变ArrayList某个位置的值
set(int index, E element)方法,由于ArrayList实现了RandomAccess,所以具备了随机访问特性,调用elementData()可以获取到对应元素的值
//设置index位置的元素值为element,返回该位置的原来的值 public E set(int index, E element) { //检查index是否合法:判断index是否大于size,否则抛出IndexOutOfBoundsException异常 rangeCheck(index); //获取该index原来的元素 E oldValue = elementData(index); //替换成新的元素 elementData[index] = element; //返回旧的元素 return oldValue; }
获取index位置上的元素
get(int index)方法
public E get(int index) { //检查index是否合法: rangeCheck(index); //获取数组index位置的元素:返回时类型转换 return elementData(index); } //获取数组index位置的元素:返回时类型转换 @SuppressWarnings("unchecked") E elementData(int index) { return (E) elementData[index]; }
上面的源码中出现了挺多次modCount,下面让我们来看一下modCount的含义:
首先:modCount是AbstractList中的成员变量
AbstractList.java
protected transient int modCount = 0;
ArrayList获取迭代器对象:
ArrayList.java
//返回一个Iterator对象,Itr为ArrayList的一个内部类,其实现了Iterator<E>接口 public Iterator<E> iterator() { return new Itr(); }
下面看一下Itr类:
Itr实现了Iterator接口,是ArrayList集合的迭代器对象
private class Itr implements Iterator<E> { //类似游标,指向迭代器下一个值的位置 int cursor = 0; //迭代器最后一次取出的元素的位置 int lastRet = -1; //Itr初始化时候ArrayList的modCount的值。 int expectedModCount = modCount; //利用游标,与数组长度size的比较,判断迭代器是否还有下一个元素 public boolean hasNext() { return cursor != size(); } //迭代器获取下一个元素: public E next() { //检查modCount是否改变: checkForComodification(); try { int i = cursor; E next = get(i); lastRet = i; cursor = i + 1; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public void remove() { if (lastRet < 0) throw new IllegalStateException(); //检查modCount是否改变:防止并发操作集合 checkForComodification(); try { //删除这个元素: AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; //重置expectedModCount: expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } //并发检查: final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
使用modCount变量其实是一种Fail-Fast机制,这种机制的详解见这篇博客:
java集合中常见异常fail-fast(面试常问)
transient修饰符
当我们序列化对象时,如果对象中某个属性不进行序列化操作,那么在该属性前添加transient修饰符即可实现;例如:
transient Object[] elementData;
为什么ArrayList不想对elementData属性进行序列化呢?elementData可是集合中保存元素的数组啊,如果不序列化elementData属性,那么在反序列化时候,岂不是丢失了原先的元素?
ArrayList在添加元素时,可能会对elementData数组进行扩容操作,而扩容后的数组可能并没有全部保存元素。
例如:我们创建了new Object[10]数组对象,但是我们只向其中添加了1个元素,而剩余的9个位置并没有添加元素。当我们进行序列化时,并不会只序列化其中一个元素,而是将整个数组进行序列化操作,那些没有被元素填充的位置也进行了序列化操作,间接的浪费了磁盘的空间,以及程序的性能。
所以,ArrayList才会在elementData属性前加上transient修饰符。
ArrayList的writeObject()、readObject():
ArrayList在序列化时会调用writeObject(),直接将elementData写入ObjectOutputStream;
而反序列化时则调用readObject(),从ObjectInputStream获取elementData;
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ int expectedModCount = modCount; s.defaultWriteObject(); s.writeInt(size); for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; s.defaultReadObject(); s.readInt(); if (size > 0) { int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); ensureCapacityInternal(size); Object[] a = elementData; for (int i=0; i<size; i++) { a[i] = s.readObject(); } } }
Arrays.copyOf()
该方法在内部创建了一个新数组,底层实现是调用System.arraycopy();
original - 要复制的数组
newLength - 要返回的副本的长度
newType - 要返回的副本的类型
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
System.arraycopy()
该方法是用了native关键字,调用的为C++编写的底层函数.
/** *src - 源数组。 *srcPos - 源数组中的起始位置。 *dest - 目标数组。 *destPos - 目标数据中的起始位置。 *length - 要复制的数组元素的数量。 */ public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);