本文使用Java 8+
目录
- 前言
- 第一章 集合与数组概念
- 第二章 集合框架
- 1、集合的两大类
- 2、集合创建
- 3、集合为何需要泛型
- 第三章 List接口
- 1、List接口主要实现类
- 2、迭代器
- 2.1 为什么要迭代器
- 2.2 什么是迭代器
- 2.3 使用迭代器注意事项
- 2.4 迭代器遍历的三种方法
- 3、ListIterator是什么
- 第四章 List接口常见面试题
- 第五章 ArrayList源码剖析
- 1、核心属性源码:
- 2、构造方法(验证核心属性)
- 3、ArrayList的扩容机制
- 4、ArrayList扩容的核心方法。
- 5、add方法(重点)
- 当添加元素时容量长度小于默认容量的长度10时,源码解析
- 当容量长度大于默认容量的长度10时,源码解析
以往想要存储多个对象,我们想到用StringBuffered、数组来存储多个对象。但是在写程序的时候我们并不知道将需要多少个对象,或者是否需要更复杂的方式来存储对象,结果发展数组尺寸是固定的,这也太过于受限了吧,一点都不灵活,太难了,数组能开放点吗?
随后Java团队为了解决数组长度不可变,就提供了这么一个集合(Collection),其中基本的类型有 List 、 Set 、 Queue 和 Map。用Java集合类都可以自动地调整自己的大小。简直就要飞天了吧。因此,与数组不同是,在编程时,可以将任意数量的对象放置在集合中,而不用关心集合应该有多大。
小结:数组和集合区别
长度区别:数组长度固定;集合的长度可变(灵活易扩展)
存储内容区别:数组存储的是同一种类型的元素;集合可以存储不同类型的元素(但我们开发者考虑安全问题一般使用范型)
存储数据的类型区别:数组可以存储基本数据类型,也可以存储引用类型;集合只能存储引用类型(如存储int,它会自动装箱成Integer)
集合按照其存储结构可以分为两大类,分别是:
单列集合java.util.Collection
,collection是单列集合类的根接口,它还有两个子接口分别是List
和set
接口。
List
接口的特点:元素有序、元素可重复。(主要实现类有ArrayList、LinkedList、vector)Set
接口的特点:元素无序,且不可重复。(主要实现类有HashSet、TreeSet、LinkedHashSet)双列集合java.util.Map
集合本身是一个工具,它存放在java.util包中。
List<Apple> apples = new ArrayList<>();
List<String> s = new ArrayList<>();
注意: ArrayList 已经被向上转型为了 List ,使用接口的目的是,如果想要改变具体实现,只需在创建时修改它就行了,就像下面这样:
List<Apple> apples = new LinkedList<>();
注意:向上转型并非总是有效,因为某些具体类有额外的功能,就比如 LinkedList 具有 List 接口中未包含的额外方法,因此,就不能将它们向上转型为更通用的接口。
使用 Java 5 之前的集合的一个主要问题是编译器允许你向集合中插入不正确的类型,早期的Java使用Object来代表任意类型的,但是向下转型有强转的问题,这样就不太好了,程序不太安全。
Java泛型设计原则:在编译期防止将错误类型的对象放置到集合中,运行时期就不会出现ClassCastException异常。
细节:在 Java 7 之前,必须要在两端都进行类型声明
ArrayList apples = new ArrayList();
泛型小结:有了泛型,代码就会更加简洁不用强制转换、程序更加健壮、可读性和稳定性。
List 接口在 Collection 的基础上添加了许多方法,允许在 List 的中间插入和删除元素。
我们都知道集合无非就是存储
和获取
的这样一个过程。毕竟,保存事物是集合最基本的工作,就比如List,插入是add()
获取是get()
,那么会发现要使用集合,必须对集合的确切类型编程。突然有一天,如果原本是对 List 编码的,但是后来发现如果能够将相同的代码应用于 Set 会更方便,此时应该怎么做?想到可能编写一段通用代码,但是它是用于不同类型的集合,那么如何才能不重写代码就可以应用于不同类型的集合?这时就使用迭代器。
迭代器(也是一种设计模式)的概念实现了这种抽象。迭代器是一个对象,它的工作就是遍历并选择序列中的每个对象,也就是提供了一种访问容器对象中的各个元素,并遍历出来。程序员不需要关心容器底层结构,就可以完美对容器进行遍历。
(1)使用 iterator()
方法要求集合返回一个 Iterator。 Iterator 将准备好返回序列中的第一个元素。
(2)使用 next()
方法获得序列中的下一个元素。
(3)使用 hasNext()
方法检查序列中是否还有元素。
(4)使用 remove()
方法将迭代器最近返回的那个元素删除。
List<String> list = new ArrayList<>();
list.add("1、kobe");
list.add("2、詹姆斯");
list.add("3、字母哥");
list.add("4、浓眉");
list.add("5、乔丹");
第一种:for循环遍历(推荐)
for (Iterator it = list.iterator();it.hasNext();) {
System.out.println(it.next());
}
第二种:声明周期就在大括号内(高并发推荐)
List<String> list = new ArrayList<>();
list.add("1、kobe");
list.add("2、詹姆斯");
{
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
第三种:通过迭代器遍历
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
ListIterator是继承自Iterator
ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。
ListIterator迭代器包含的方法有:
add(E e): 将指定的元素插入列表,插入位置为迭代器当前位置之前
hasNext():以正向遍历列表
hasPrevious():如果以逆向遍历列表,列表迭代器前面还有元素,则返回 true,否则返回false
next():返回列表中ListIterator指向位置后面的元素
nextIndex():返回列表中ListIterator所需位置后面元素的索引
set(E e):从列表中将next()或previous()返回的最后一个元素返回的最后一个元素更改为指定元素e
previous():返回列表中ListIterator指向位置前面的元素
previousIndex():返回列表中ListIterator所需位置前面元素的索引
代码演示:
List<String> list = new ArrayList<>();
list.add("kobe");
list.add("詹姆斯");
list.add("字母哥");
list.add("浓眉");
list.add("乔丹");
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
System.out.println(listIterator.next() + "," + listIterator.nextIndex());
}
最终输出:
kobe, 1
詹姆斯,2
字母哥,3
浓眉, 4
乔丹, 5
说一下List、Set、Map三者的区别?
* List是有序可重复
* Set是无序不可重复
* Map它是采用键值对来存储,两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
Arraylist 与 LinkedList 区别?
1. Arraylist底层使用的是数组;LinkedList底层使用的是双向链表,数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。(面试官有可能让你聊聊什么单链表和双链表)
2. Arraylist擅长随机访问元素,快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。LinkedList`随机访问来说相对较慢,但它具有比ArrayList更大的特征集。
3. ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。比如:执行add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
4. LinkedList采用链表存储,所以对于`add(E e)`方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置`i`插入和删除元素的话((add(int index, E element)`) 时间复杂度近似为`o(n))`因为需要先移动到指定位置再插入。
补充单链表、双链表、双向循环链表
单链表:一个节点指向下一个节点(操作链表要时刻记住的是:节点中指针域指向的就是一个节点了!)
双向链表:包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。
双向循环链表:最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。
推荐阅读:看图轻松理解数据结构与算法系列(双向链表)
说一下ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
Arraylist不是同步的,所以在不需要保证线程安全时建议使用Arraylist。
ArrayList的大小是如何自动增加的?
1、添加元素时,首先进行判断是否大于默认容量10
2、如果,小于默认容量,直接在原来基础上+1,元素添加完毕
3、如果,大于默认容量,则需要进行扩容,扩容核心是grow()方法
3.1 扩容之前,首先创建一个新的数组,且旧数组被复制到新的数组中
这样就得到了一个全新的副本,我们在操作时就不会影响原来数组了
3.2 然后通过位运算符将新的容量更新为旧容量的1.5陪(原来长度的一半再加上原长度也就是每次扩容是原来的1.5倍)
3.3 如果新的容量-旧的容量<=0,就拿新的容量-最大容量长度如果<=0的,那么最终容量就是扩容后的容量
什么情况下你会使用ArrayList?什么时候你会选择LinkedList?
1. 当你遇到访问元素比插入或者是删除元素更加频繁的时候,你应该使用ArrayList
2. 插入或者是删除元素更加频繁,或者你压根就不需要访问元素的时候,你会选择LinkedList
由上面问题,引入下一个问题:ArrayList插入和删除效率为什么这么低?
因为在ArrayList中增加或者删除某个元素,通常会调用System.arraycopy方法,这是一种极为消耗资源的操作,因此,在频繁的插入或者是删除元素的情况下,LinkedList的性能会更加好一点。
//ArrayList初始容量为10
private static final int DEFAULT_CAPACITY = 10;
//用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//用于默认大小空实例的共享空数组实例。
//我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//保存ArrayList数据的数组
transient Object[] elementData;
//该属性设置容量大小,ArrayList 所包含的元素个数
private int size;
//1、构造具有指定初始容量的空列表
public ArrayList(int initialCapacity) {
//如果指定容量大于0,那么数组就进行初始化成对应的容量
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果初始容量为0,它会默认给定一个空的数组(EMPTY_ELEMENTDATA)
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//2、构造一个初始容量为10的空列表。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//3、按照集合的迭代器返回的顺序构造一个包含指定集合元素的列表。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
//在使用ensureCapacity操作添加大量元素之前,应用程序可以增加ArrayList实例的容量。
//这可能会减少增量重新分配的数量。
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}
//oldCapacity旧容量,newCapacity新容量
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//将新容量更新为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE)
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE;
}
//返回此列表中的元素数。
public int size() { return size; }
具体实现步骤分为:首先判断是否需要扩容、再插入元素
//1、添加元素,e获取你添加的元素
public boolean add(E e) {
modCount++;//在之前的元素自增
add(e, elementData, size);
return true;
}
//2、判断是否需要扩容
private void add(E e, Object[] elementData, int s) {
//判断s长度是否等于默认容量长度10
//如果大于默认容量10,则走扩容elementData = grow();
//如果小于默认容量10,则走直接添加elementData[s] = e;
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
//小于默认容量长度10,则size等于原来的+1
size = s + 1;
}
//3、最终返回size = s + 1;
public boolean add(E e) {
modCount++;
add(e, elementData, size);
//最终的返回
return true;
}
//1、添加元素
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
//2、判断是否需要扩容
private void add(E e, Object[] elementData, int s) {
//判断s长度是否等于默认容量长度10
//如果大于默认容量10,则走扩容elementData = grow();
//如果小于默认容量10,则走直接添加elementData[s] = e;
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
//3、默认容量+1
private Object[] grow() {
return grow(size + 1);
}
//4、minCapacity=默认容量size+1
private Object[] grow(int minCapacity) {
//要复制的数组;拷贝的新数组长度
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));//进行扩容了
}
//5、实现扩容
//用来确定是否需要容量,最后调用grow()来扩容
//假设当添加11个元素时,minCapacity=11,而默认数组为10,此时需要进行扩容
private int newCapacity(int minCapacity) {
// overflow-conscious code
//5.1 旧容量10
//扩容1.5倍
int oldCapacity = elementData.length;
//5.2新的容量=旧容量+(oldCapacity >> 1)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//5.3判断,如果新的容量-最小容量<=0(也就是扩容后的容量-minCapacity)
//minCapacity(最小容量=默认的容量10+你存入元素得到的长度)
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
//5.4新的容量减去MAX_ARRAY_SIZE <= 0的话,就等于扩容后的长度
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
//6、得到最小容量(就是假如你存入长度为11,则最终minCapacity=11的意思)
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
//7、newLength扩容后的长度、original原始的
@SuppressWarnings("unchecked")
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
//8、数组的拷贝复制
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);
//Math.min进行计算
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
//9、a=默认容量10,b=扩容后的容量
public static int min(int a, int b) {
return (a <= b) ? a : b;
}
//10、再回到System.arraycopy(original, 0, copy, 0,
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;
}
//11、复制结束return (T[]) copyOf(original, newLength,original.getClass());
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
//12、grow()方法扩容得到最终的长度
private Object[] grow() {
return grow(size + 1);
}
//13、
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
//13.1 elementData = grow();
elementData = grow();
//13.2 e是存入的元素
elementData[s] = e;
//13.3最终长度为size = s + 1;
size = s + 1;
}
//14、结束
public boolean add(E e) {
modCount++;
add(e, elementData, size);
//返回
return true;
}
小结:
1、添加元素时,首先进行判断是否大于默认容量10
2、如果,小于默认容量,直接在原来基础上+1,元素添加完毕
3、如果,大于默认容量,则需要进行扩容,扩容核心是grow()方法
3.1 扩容之前,首先创建一个新的数组,且旧数组被复制到新的数组中
这样就得到了一个全新的副本,我们在操作时就不会影响原来数组了
3.2 然后通过位运算符将新的容量更新为旧容量的1.5陪
3.3 如果新的容量-旧的容量<=0,就拿新的容量-最大容量长度如果<=0的,那么最终容量就是扩容后的容量
总结:添加(add)方法的实现,添加时首先检查数组的容量是否满足
本篇文章如有错的地方,欢迎在评论指正。喜欢在微信看技术文章,可以微信搜索「MarkerJava」,回复【Java】【大数据】【Spring全家桶】【电子书籍】即可获得精品全套视频,还有更多资料,建议后台留言或者直接私信我。
另,如果觉得这本篇文章写得不错,有点东西的话,各位人才记得来个三连【点赞+关注+分享】。