数据结构是指逻辑意义上的数据组织方式及其处理方式。
从 直接前驱 和 直接后继 个数的维度来看,大体可以将数据结构分为以下四类:
(1)线性结构
0 至 1 个直接前驱 和 直接后继。线性结构包括 顺序表、链表、栈、队列等。
(2)树结构
0 至 1 个直接前驱 和 0 至 n 个直接后继(n 大于或等于2 )
(3)图结构
0 至 n 个直接前驱 和 直接后继(n 大于或等于 2)
(4)哈希结构
没有直接前驱和后继。哈希结构通过某种特定的哈希函数将索引 和 存储的值关联起来,它是一种查找效率非常高的数据结构。
Java类集提供了两个重要的接口:Map 和 Collection。
Collection是所有存放单个元素的集合的最大父接口。它有List(允许重复)和Set(不允许重复)两个重要子接口。
List 有几个常见实现类:ArrayList [ 动态对象数组 ] ,Vector,,LinkedList [ 链表实现 ]。List 比 Collection 多提供了 get 和 set 方法。
文章目录
一、ArrayList
1、字段
private static final long serialVersionUID = 8683452581122892189L;
//默认容量
private static final int DEFAULT_CAPACITY = 10;
//无参构造 或 传入长度为0 时,是空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存储ArrayList元素的数组缓冲区。
transient Object[] elementData;
// non-private to simplify nested class access
//数组长度
private int size;
//最大数组长度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
2、构造方法
先来看 ArrayList 的构造方法:
(1)有参构造:
 注意:如果传入了初始容量,是直接按照初始容量构建数组的。·
