一、List接口概述
- List接口继承自Collection(is a)
- 存储元素特点:有序可重复
有序是指元素存与取的顺序相同(不是指排序),元素有下标,下标从0开始,以1递增
可重复是指集合中的元素可以相同
二、List接口特有的常用的方法
链接: Collection中的常用方法
1. add()
- void add(int index, Object element)
- 在列表的指定位置插入指定元素(第一个参数是下标)
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
public static void main(String[] args) {
/**
* 在列表的指定位置插入指定元素(第一个参数是下标)
*/
// 创建List类型的集合
//List myList = new LinkedList();
//List myList = new Vector();
List myList = new ArrayList();
// 添加元素
myList.add("A"); // 默认都是向集合末尾添加元素
myList.add("B");
myList.add("C");
myList.add("D");
// 这个方法使用不多,因为对于ArrayList集合来说效率比较低
myList.add(1, "a");
Iterator it = myList.iterator();
while(it.hasNext()){
Object o = it.next();
System.out.println(o);
}
}
}
2. set()
- Object set(int index, Object element)
- 修改指定位置的元素
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
/**
* 修改指定位置的元素
*/
// 创建List类型的集合
//List myList = new LinkedList();
//List myList = new Vector();
List myList = new ArrayList();
// 添加元素
myList.add("A"); // 默认都是向集合末尾添加元素
myList.add("B");
myList.add("C");
myList.add("C");
myList.add("D");
// 修改指定位置的元素
myList.set(2, "M");
// 遍历集合
for(int i = 0; i < myList.size(); i++){
Object obj = myList.get(i);
System.out.println(obj);
}
}
}
3. get()
- Object get(int index)
- 根据下标获取元素
public class Test {
public static void main(String[] args) {
/**
* 根据下标获取元素
*/
// 创建List类型的集合
//List myList = new LinkedList();
//List myList = new Vector();
List myList = new ArrayList();
// 添加元素
myList.add("A"); // 默认都是向集合末尾添加元素
myList.add("B");
myList.add("C");
myList.add("D");
// 根据下标获取元素
Object firstObj = myList.get(0);
System.out.println(firstObj); // A
// 因为有下标,所以List集合有自己比较特殊的遍历方式
// 通过下标遍历,List集合特有的方式,Set没有
for(int i = 0; i < myList.size(); i++){
Object obj = myList.get(i);
System.out.println(obj);
}
}
}
4. indexOf()
- int indexOf(Object o)
- 获取指定对象第一次出现处的索引
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
/**
* 获取指定对象第一次出现处的索引
*/
// 创建List类型的集合
//List myList = new LinkedList();
//List myList = new Vector();
List myList = new ArrayList();
// 添加元素
myList.add("A"); // 默认都是向集合末尾添加元素
myList.add("B");
myList.add("C");
myList.add("C");
myList.add("D");
// 获取指定对象第一次出现处的索引
System.out.println(myList.indexOf("C")); // 2
}
}
5. lastIndexOf()
- int lastIndexOf(Object o)
- 获取指定对象最后一次出现处的索引
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
/**
* 获取指定对象最后一次出现处的索引
*/
// 创建List类型的集合
//List myList = new LinkedList();
//List myList = new Vector();
List myList = new ArrayList();
// 添加元素
myList.add("A"); // 默认都是向集合末尾添加元素
myList.add("B");
myList.add("C");
myList.add("C");
myList.add("D");
// 获取指定对象最后一次出现处的索引
System.out.println(myList.lastIndexOf("C")); // 3
}
}
6. remove()
- Object remove(int index)
- 删除指定下标位置的元素
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
/**
* 删除指定下标位置的元素
*/
// 创建List类型的集合
//List myList = new LinkedList();
//List myList = new Vector();
List myList = new ArrayList();
// 添加元素
myList.add("A"); // 默认都是向集合末尾添加元素
myList.add("B");
myList.add("C");
myList.add("C");
myList.add("D");
// 删除指定下标位置的元素
myList.remove(0);
System.out.println(myList.size()); // 4
}
}
三、ArrayList
-
ArrayList 继承了 AbstractList ,并实现了 List 接口
-
底层是可变长数组,查询快、增删改慢,因为底层数组操作连续的内存空间,只适合查询,不适合频繁的增删
-
以下情况使用 ArrayList :
- 频繁访问列表中的某一个元素
- 只需要在列表末尾进行添加和删除元素操作
1. ArrayList的初始化容量
- ArrayList底层是Object类型的数组,默认初始化容量是10(底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量为10)
// 底层是Object类型的数组
transient Object[] elementData;
private static final int DEFAULT_CAPACITY = 10; // //默认容量
// 数组,没有向数组中添加元素
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空数组,底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量为10
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 无参构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2. ArrayList的扩容
- ArrayList底层默认长度是10,每次元素进来判断是否溢出,有溢出则扩大为原来的1.5倍
- 先判断列表的capacity容量是否足够,是否需要扩容;
- 再将元素放在列表的元素数组里
源码分析:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 检查是否需要扩容
elementData[size++] = e; // 把元素插到最后一位
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//如果是空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就初始化为默认大小10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 容量增长为原来的1.5倍
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); //以新容量扩容出来一个新数组
}
2.1 ArrayList扩容优化
- 尽可能少的扩容,因为数组扩容效率比较低,建议在使用ArrayList集合的时候预估计元素的个数,给定一个初始化容量
- 回顾一下数组的优缺点
- 优点:
查询/检索某个下标对应的元素效率极高 - 原因:
每一个元素的内存地址在空间存储上是连续的
每一个元素数据类型相同,所以占用空间大小一样
已知元素内存地址、元素占用空间的大小、下标,可以方便的通过一个数学表达式计算出某个下标上元素的内存地址(偏移量),即直接通过内存地址定位元素 - 缺点:
随机增删效率较低(最后一个元素除外);无法存储大数据量 - 原因:
为了保证数组中每个数组元素的内存地址连续,增删时会涉及到某些元素向前或向后位移的操作
很难在内存空间上找到一块特别大的连续的内存空间 - ArrayList集合向数组末尾添加元素,效率很高,不受影响
- 面试题:哪个集合使用最多?
ArrayList集合,因为往数组末尾添加元素,效率不受影响,并且通常检索/查找某个元素的操作比较多
3. ArrayList的构造方法
3.1 无参构造方法底层源码
即 new ArrayList();
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
3.2 有参构造方法1底层源码
即 new ArrayList(传入一个int类型的容量值);
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
3.3 有参构造方法2底层源码
即 new ArrayList(传入一个集合);
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
3.4 测试
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
public class Test {
public static void main(String[] args) {
// 默认初始化容量10
List myList1 = new ArrayList();
// 指定初始化容量100
List myList2 = new ArrayList(100);
// 创建一个HashSet集合
Collection c = new HashSet();
// 添加元素到Set集合
c.add(1);
c.add(2);
c.add(3);
c.add(4);
// 通过这个构造方法就可以将HashSet集合转换成List集合
List myList3 = new ArrayList(c);
for (int i = 0; i < myList3.size(); i++) {
System.out.println(myList3.get(i));
}
}
}
四、LinkedList
-
LinkedList 继承了 AbstractSequentialList ,并实现了 List 接口
-
底层采用双向循环链表,查询慢、增删改快,因为底层操作链表,链表的元素在空间存储上内存地址不连续,在查询的时候只能从头结点开始遍历查找,所以查询效率低,但是增删的时候可以进行断链操作,所以增删效率高
1. 链表数据结构
- 链表是一种非线性、非顺序的物理结构,是由若干个节点组成,即基本的单元是节点(Node)
- 每一个链表都包含多个节点,节点又包含两个部分,第一个是数据域(往节点里面储存的信息),第二个是引用域(相当于指针,单向链表有一个指针,指向下一个节点;双向链表有两个指针,分别指向下一个和上一个节点)
- 链表的物理存储方式为随机存储,访问方式为顺序访问
- 查找节点的时间复杂度为O(n),插入、删除节点的时间复杂度为O(1)
- 链表适用于写操作多,读操作少的场景
- 以下情况使用 LinkedList :
- 需要通过循环迭代来访问列表中的某些元素
- 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作
1.1 单向链表数据结构
- 对于单向链表来说,任何一个节点Node中都有两个属性:
存储的数据、下一节点的内存地址(即当前节点的值和一个指向下一个节点的链接) - 所以单向链表无法从后一节点找到前一节点
1.2 双向链表数据结构
- 对于双向链表来说,有两个指针,分别指向下一个和上一个节点,即一个节点既有向前连接的引用, 也有一个向后连接的引用
2. 向LinkedList中添加元素
- LinkedList没有初始化容量这一概念,最初的链表中没有任何元素,且头尾节点都为null,可以通过add()方法向LinkedList中添加元素
- 底层源码:
transient int size = 0; // 最初链表中没有任何元素(LinkedList没有初始化容量这一概念)
(first == null && last == null) || (first.prev == null && first.item != null)
transient Node<E> first; // 头节点为null
(first == null && last == null) || (last.next == null && last.item != null)
transient Node<E> last; // 尾结点为null
2.1 测试
import java.util.LinkedList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List list = new LinkedList();
list.add("a");
list.add("b");
list.add("c");
for (int i = 0; i < list.size(); i++) {
Object obj = list.get(i);
System.out.println(obj);
}
}
}
2.2 源码分析
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last; // 默认为null
final Node<E> newNode = new Node<>(l, e, null); // 新的节点,Node构造方法(这是一个内部类)
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
// 静态内部类Node
private static class Node<E> {
E item;
Node<E> next; // 下一个节点的内存地址
Node<E> prev; // 上一个节点的内存地址
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
五、Vector
- Vector 继承了 AbstractList ,并实现了 List 接口
1. Vector的初始化容量
- Vector底层是Object类型的数组,初始化容量为10
2. Vector的扩容
- 源码分析:
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- Vector扩容为原来的2倍
3. Vector线程安全
- Vector中所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的,但效率比较低,使用较少
3.1 ArrayList转换为线程安全
- 使用集合工具类:java.util.Collections
- java.util.Collection 是集合接口,java.util.Collections 是集合工具类
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Test {
public static void main(String[] args) {
// 非线程安全
List myList = new ArrayList();
// 变成线程安全的
Collections.synchronizedList(myList);
// myList线程安全
myList.add("1");
myList.add("2");
myList.add("3");
}
}