Java基础 -- 容器(List)
Java基础 -- 容器(List)
集合的总结构继承图
集合分为两个大的板块:一个是:单列集合 一个是:双列集合


List集合

Collection接口是集合的顶级接口,List接口也是Collection的一个子接口,在Collection接口中定义了一些方法用于对集合的基本的操作,List接口对其进行了继承,同时List接口也额外的添加了自己的方法(因为List集合的特点:有序、可重复,有序就意味着有下标索引而在Collecton集合的基本方法中比,诶呦对索引进行操作的方法,索引List集合就对其进行了基本的实现)。
Collection接口的基本方法:
| Collection接口的基本方法 | 说明 |
int size(); |
用于返回int类型的数值,代表该集合中元素的个数 |
boolean isEmpty(); |
返回的是boolean类型,用于判断该集合是否为空。 |
boolean contains(Object o); |
返回boolean类型,用于判断该集合中是否有所指定的元素。 |
Iterator<E> iterator(); |
迭代器 |
Object[] toArray(); |
将集合转换为Object类型的数组。 |
boolean add(E e); |
add()方法要揪其原理,就是往集合中添加元素。 |
boolean remove(Object o); |
remove()方法要揪其原理,就是从集合中删除该元素。 |
boolean containsAll(Collection<?> c); |
返回的是boolean类型的数据,就是用于判断是否在当前集合中包含所指定的集合中的所有元素。 |
boolean addAll(Collection<? extends E> c); |
添加指定集合中的所有元素至当前集合中。 |
boolean removeAll(Collection<?> c); |
在该集合中,移除所指定的集合中的所有的元素。 |
| 函数式编程 | 还未涉及 |
首先,在学习List接口的具体实现类之前,先了解List接口的特点:有序、可重复。所谓的有序,就是有下标索引,可以重复,就是在该集合中可以存放内容相同的数据,下标不同位置不同,内容相同,就可以存放在该集合中。在平时使用中List接口使用的也是非常的多,下面就具体的学习其中的实现类:
ArrayList接口实现类的学习
该接口中的基本方法除了继承了Collection接口中的方法外,还有自己的相关索引的方法:
| List接口新添加的方法 | 说明 |
boolean addAll(int index, Collection<? extends E> c); |
添加所指定的集合中的所有的元素,在index索引位置处。 |
E get(int index); |
get(index),就是在集合中得到下标索引为index所指定的元素。 |
E set(int index, E element); |
set(index,E element)方法就是在index索引的位置,设置所指定的元素。 |
void add(int index, E element); |
add(index,E element);方法就是在index索引位置处,添加所指定的元素。 |
E remove(int index); |
删除index索引处的元素。 |
int indexOf(Object o); |
在集合中得到所指定元素第一次出现的索引位置。 |
int lastIndexOf(Object o); |
在集合中得到所指定元素随后一次出现的索引位置。 |
| List<E> subList(int fromIndex, int toIndex); | 得到一个子的集合,根据fromIndex和toIndex来确定索引的开始和结束。 |
这些方法都是很简单的,不做演示,是需要对add()方法和remove()方法进行演示与研究其原理。
添加方法add()会发现有这几种:add(E element)、add(int index,E element)、addAll(int index , Collection c);现在主要研究add(E element)方法,其余的都是一个原理。
add(E element)方法
import java.util.ArrayList; import java.util.List; public class Demone7 { public static void main(String[] args) { //实例化一个ArrayList List<String> list = new ArrayList<>(); list.add("zhangsan"); list.add("lisi"); list.add("wangwu"); list.add("zhangsan"); for (String str : list) { System.out.println(str); } } }

验证了List集合的可重复性。
add(E element)方法底层源码分析:
在这里首先要说明一个问题,jdk7与jdk8在源码上是有区别的,在这里的add方法也是采用了不同的机制。因为现在很多都在使用的是jdk8,所以我先介绍jdk8的源码情况,再去介绍jdk7,并说明其区别。
ArrayList底层是一个数组结构,采用的扩容机制,与一般的数组是不一样的,也可以认为ArrayList集合是一个加强版的数组。为什么会这样说?因为一般的数组的缺点就是:数组在创建的时候是必须要初始化大小的,并且是大小固定的,不可变,若是出现存储的元素的个数大于数组的存储空间就会报异常。
在ArrayList底层其实也是一个数组,不过是一个可以自动扩展容量的数组。同时在JDK8中在实例化ArrayList时,使用的机制是延迟加载,也就是说实例化后,是一个空数组,没有占用内存,用的时候再去创建内存,这点实现了对内存的最佳管理。
部分源码如下:
private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private int size; public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
源码的执行流程:
1、实例化一个ArrayList接口实现类:
//实例化接口实现类 List<String> list = new ArrayList<>();
2、实例化时,调用的是无参的构造方法
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
会发现,将一个空的数组,赋值给了存储元素的数组,使其成为空的数组。这也就是,我说的ArrayList的特点:延迟加载创建,对内容的使用更佳。
| 具体实例:现在的存储空间是null。 |
3、调用add(Object o)具体分析
list.add("123");
4、点击add()方法的源码:
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
此时,会发现一共有三条语句,就可以实现数据的添加。第二条就是根据索引添加元素,第三条就是返回,所以,重中之重就是第一条语句,将其源码点开。
解释:
第一条语句:主要是用于对存储数据的数组进行初始化或扩容的。只有初始化后,才能对数组进行操作。
|
先不看第一句,就看第二个语句,可以执行吗?显然不行,elementData是一个空的数组,不能存放值。所以得先执行完第一条语句后,对其数组初始化,在做操组。 此时的size是0,size+1是1 |
5、进一步打开 ensureCapacityInternal(size + 1); 方法的源码
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
|
括号中的函数,其实就是判断当前的存放数据的数组是否是空 此时的minCapacity是1 |
6、打开括号中的 calculateCapacity(elementData, minCapacity) 的源码:
private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
|
其中的if判断,其实就只执行一次,就是在数组为空的条件下,只执行一次,在后序创建后,就不执行,直接返回索引值。 此时,elementData是null minCapacity是1 对于第一次添加元素,肯定数组是null,就执行if获取默认值,进行返回。 |
7、打开 第五步中的 ensureExplicitCapacity(); 的源码:
private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
|
此时,minCapacity是默认值10,进行if判断显然elementData为null长度为0,则无法添加元素,就进行扩容。 |
8、打开 grow(minCapacity); 源码:
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
copyOf()方法,就是那这新的数组容量与旧的存储数据的数组,实现创建新的大的数组,再将旧的数组复制到新的数组中,最后返回的是新的数组,elementData所指向数组也是最后创建的新的数组。
|
此时,minCapacity是10,扩容的思想是:想暂存老数组的长度,再在老数组长度的基础上扩展1.5倍的长度。 而且,看到源码会马上看到:是空间满了才去进行扩容的。 |
9、执行完步骤8后,就返回到步骤4,表示第一条语句执行完毕,开始执行第二条语句,根据索引下标添加元素,第三条返回boolean数据值。
ArrayList的适用场景:
介绍适用场景,就要分析ArrayList的特点:基于底层是一个数组,则查询效率高,增删效率低(数据的大量搬移),线程不安全。
如果不考虑线程安全,并且在多查少改的业务中,ArrayList是很适用的。
我的JDK7与JDK8中的ArrayList区别不大啊???这是为啥?
LinkedList的接口实现类的学习
LinkedList底层与ArrayList底层是不一样的数据结构。LinkedList是一个双向链表,ArrayList是一个动态数组。
基于LinkedList是一个双向链表,涉及到一些特别的针对于链表的方法在Collection集合中是没有的,所以有自己额外的方法。
| LinkedList新添加的方法 | 说明 | ||
|
在该集合的头部添加元素,就像是学习链表时的头插法 | ||
|
在该集合的尾部添加元素 | ||
|
清空集合元素 | ||
|
得到集合的第一个元素 | ||
|
得到集合的最后一个元素 | ||
|
检索并删除头部元素 | ||
|
检索并删除头部第一个元素 | ||
|
检索并删除该列表最后一个元素 | ||
|
检索但不是删除第一个元素 | ||
|
检索第一个元素但不删除 | ||
|
检索最后一个元素但不删除 |
底层结构:是一个双向链表,结点可以指向前一个节点,也可以指向后一个节点。
源码分析:
transient int size = 0; /** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; 空 /** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last; 空 /** * Constructs an empty list. */ public LinkedList() { 无参构造函数 }
当实例化LinkedList时,创建的实例对象也是一个空的集合。
add()方法源码分析:
public boolean add(E e) { linkLast(e); return true; }
点击linkLast();
void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
分析:向链表中添加元素的思路就是,根据传进来的值,将其封装成一个新的结点对象,然后将其赋给last指针(也就是指针后移),然后就是判断这个最后一个last指针是否为null,如果为空就以为着没有最后一个元素,换句话说就是该集合中没有元素,所以会看到if(l == null) first = newNode 就把新的元素赋给第一个节点(first节点)反之如果已经有节点了,那么last肯定不是null,则每一次在添加元素的时候,将last指针后移到新的节点上,然后将新的结点赋给最后一个结点的next的结点(l.next = newNode)
而链表的扩容机制不像ArrayList那样,底层是一个数组,后期满的时候要扩容。链表是一个非连续的存储空间,节点与结点之间依靠的是指针连接,所以不需要扩容机制。
remove()方法的源码分析:
在链表中对接点的删除思路:其实就是从头结点开始查找找到对应的元素后将其指针域next值为空即可或者是断开连接即可。这个与ArrayList是不一样的,动态数组是可以对索引位置删除的,链表其实也是可以对索引位置进行删除的,但是在链表中严格的说并不是一个索引下标,而是一个位置序号。remove(index)方法可以对该位置的元素进行删除。
remove系列的删除方法返回的是删除后的对象的数据。
public E remove() { return removeFirst(); remove()方法默认的是删除第一个元素 }
点击removeFirst()方法
public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); }
点击unlinkFirst()方法
private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; }
对上面这个unlinkFirst方法进行分析,这个就是删除元素的核心代码:
首先,该链表是一个双向链表,并且此时删除的是链表的第一个节点,那就意味着先去判断第一个节点是否为空,若不为空则将first指针移向下一个结点next将第一个节点置为空并且next也是置为空(无引用,便于GC),而对于原头结点的下一个结点的pre指针域应该设置为null,因为头结点的前一个节点肯定是null,既然头结点改变,那头结点的pre域也应该改变。
而他的源码的思路就是,我先得到头结点的值形成头结点对象,再得到头结点的next结点,对原结点的头结点进行置null,然后头结点指针后移,再去判断源头结点的next结点是否为空,若是的话,就原链表只有一个结点,则就将last指针置null若next不为null的话,那就将下一个结点作为头结点,并且将该节点的pre置null即可。
与ArrayList的区别:说到区别,就应该分析其两个结构的特点特性。
ArrayList的特点:底层是一个数组,默认的长度是10,jdk8采用延迟加载机制,每一次扩容都是原基础的1.5倍,数组的查询效率很高,但是增删的效率很低(涉及数据的大量搬移)。
LinkedList的特点:底层是一个双向链表,链表不涉及扩容机制,链表的特点是对于查询的效率低下,因为需要从头查找,但是对于增删的效率很高,不需要设计数据的大量搬移。
应用场景:根据其特点,若是在多改的业务中则使用链表的方法会效率更高。
Vector容器
vector接口实现类实现类List接口继承了AbstractList抽象类。vector容器的底层也是一个动态数组,可实现扩容机制,默认的容量是10,线程安全,有synchronized同步锁加锁实现可解决在并发情况下的并发控制问题,但是也降低的并发量,效率低下。
源码分析:
protected Object[] elementData; 存放元素的数组,默认为空 /** * The number of valid components in this {@code Vector} object. * Components {@code elementData[0]} through * {@code elementData[elementCount-1]} are the actual items. * * @serial */ protected int elementCount; 元素数量,默认为0 /** * The amount by which the capacity of the vector is automatically * incremented when its size becomes greater than its capacity. If * the capacity increment is less than or equal to zero, the capacity * of the vector is doubled each time it needs to grow. * * @serial */ protected int capacityIncrement; 容量增量,默认为0
构造函数:
public Vector() { this(10); }
public Vector(int initialCapacity) { this(initialCapacity, 0); }
public Vector(int initialCapacity, int capacityIncrement) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); this.elementData = new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; }
调用无参构造就会执行这三个函数:最终的结果就是实例化出来的对象中的数据的元素数组的容量设置为10,容量增量还是0。
点击add方法源码:
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
点击ensureCapacityHelper()方法
private void ensureCapacityHelper(int minCapacity) { // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
会发现它的扩容时机也是满了之后才去进行扩容的。当发现当前的存放的元素的容量大于数组的容量时,就以为需要扩容了,就执行grow函数。
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
从他的这条语句可以看出:
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
容量增量不大于0就在原容量基础上加原容量。即新的容量是就的容量的两倍。
会发现一个问题:既然是在原基础上扩大一倍,那干脆直接乘2不就行啦,为啥还要创建一个 protected int capacityIncrement;属性呢?但是至少现在在扩容的时候是这样子的,或许在其他的方法里面可以改动这个容量增量。
方法的使用学习:
void |
add(int index, E element) |
将指定元素插入此Vector中的指定位置。
|
|---|---|---|
boolean |
add(E e) |
将指定的元素追加到此Vector的末尾。
|
boolean |
addAll(int index, Collection<? extends E> c) |
将指定Collection中的所有元素插入到此Vector中的指定位置。
|
boolean |
addAll(Collection<? extends E> c) |
将指定Collection中的所有元素追加到此Vector的末尾,按照指定Collection的Iterator返回的顺序。
|
void |
addElement(E obj) |
将指定的组件添加到此向量的末尾,将其大小增加1。
|
int |
capacity() |
返回此向量的当前容量。
|
void |
clear() |
从此Vector中删除所有元素。
|
Object |
clone() |
返回此向量的副本。
|
boolean |
contains(Object o) |
如果此向量包含指定的元素,则返回
true 。 |
boolean |
containsAll(Collection<?> c) |
如果此Vector包含指定Collection中的所有元素,则返回true。
|
void |
copyInto(Object[] anArray) |
将此向量的组件复制到指定的数组中。
|
E |
elementAt(int index) |
返回指定索引处的组件。
|
Enumeration<E> |
elements() |
返回此向量的组件的枚举。
|
void |
ensureCapacity(int minCapacity) |
如有必要,增加此向量的容量,以确保它至少可以容纳由minimum capacity参数指定的组件数。
|
boolean |
equals(Object o) |
将指定的Object与此Vector进行比较以获得相等性。
|
E |
firstElement() |
返回此向量的第一个组件(索引
0处的项)。 |
void |
forEach(Consumer<? super E> action) |
对
Iterable每个元素执行给定操作,直到处理 Iterable所有元素或操作引发异常。 |
E |
get(int index) |
返回此Vector中指定位置的元素。
|
int |
hashCode() |
返回此Vector的哈希码值。
|
int |
indexOf(Object o) |
返回此向量中第一次出现的指定元素的索引,如果此向量不包含该元素,则返回-1。
|
int |
indexOf(Object o, int index) |
返回此向量中第一次出现的指定元素的索引,从
index向前搜索,如果找不到该元素,则返回-1。 |
void |
insertElementAt(E obj, int index) |
将指定对象作为此向量中的组件插入指定的
index 。 |
boolean |
isEmpty() |
测试此向量是否没有组件。
|
Iterator<E> |
iterator() |
以适当的顺序返回此列表中元素的迭代器。
|
E |
lastElement() |
返回向量的最后一个组件。
|
int |
lastIndexOf(Object o) |
返回此向量中指定元素最后一次出现的索引,如果此向量不包含该元素,则返回-1。
|
int |
lastIndexOf(Object o, int index) |
返回此向量中最后一次出现的指定元素的索引,从
index向后搜索,如果找不到该元素,则返回-1。 |
ListIterator<E> |
listIterator() |
返回此列表中元素的列表迭代器(按适当顺序)。
|
ListIterator<E> |
listIterator(int index) |
从列表中的指定位置开始,返回列表中元素的列表迭代器(按正确顺序)。
|
E |
remove(int index) |
删除此Vector中指定位置的元素。
|
boolean |
remove(Object o) |
删除此向量中第一次出现的指定元素如果向量不包含该元素,则不会更改。
|
boolean |
removeAll(Collection<?> c) |
从此Vector中删除指定Collection中包含的所有元素。
|
void |
removeAllElements() |
从此向量中移除所有组件并将其大小设置为零。
|
boolean |
removeElement(Object obj) |
从此向量中移除参数的第一个(索引最低)事件。
|
void |
removeElementAt(int index) |
删除指定索引处的组件。
|
boolean |
removeIf(Predicate<? super E> filter) |
删除此集合中满足给定谓词的所有元素。
|
protected void |
removeRange(int fromIndex, int toIndex) |
从此列表中删除索引介于
fromIndex (含)和 toIndex (独占)之间的所有元素。 |
void |
replaceAll(UnaryOperator<E> operator) |
将该列表的每个元素替换为将运算符应用于该元素的结果。
|
boolean |
retainAll(Collection<?> c) |
仅保留此Vector中包含在指定Collection中的元素。
|
E |
set(int index, E element) |
用指定的元素替换此Vector中指定位置的元素。
|
void |
setElementAt(E obj, int index) |
将此向量的指定
index处的组件设置为指定对象。 |
void |
setSize(int newSize) |
设置此向量的大小。
|
int |
size() |
返回此向量中的组件数。
|
Spliterator<E> |
spliterator() |
在此列表中的元素上创建late-binding和失败快速
Spliterator 。 |
List<E> |
subList(int fromIndex, int toIndex) |
返回此List的部分在fromIndex(包含)和toIndex(独占)之间的视图。
|
Object[] |
toArray() |
以正确的顺序返回包含此Vector中所有元素的数组。
|
<T> T[] |
toArray(T[] a) |
以正确的顺序返回包含此Vector中所有元素的数组; 返回数组的运行时类型是指定数组的运行时类型。
|
String |
toString() |
返回此Vector的字符串表示形式,包含每个元素的String表示形式。
|
void |
trimToSize() |
修剪此向量的容量为向量的当前大小。
|
Vector与ArrayList和LinkedList的区别:
线程安全考虑:Vector是线程安全的,在Vector的方法中都含有synchronized关键字进行同步加锁。但是由于加同步锁,导致并发量降低,效率低下。
效率方面:ArrayList是一个动态的数组,更加适合于对数据的查询;LinkedList是一个链表,跟家适合于对数据的删除。这两个都是线程不安全的。
Stack学习:
Stack类继承自Vector类,并做出了一些扩展功能。
Stack类表示后进先出(LIFO)对象堆栈。 它通过五个操作扩展了类Vector,允许将向量视为堆栈。 提供了通常的push和pop操作,以及在堆栈顶部项目中的peek的方法,用于测试堆栈是否为empty的方法,以及用于项目的堆栈的方法以及发现它的距离search是从顶部。
| 构造器 | 描述 |
|---|---|
Stack() |
创建一个空堆栈。
|
| 变量和类型 | 方法 | 描述 |
|---|---|---|
boolean |
empty() |
测试此堆栈是否为空。
|
E |
peek() |
查看此堆栈顶部的对象,而不将其从堆栈中删除。
|
E |
pop() |
移除此堆栈顶部的对象,并将该对象作为此函数的值返回。
|
E |
push(E item) |
将项目推到此堆栈的顶部(说白了,就是添加元素)
|
int |
search(Object o) |
返回对象在此堆栈上的从1开始的位置。
|
方法的使用:
import java.util.Stack; public class Demone7 { public static void main(String[] args) { Stack<String> stack = new Stack<>(); stack.add("zhangsan"); stack.add("lisi"); stack.add("wangwu"); System.out.println(stack); System.out.println("判断栈是否为空:" + stack.empty()); System.out.println("========================================================================"); System.out.println("移除栈顶元素"); String pop = stack.pop(); System.out.println("删除的元素:" + pop); System.out.println("栈中现在存在的是:" + stack); System.out.println("========================================================================"); System.out.println("查看栈顶元素,并不删除:" + stack.peek()); System.out.println("栈中现在存在的是:" + stack); System.out.println("========================================================================"); System.out.println("添加元素:" + stack.push("niubi")); System.out.println("栈中现在存在的是:" + stack); System.out.println("========================================================================"); System.out.println("查找栈中的元素,返回的是对象所在的索引位置,从一开始:" + stack.search("zhangsan")); } }
结果:
|
[zhangsan, lisi, wangwu] Process finished with exit code 0 |
对Stack源码的了解:
对add方法的了解:
class Stack<E> extends Vector<E> Stack类继承自Vector类
无参构造:
public Stack() { }
add方法
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
点击 ensureCapacityHelper(elementCount + 1);
private void ensureCapacityHelper(int minCapacity) { // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
若是容量不够,就增容。
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
增容机制:Stack的增容机制与Vector的增容机制是一样的,都是满的时候在去增容,并且是原基础的2倍。

浙公网安备 33010602011771号