public ArrayList(int initialCapacity) {
//值大于0时,根据构造方法的参数值,忠实地创建一个多大的数组
if (initialCapacity > 0) {
//按传入初始长度构造数组
this.elementData = new Object[initialCapacity];
}
else if (initialCapacity == 0) {
//如果传入长度为0 ,为空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
(2)无参构造
 注意:如果是无参构造,默认的数组是空数组,(我老记得是默认容量是10 。QAQ )
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
//默认是空数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
 如果是无参构造,开始创建的是个空数组,当调用 add方法,开始添加元素时,首先会检查容量是否够用,这时才把 默认的容量 10 赋给minCapacity ,然后会调用 grow 方法扩容,这时 才扩容成长度为 10 的数组。
3、扩容方法 grow()
既然是数组,容量不够时就需要扩容,ArrayList扩容机制 1.5倍,Vector扩容后机制 2 倍。
private void grow(int minCapacity) {
// overflow-conscious code
//获取数组长度赋给 oldCapacity
int oldCapacity = elementData.length;
//新容量=原数组长度的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新容量小于传入的参数要求最小容量
if (newCapacity - minCapacity < 0)
//这给扩的不够啊,得按传入的参数扩
newCapacity = minCapacity;
//如果新容量大于数组能容纳的最大元素个数
if (newCapacity - MAX_ARRAY_SIZE > 0)
//那么再判断传入的参数是否大于MAX_ARRAY_SIZE,
//如果传入的参数大于MAX_ARRAY_SIZE,那么新容量等于Integer.MAX_VALUE,
//否则等于MAX_ARRAY_SIZE
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
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;
}
说人话:ArrayList 的扩容过程是这样的,传入 要求的最小参数 minCapacity ,先获取当前数组的长度,以1.5倍扩容,得到新容量 newCapacity,接下来,如果新容量比 要求的最小的还小,那说明扩容得不够,以要求的最小的为准,即 将要求的最小的容量赋给 新容量。接下来要检查新容量有没有超过 允许的数组的最大容量——int 整型的最大值-8(为什么要减 8 呢?等会回答。) 如果超过了,可能是因为扩容过头了,(毕竟是 1.5倍扩容,可能人家传入的刚好是长度的1.1 呢,本身的oldCapacity不够,扩了个 1.5 又太多了),那就看看要求的最小的容量是否大于 允许的最大容量,也就是… … -8,如果大于,就把整数最大值 (2^ 31-1)赋给 新容量;如果不大于,说明确实扩容过头了,把 … .-8赋给新容量。
之前老看网上说 ArrayList 的最大容量是 2^31-8 ,可是看上面的源码,扩容时候,如果要求的最小容量 已经比 …-8 大了,那就应该把 newCapacity 赋为整数的最大值(因为用来衡量数组的长度是用整数),grow 方法的最后一步是按 调用 Arrays 的 copyOf,把原来的数组元素都拷贝到扩容后的新数组里去,所以我觉得 ArrayList 的最大容量是 2 ^ 31,来看 MAX_ARRAY_SIZE 字段上的注释:
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
有道翻译:要分配的数组的最大大小。 有些虚拟机在一个数组中保留一些头 header。试图分配更大的数组可能会导致 OutOfMemoryError:请求的数组大小超过VM限制。所以在不超过 …-8 的前提下,应当是以 … -8 为准的,而超过了,那就只能把 MAX-VALUE 作为数组的容量啦。
假如需要将 1000 个元素放置在 ArrayList 中,采用默认构造方法,则需要被动扩容 13 次才可以完成存储。反之,如果在初始化时便指定了容量 new ArrayLIst(100), 那么在初始化 ArrayList 对象的时候,就直接分配 1000 个存储空间,从而避免被动扩容 和 数组复制的额外开销。
4、add方法
- 在末尾添加
public boolean add(E e) {
//先确保数组容量是否够用
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//如果是无参构造建立的空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//会按 默认容量10 和 要求最小的容量的值 中 最大值,作为容量最小值,
//所以默认的无参构造是在这一步才
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
//还是以无参构造为例,这时,minCapacity为10 ,大于 0,需要扩容
if (minCapacity - elementData.length > 0)
//这时才扩容成大小为 10 的数组
grow(minCapacity);
}
👀 ArrayList 的 add 方法线程不安全举例
(1)
elementData[size++] = e; 分两步执行:elementData [size]=e 和 size++。
假设 size =9,当前的长度是默认的容量 10,先检查数组容量是否够用——无需扩容,接下来,CPU 先给线程 A ,线程A 将 e1 值赋给 elementData[9] ,线程A交出 CPU ,线程 B 获取 CPU ,这时 size 仍为 9 ,线程B 会给 elementData[9] 赋值 e2,相当于覆盖了线程 A 的值,然后 CPU 给线程 A ,size++ 变为10,CPU再给 线程 B ,size++ 变为 11,但是 size [10] 的值却是 null 的。
(2)
假设 size=9,线程A ,先检查数组容量是否够用——无需扩容,CPU 给线程B ,——也是 无需扩容。接下来 CPU 给线程 A,添加元素 ,size变成10 ,A 线程结束,CPU 给 B 线程,因为已经检查过容量,所以 B 线程就会直接给 elementDate [10] 进行赋值,这时发生 数组下标越界异常 ArrayIndexOutOfBoundsException 。
- 在指定下标处添加
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//将该下标后面的元素都后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
remove
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
//把要删除的下标 以后的元素都往前移了一个,
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将末尾赋为 null,并将 size-1
elementData[--vsize] = null; // clear to let GC do its work
return oldValue;
}
❓ArrayList 不是线程安全的,怎么办❓
(1)用 concurrent 包提供的工具将 ArrayList 变为线程安全的。
如:
List<String> list1=Collections.synchronizedList(new ArrayList<String>());
要注意用户需要手动加锁 (当通过 Iterator 、spliterator 或Stream 进行遍历时)。
(Vector也是需要用户手动加锁的。)
如:
synchronized(list1);
{Iterator i=list1.iterator();
while(i.hasNext())
{Systrm.out.println(i.next);}
}
(2)用CopyOnWriteArrayList [COW:写入时复制 容器]。增删改查时都会加锁,都会创建一个新数组,操作完成后再赋给原来的引用,而读操作是不需要锁的。
5、ArrayList 的 iterator 删除元素
Collection 就实现了一个接口 Iterable ,这个接口有以下方法:
//它会返回 Iterator(是个接口) 的实现类类型,Itr 类代码请稍候
public Iterator<E> iterator()
//在 ArrayList 中的实现:
{
return new Itr();
}
default void forEach(Consumer<? super T> action)
default Spliterator<T> spliterator()
再总结一下:
“Iterator 接口是由 Collection 接口支持的”:
Collection 接口只实现了一个接口,那就是 Iterable 接口,而Iterable 中有三个方法:iterator(),Foreach(),spliterator()。
所以所有实现了 Collection 接口的子类(各种 List、Set)都会覆写 iterator 方法(像 ArrayList,是有个内部类 Itr implements Iterator ,调用iterator()方法时返回 new Itr(); 像LinkedList,它的父类——一个抽象类AbstractSequentialList 覆写了 iterator()方法,返回的是一个 ListIterator 对象;
像 HashSet ,它的底层是 HashMap ,调用 iterator() 方法时是调用 map.keySet 的iterator 方法,Set 接口实现了 Collection 接口,它的实现类也是要覆写 iterator()方法的。)
所以所有实现了 Collection 接口的子类都可以用 foreach。
“ListIterator 接口是由 List 接口支持的”:
List接口中定义的 listIterator() 方法,这样所有实现了 List 接口的子类都需要覆写 listiterator 方法。
所以 所有的 Collection 接口的实现类肯定都覆写了 iterator 方法,通过迭代输出的形式删除内容:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TestDemo {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("Hello");
list.add("Hello");
list.add("B");
list.add("Bit");
list.add("Bit");
Iterator<String> iterator=list.iterator();
while (iterator.hasNext())
{
String str=iterator.next();
if(str.equals("B"))
{ iterator.remove();
continue;}
System.out.println(str);}
}
}
注意到,想删除一个元素,必须先跳过该元素,next 和 remove 方法是互相依赖的。
♥ 来看看 next 和 remove 的源码:
//ArrayList 有个内部类 Itr 实现了 Iterator 接口
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
//上一个元素的下标
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
public E next() {
//快速失败机制
checkForComodification();
//指针,需要返回的元素的下标
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//this:在内部类的某个方法中 指定某个嵌套层次的 外围类的 "this"
//调用 ArrayList 的 remove 方法,即把下标以后的元素依次向前移一位,
//末尾赋 null
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
}
catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
如果先调用 remove ,指向前一个元素的指针 lastRet 是指向 -1 的,数组下标越界,所以必须结合 next() ,先让 cursor、lastRet 都往后挪一个,然后调用 remove 才能删除 lastRet 也就是这时候的[0],删除元素后,再把 cursor、lastRet 挪回原位——0,-1。
总结:
调用 remove 时 cursor 会往前指一个,因为删除掉当前的元素,它后面的所有元素会依次往前覆盖,然后末尾赋null,(正是通过ArrayList的remove(int index)方法实现的,ArrayList.this 表示外部类的对象,因为是在内部类Itr中调用这个方法。(类名.this:在内部类方法中指定某个嵌套层次的外围类的“this”)
所以 cursor 需要前指一个保持是最新的next。而且 lastRet会变为 -1,因此每次 remove 前都需要调用 next 方法 ,才能通过 next 方法中的 cursor 找到当前的 next ,然后将该值赋给 lastRet ,lastRet 不为-1,才不会出现数组下标越界,而抛出 ArrayIndexOutOfBoundsException,才能正确删除元素。 所以如果用 listIterator 可以从前往后 也可以从后向前遍历,需要先从头往前,才能从后向前遍历,正是因为 cursor 和 lastRet 这两个下标。
ArrayList 和 LinkedList 区别
(1).是否保证线程安全:
ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
(2)底层数据结构:
ArrayList 的底层是 Object数组,可以实现动态扩容,每次扩容成原来的 1.5 倍,支持随机访问,但是删除或者插入元素效率很低,例如在数组中间插入(删除)一个元素,必须把这个元素以后的元素全部向后(前)搬移一个位置。
LinkedList 底层使用的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。不存在初始容量和扩容的概念。访问一个元素要遍历节点,时间复杂度为 O(n)。删除或者插入一个给定指针指向的结点时间复杂度为 O(1)。但是 LinkedList 的remove(Object o) 方法时间复杂度是 O(n^2) 的,因为要先遍历找到删除的节点。
(3)内存空间占用:
ArrayList的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间【8】,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比 ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)
ArrayList 与 Vector
- 相同之处
底层都是 Object[ ] 数组,默认初始容量都是 10 ,遍历集合中元素可以调用iterator,foreach,listIterator。 - 不同之处:
(1)ArrayList 是 JDK1.2 提出的,Vector是 JDK1.0 提出的。
(2)ArrayList 非同步,线程不安全,Vector 同步,线程安全。
(3)在需要遍历集合中元素时,Vector 可以调用 elements 方法,返回值是 Enumetor 类型。
(4)Vector扩容时可以指定容量增长因子。默认是 2 倍 。
二、Arrays
Arrays 是针对数组对象进行操作的工具类,包括数组的排序、查找、对比、拷贝等操作。(尤其是排序,在多个 JDK 版本中不断地进化,比如原来的归并排序改成 Timsort,明显地改善了集合的排序性能。)
数组 与 集合 都是用来存储对象的容器,前者性质单一,方便易用;后者类型安全,功能强大,且两者之间必然有互相转换的方式。 在数组转集合的过程中,注意是否使用了视图方式直接返回数组中的数据。比如 Arrays.asList() 为例,它把数组转换成集合时,不能使用其 修改集合相关的方法,它的 add / remove / clear 方法会抛出 UnsupportedOperationException 异常。
👀 例:
import java.util.Arrays;
import java.util.List;
public class ArrayAsList {
public static void main(String[] args) {
String[] stringArray=new String[3];
stringArray[0]="one";
stringArray[1]="two";
stringArray[2]="three";
List<String> stringList= Arrays.asList(stringArray);
//修改转换后的集合,将第一个元素“one”改成“oneLIst”
stringList.set(0,"oneList");
System.out.println(stringArray[0]);
stringList.add("four");
stringList.remove(2);
stringList.clear();
}
}
运行结果:
可以通过 set() 方法修改元素的值,原有数组相应位置的值同时也会被修改,但是不能进行修改元素个数的任何操作,否则 编译正确,但是均会抛出 UnsupportedOperationException 异常。 Arrays.asList 体现的是适配器模式,后台的数据仍是原有数组,set() 方法即间接对数组进行值的修改操作。Arrays.asList 返回对象是一个 Arrays 的内部类,它并没有实现集合个数的相关修改方法,这也是抛出异常的原因之一。源码如下:
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
返回的明明是 ArrayList 对象,咋就不能随心所欲对集合进行修改呢? 注意 此 ArrayList 非彼 ArrayList,虽然 Arrays 和 ArrayList 同属一个包,但是在 Arrays 类中还定义了一个 ArrayList 的内部类,根据作用域就近原则,此处的 ArrayList 是李鬼😂,即 这是个内部类。此 李鬼十分简单,只提供了个别方法的实现:
会抛出 UnsupportedOperationException 异常 ,是因为它的父类 AbstractList:
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
好家伙,这货有气节,它传递的信息是 “要么直接用我,要么小心异常!”。数组转集合引发故障还是很常见的,比如,某业务调用某接口时,对方以这样的方式返回一个 List 类型的集合对象,本方获取集合数据时,99.9% 是只读操作,但小概率需要增加一个元素,就会引发故障。所以, 在使用数组转集合时,需要使用 李逵 java.util.ArrayList 直接创建一个新集合,参数就是 Arrays.asList 返回的不可变集合 ,源码如下:
♥
List<Object> objectList=new java.util.ArrayList<Object>(Arrays.asList(数组));
相比于 数组 转 集合,集合 转 数组 更加可控,毕竟是从相对自由的集合容器 转为 更加苛刻的 数组。比如,适配别人的数组接口,或者 进行局部方法计算等,都可能遇到 集合 转 数组的情况。
👀 例:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class LIstToArray {
public static void main(String[] args) {
List<String> list=new ArrayList<String>(3);
list.add("one");
list.add("two");
list.add("three");
//(1)
Object[] array1=list.toArray();
//(2)
String[] array2=new String[2];
list.toArray(array2);
System.out.println(Arrays.asList(array2));
//(3)
String[] array3=new String[3];
list.toArray(array3);
System.out.println(Arrays.asList(array3));
}
}
运行结果:
运行成功了,从 (1) 可以看出,list.toArray() 返回的是 Object[ ] ,改代码后发现,不能用 String[ ] 去接,尽管返回数组的每个元素都是 String 类型的,但就是不能用 String[ ] 去接,书上说这样说明泛型丢失 ,不要用 toArray() 无参方法把集合转换成数组。来看看 toArray 方法的源码,就懂了:
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
关键在这,返回的是 Object[ ] 类型的数组 elementData:
transient Object[] elementData; // non-private to simplify nested class access
(同时也注意到,这个存储 ArrayList 真实数据的数组由 transient 修饰,表示此字段在类的序列化时常被忽略。因为集合序列化时 系统会调用 writeObject 写入流中,在网络客户端反序列化的 readObject 时,会重新赋值到 新对象的 elementData 中。点解多此一举? 因为 elementData 容量经常会大于 实际存储元素的数量,所以只需发送真正有价值的数组元素即可。)
@SuppressWarnings("unchecked")
public static <T> T[] copyOf(T[] original, int newLength)
{
return (T[]) copyOf(original, newLength, original.getClass());
}
但是吧,复制值的时候,传进来的是泛型 U[ ] 数组,所以 拷贝的原料 original 里的元素是啥类型,最后结果里的数组的元素也就是啥类型(个人理解,如有异议请及时指出):
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
//新建一个数组 copy
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;
}
从 (2)可以看出,传入的数组的容量不够,输出值是 null,来看源码:
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
最后来用代码模拟一下 入参数组容量不够时、入参数组容量刚好时,以及 入参数组容量超过集合大小时,大概的执行时间,代码如下:
import java.util.ArrayList;
import java.util.List;
public class ToArraySpeedTest {
private static final int COUNT=100 * 100 * 100;
public static void main(String[] args) {
List<Double> list=new ArrayList<>(COUNT);
//构造一个 100 万个元素的测试集合
for(int i=0;i<COUNT;i++)
{list.add(i * 1.0);}
long start=System.nanoTime();
Double[] notEnoughArray=new Double[COUNT-1];
list.toArray(notEnoughArray);
long middle1=System.nanoTime();
Double[] equalArray=new Double[COUNT];
list.toArray(equalArray);
long middle2=System.nanoTime();
Double[] doubleArray=new Double[COUNT * 2];
list.toArray(doubleArray);
long end=System.nanoTime();
long notEnoughArrayTime=middle1-start;
long equalEnoughArrayTime=middle2-middle1;
long doubleArrayTime=end-middle2;
System.out.println("数组容量小于集合大小:notEnoughArrayTime:"+notEnoughArrayTime/
(1000.0 * 1000.0) + "ms");
System.out.println("数组容量等于集合大小:equalEnoughArrayTime:"+equalEnoughArrayTime/
(1000.0 * 1000.0) + "ms");
System.out.println("数组容量大于集合大小:doubleArrayTime:"+doubleArrayTime/
(1000.0 * 1000.0) + "ms");
}
}
运行结果:
数组容量小于集合大小:notEnoughArrayTime:85.5096ms
数组容量等于集合大小:equalEnoughArrayTime:7.1272ms
数组容量大于集合大小:doubleArrayTime:8.7597ms
具体的执行时间,由于 CPU 资源占用的随机性,会有一定差异,多次运行结果显示,当数组容量等于集合大小时,运行总是最快的,空间消耗也是最少的。由此证明,如果数组初始大小设置不当,不仅会降低性能,还会浪费空间。使用集合 toArray(T[ ] array) 方法,转换成数组时,注意需要传入类型完全一样的数组,并且它的容量大小为 list.size( ) 。