ArrayList的实现原理
ArrayList和LinkedList比较
ArrayList可以说是我们平时开发过程中用的最多了一个集合类了,其中ArrayList比LinkedList用的场景还多,我们在面试过中中经常会被问到这两个结合类有什么区别,当然他们都是List的子类,网上大部分的解释如下:
ArrayList和LinkedList都是List的子类,ArrayList是数组结构,而LinkedList是链表结构,LinkedList维护了两个指针,头和尾,所以LInkedlist在插入的时候比较快,而ArrayList在插入的时候没有LinkedList快,但是ArraList通过下标访问,访问速度比较快。所以总结如下:
1.当我们的业务场景中如果需要对一个集合频繁的插入,删除,而读取较少的时候就使用LinkedList
2.当我们的业务场景中如果需要对一个集合频繁的读取,而插入和删除较少的时候就使用ArrayList
当然了上面的说法是基于单线程来说的,如果多线程可能会出现线程安全问题
但是上面的说法只是看到了表面,没有看到实物的本质,我们来深思一个问题?ArrayList的插入如果不指定插入的位置,默认都是通过尾部添加的,那么我们的Arraylist为什么就没有LInkedList快呢?
原因是ArrayList底层维护的是一个Object数组,我们的元素都存放在这个Object数组中,数据的默认大小是10,超过大小是按1.5倍进行扩容,所以我们通过底层源码的分析和测试得出一个结论就是如果要比较Arraylist和LInkedlist谁的插入速度比较快的前提是ArrayList有没有指定初始容量大小,如果我们指定了ArrayList的容量大小,那么ArrayList在插入的时候就不会进行扩容,我们都知道我们的普通数组的长度一经指定,是不可变的,所以ArrayList在扩容的时候需要将原来的数据复制到新的数组,而老的数组进行回收,这样就非常费时间;
所以如果我们要比较这两个集合的插入速度是建立在ArrayList是否进行动态扩容的基础之上的,比如我们来看一个程序:
public class T0904 {
public static void main(String[] args) {
int count = 10000000;
List<Integer> list1 = new ArrayList();
List<Integer> linklist = new LinkedList<>();
long startTime = System.currentTimeMillis();
for(int i = 1 ; i <= count ; i ++ ){
list1.add(i);
}
long endTime = System.currentTimeMillis();
System.out.println("arraylist add 耗时:"+(endTime - startTime));
list1.clear();
startTime = System.currentTimeMillis();
for(int i = 1 ; i <= count ; i ++ ){
linklist.add(i);
}
endTime = System.currentTimeMillis();
System.out.println("linklist add 耗时:"+(endTime - startTime));
}
}
结果如下:
arraylist add 耗时:4623
linklist add 耗时:3336
明显的linklist耗时比较低,则证明了上面的说法,在插入的时候(当然是数据量较大的情况,数据较少的情况都差不多)LinkedList的效率确实要高一点,但是我们修改下程序,我们指定Arraylist的容量,让其不进行动态扩容:
public class T0904 {
public static void main(String[] args) {
int count = 10000000;
List<Integer> list1 = new ArrayList(count);
List<Integer> linklist = new LinkedList<>();
long startTime = System.currentTimeMillis();
for(int i = 1 ; i <= count ; i ++ ){
list1.add(i);
}
long endTime = System.currentTimeMillis();
System.out.println("arraylist add 耗时:"+(endTime - startTime));
list1.clear();
startTime = System.currentTimeMillis();
for(int i = 1 ; i <= count ; i ++ ){
linklist.add(i);
}
endTime = System.currentTimeMillis();
System.out.println("linklist add 耗时:"+(endTime - startTime));
}
}
输出如下:
arraylist add 耗时:3577
linklist add 耗时:7088
很明显的是Arraylist的插入效率跟高,而且差不多是一倍的差距,所以当我们来解答这个问题的时候一定要考虑到全方面,不要以便盖全的去回答这个问题。
ArrayLIst源码分析
ArrayList的标记接口
我们先来看下ArrayList的底层继承关系:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
.........
}
可以看到我们的ArrayList继承了AbstractList,实现了List,RandomAccess, Cloneable, java.io.Serializable,其中AbstractList和List都是定义了ArrayList中的一些常用的操作方法,但是我们注意到RandomAccess、Cloneable、Serializable这个三接口没有?
RandomAccess
这三个接口都是一个空接口,什么都没有,如下:
public interface RandomAccess {
}
其他两个也都是什么都没有,那么什么都没有为什么要定义?为什么还要实现它呢?
其实这在java里面可以被叫做标记接口,就是为这个ArrayList打一个标记,标记ArrayList是一个RandomAccess,那么RandomAccess是什么意思呢,其实就是可以被随机访问,通过下标随机访问
就类似于我们判断一个对象是否是继承了某个类或者实现了某个接口,比如 obj instanceof xxx一样;
而RandomAccess其实是用来标记我们的接口使用什么方式进行迭代,比如LinkedList和ArrayList因为迭代的方式不同,所以就是通过是否实现了RandomAccess来确定用什么样迭代器;
通俗点说就是实现了RandomAcess使用for进行迭代,而LinkedList则使用Iterator更快一点
Cloneable
Cloneable也是一个标记接口,这个标记接口就代表我们的对象可以被拷贝,至于拷贝的结果是浅拷贝还是深拷贝,看你重写Object的方法中clone的深度;
对象的拷贝其实就是再创建一个对象,这个对象有着原来的对象一份精确的值,这里如果是基本数据类包括string类型的变量,会直接进行拷贝,虽然说string是对象类型,但是string是final的,当我们在程序中这样使用:
String str1 = “aa”;
str1=“aabb”;
或者
String str2 = “aa” + “bb”;
那么实际操作过程中是怎样的呢?我这边简单画个图如下:
比如我上面的代码块string str= “aa”;在方法中这样去写,那么会在堆区的字符串常量池(stringTable)中找寻是否是字符串aa,如果有,直接指向这个字符串,如果没有,就创建一个aa,并且栈中的对象str1指向这个字符串常量aa;代码第二行str1 = “aabb”;因为我们的字符串是final对象,所以final对象都不可能修改值的,但是我们的代码是ok的,但是对于string来说,如果你重新赋值了,那么这个时候也会在堆区去找寻这个字符串常量是否存在,如果存在就让str1重新指向一个新的对象,而原来这个对象aa也还在堆区,只是不再被str1所指向,如果没有,就创建一个新的aabb,并且使str1指向它;当我们的第三行代码String str2 = “aabb”;这个时候我们的字符串常量池已经有了aabb,所以这个时候str2直接指向了aabb,说白了就是str1和str2的内存地址一样,这个时候我们执行str1 == str2肯定是返回true;
综上:所以说如果是对象拷贝,基本数据类型、string类型拷贝过去过后,修改新的对象的值不会影响到旧对象的值,因为涉及到的知识点就是”值传递“;而如果对象中有应用类型,那么拷贝就分两种,浅拷贝和深拷贝,浅拷贝只是把应用地址拷贝过去了,如果源对象或者目标对象修改了对象的值,那么两个对象的中的值也一起改变,这里涉及到的知识点“引用传递”;如果是深拷贝,那么就各自独立了,看以下代码:
这里需要说明的就是如果我们使用clone()方法的时候,那么拷贝的对象必须实现Object的clone()方法,必须实现Cloneable这个标记接口
public class T0904 implements Cloneable{
private String name;
private int age;
private T0904_01 t0904_01;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
//浅拷贝
T0904 t0904 = (T0904) super.clone();
return t0904;
}
public T0904_01 getT0904_01() {
return t0904_01;
}
public void setT0904_01(T0904_01 t0904_01) {
this.t0904_01 = t0904_01;
}
@Override
public String toString() {
return "T0904{" +
"name='" + name + '\'' +
", age=" + age +
", t0904_01=" + t0904_01 +
'}';
}
}
public class T0904_01 {
private int a;
private String b;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public String getB() {
return b;
}
public void setB(String b) {
this.b = b;
}
@Override
public String toString() {
return "T0904_01{" +
"a=" + a +
", b='" + b + '\'' +
'}';
}
}
浅拷贝的测试类:
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
T0904 t = new T0904();
t.setAge(12);
t.setName("bml");
T0904_01 t0904_01 = new T0904_01();
t0904_01.setA(11);
t0904_01.setB("bbbb");
t.setT0904_01(t0904_01);
T0904 t2 = (T0904) t.clone();
t2.setAge(24);
t2.setName("duy");
t2.getT0904_01().setA(44);
t2.getT0904_01().setB("aaaaa");
System.out.println("t="+t);
System.out.println("t2="+t2);
}
}
输出:
t=T0904{name=‘bml’, age=12, t0904_01=T0904_01{a=44, b=‘aaaaa’}}
t2=T0904{name=‘duy’, age=24, t0904_01=T0904_01{a=44, b=‘aaaaa’}}
从上面就可以看出浅拷贝的基本类型age和string类型name在拷贝后,修改值过后是和拷贝前不一样的,而引用类型T0904_01在拷贝过后修改了值,那么这个时候也会改变了原有的值,因为浅拷贝对于应用类型来说都是指向的同一内存地址,那么我来看下深拷贝,我们修改下T0904 代码:
protected Object clone() throws CloneNotSupportedException {
T0904 t0904 = (T0904) super.clone();
t0904.setT0904_01((T0904_01) this.t0904_01.clone());
return t0904;
}
在T0904_01中实现标记接口Cloneable,并且实现Object的clone方法:
public class T0904_01 implements Cloneable{
private int a;
private String b;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public String getB() {
return b;
}
public void setB(String b) {
this.b = b;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "T0904_01{" +
"a=" + a +
", b='" + b + '\'' +
'}';
}
}
那么我们在执行下测试方法输出:
t=T0904{name=‘bml’, age=12, t0904_01=T0904_01{a=11, b=‘bbbb’}}
t2=T0904{name=‘duy’, age=24, t0904_01=T0904_01{a=44, b=‘aaaaa’}}
很明显都不一样了
总结:在对象中的拷贝中其实记住一句话就是拷贝过程中基本数据类型、String类型相对独立就可以了,而这句话涉及的知识点就是值传递和应用传递,把三个知识点搞明白就搞懂了java中的浅拷贝和深拷贝,从基础上来解决我们的拷贝问题。
Serializable
Seriallizable就是java中序列化和反序列化的意思,就是在网络传输过程中,把对象序列化成二进制流给别人,别人拿到过后进行反序列化然后又可以使用的意思,如果加了Seriallizable就可以序列化和反序列化,这是一种标准;
在Java中的这个Serializable接口其实是给jvm看的,通知jvm,我不对这个类做序列化了,你(jvm)帮我序列化就好了。如果我们没有自己声明一个serialVersionUID变量,接口会默认生成一个serialVersionUID,默认的serialVersinUID对于class的细节非常敏感,反序列化时可能会导致InvalidClassException这个异常(每次序列化都会重新计算该值);
ArrayList属性分析
//Arraylist中的序列化ID,也可以叫序列化版本号,
这个ID非常重要,如果不写 ,
jvm会默认生成一个,类内容的改变会影响签名变化,导致反序列化失败
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*初始容量大小,默认是10,就是我们在什么Arraylist的时候
没有传入大小的时候,默认会给10的容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
//这个被static修饰的空数据,我们的ArrayList中添加元素的时候
//其实是添加到Object数组中去了,而这个空数据是代表当我们
//申明空构造的Arraylist的时候,默认的数组实例。
//它指向的是空参数构造的Arraylist实例。
//这个是有参数构造的空数组实例(参数值 ==0)
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
//这个无参数的构造空数组实例,这两个空数组有什么区别吗?
//其实Arraylist是用来区别我们往数组中添加元素的时候
//到底是通过无参构造初始化的还是有参构造初始化的,以便
//在容量达到最大过后知道如何扩容;空的构造器初始化容量为10,有参构造根据
//扩容因子进行扩容
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify
//nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
//ArrayList中元素的个数,size永远小于等于elementData.length
private int size;
//扩容时,数组的最大大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
ArrayList构造方法
//当使用无参数构造的ArrayList的时候,是会直接把DEFAULTCAPACITY_EMPTY_ELEMENTDATA
//空数组实例直接赋值给elementData的,当使用有参数的ArrayList实例化的时候,要判断
//参数值是否大于0,如果大于0就直接把elementData进行初始化,大小为传入的大小;
//如果等于0,就把EMPTY_ELEMENTDATA空数组实例赋值给elementData
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
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);
}
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
//这个构造是初始化的时候,将一个集合赋值给这个ArrayList,其实说白了就是把
//原来集合的elementData放到这个新的elementData中
//如果这个集合长度0,则把有参的构造空数组实例赋值给elementData,相当于
// new ArrayList(0)
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;
}
}
添加原始add(尾部添加)
//这个方法就是我们平时list中的add方法,代码很简洁,只有三行代码,
//其中最重要的就是ensureCapacityInternal,这个方法做什么用的,
//我们看下面的实现
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//这个方法所做的工作简单来说就是看下我们的这次添加的元素加的进去不
//如果加不进去就要扩容哦
//首先要计算,看下我们当初实例化Arraylist的时候是用的默认构造还是有
//参数构造方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
判断如果 elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA
//就取 DEFAULT_CAPACITY 和 minCapacity 的最大值也就是 10。
//这就是 EMPTY_ELEMENTDATA 与 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
//的区别所在。同时也验证了上面的说法:
//使用无参构造函数时是在第一次添加元素时初始化容量为 10 的
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//记录操作数modCout++,这个modCount有什么用
//后面代码解释看,就是fast-fail,快速失败
//然后看下minCapacity是否是大于了目前的elementData的长度
//如果大于的话,那么就要进入扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容的方法
//首先将原来的长度赋值给oldCapacity,然后进行扩容
//oldCapacity >> 1扩容为原来的1.5倍
//如果扩容过后值还是小于我们的要求的容量
//则直接把要求的大小赋值给扩容的后的大小
//如果扩容后的newCapacity 大于数组存放的最大值
//则把最大值赋值给newCapacity
//简单一句话就是扩容后如果大于要求值,则按传入的大小走;
//否则看是否大于了Interger的最大值,如果大于最大值,就按Integer的最大值来
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
//为什么说ArrayList插入速度不如Linkedlist,就是在扩容的时候
//要动态复制数组,就是转移数组的内存空间,原有的老数组回收
elementData = Arrays.copyOf(elementData, newCapacity);
}
指定下标添加元素
public void add(int index, E element) {
rangeCheckForAdd(index);//下标越界检查
//记录操作数,并且检查是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//因为要在指定位置添加元素,所以要涉及数据的移动,时间复杂度是O(n)
//就看你的数组要插入到那个位置
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
如下图:
这个就是指定位置添加元素的时候,需要对元素进行移动操作
元素删除
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
rangeCheck(index);
modCount++;//记录操作数
//将要移除的值取出来
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
//判断要删除的元素是否是最后一个位,如果 index 不是最后一个,
//就从 index + 1 开始往后所有的元素都向前拷贝一份。
// 然后将数组的最后一个位置空,
// 如果 index 是最后一个元素那么就直接将数组的最后一个位置空
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
* (if such an element exists). Returns <tt>true</tt> if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return <tt>true</tt> if this list contained the specified element
*/
//当我们调用 remove(Object o) 时,
//会把 o 分为是否为空来分别处理。
//然后对数组做遍历,找到第一个与 o 对应的下标 index,
//然后调用 fastRemove 方法,
//删除下标为 index 的元素。
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
//fastRemove(int index) 方法和 remove(int index) 方法基本全部相同。
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
迭代器Iterator
/**
* Returns an iterator over the elements in this list in proper sequence.
*
* <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
*
* @return an iterator over the elements in this list in proper sequence
*/
//迭代器是创建一个新的Itr对象,Itr是一个内部内
public Iterator<E> iterator() {
return new Itr();
}
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;
Itr() {}
public boolean hasNext() {
//迭代器,如果下一个要访问的元素下标没有达到最后下标就一直迭代
return cursor != size;
}
@SuppressWarnings("unchecked")
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();
//将下一个要访问的元素+1,比如这次访问的下标为1,则下次访问的下标为2
cursor = i + 1;
//而将当前访问的下标赋值给lastRet,lastRet代表上一个要访问的元素
return (E) elementData[lastRet = i];
}
public void remove() {
//判断lastRet是否小于0
if (lastRet < 0)
throw new IllegalStateException();
//检查修改异常
checkForComodification();
try {
//直接调用ArrayList的remove的方法来移除
//而且只能移除迭代过程中的上一下标元素
//而且必须是先迭代,才能删除
ArrayList.this.remove(lastRet);
//删除过后,将删除的当前下标赋值给下一次迭代要访问的元素
//重写赋值lastRet为-1
//为了避免下一次迭代或者remove出现修改异常,这边重新赋值期望修改值
//的大小
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//检查修modCount 是否等于我们的期望值
//如果不等于期望值,则怎么我们的集合被修改了,比如添加和删除
//所以这个时候报数据修改异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
remove 方法的弊端。
1、只能进行remove操作,add、clear 等 Itr 中没有。
2、调用 remove 之前必须先调用 next。因为 remove 开始就对 lastRet 做了校验。而 lastRet 初始化时为 -1。
3、next 之后只可以调用一次 remove。因为 remove 会将 lastRet 重新初始化为 -1
不可变集合List
我们可以通过Collections中的unmodifiableList将一个集合变成不可变集合,但是会出现一个问题就是原集合的改变(删除,添加)会影响到不可变集合的影响,也就是在内存中他们是指向同一个内地址区域,比如:
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
List<Integer> list2 = Collections.unmodifiableList(list);
list.remove(1);
list.clear();
System.out.println(list2.size());
}
最后我们的可变集合输出为0,所以是受了可变集合list的影响的;我们来思考下,我们的平时的项目场景,再以前的单体应用中,一般都会启动将一些在系统运行过程中不会经常变的参数放入到内存中,我们通常会用集合存储,但是如果只用List或者Map存储的话,那么这个集合可以随时可以变更的,那么如果我们不需要他被别人更改的话,就可以通过unmodifiableList来变称一个不可变集合,但是他受原集合的影响,那么要怎么做呢?
其实我们只需要把变成的可变集合重新赋值给list就可以了,那么这样的话,如何对list进行的添加或者删除都会报UnsupportedOperationException,不支持的操作异常,比如
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list= Collections.unmodifiableList(list);
list.remove(1);
list.clear();
System.out.println(list.size());
}
输出:
Exception in thread “main” java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableList.remove(Collections.java:1317)
at org.bml.t0904.Clien_01.main(Clien_01.java:18)
Arrays.asList
我们在平时开发过程中经常会这样写Arras.asList(…),但是可能大家平时也没太注意,asList中一些细节问题,我们先来看下我们的泛型T,这个泛型T只支持引用类型,也就是对象类型,而基本数据类型是不支持的,所以这里就出现了两个问题,比如int [] t1 = new int[]{1,2,3,4};和Integer [] t2 = new Integer[]{1,2,3,4};在通过Arras.asList过后是一样的吗??
根据我们前面说的Arrays.asList是泛型T,那么int是基本数据类型,所以int[]通过Arrays.asList过后只有一个元素,而且是一个二维数组的元素,而Integer[]t2是引用对象类型,生成是一个长度为4的ArrayList,我们来看下程序:
public class Clien_01 {
public static void main(String[] args) {
int [] t1 = new int[]{1,2,3,4};
Integer t2 []= new Integer[]{1,2,3,4};
System.out.println(Arrays.asList(t1));
System.out.println(Arrays.asList(t2));
}
}
输出:
[[I@1b6d3586]
[1, 2, 3, 4]
第一行代码输出的句柄是[[I就知道一个二维数组,不行我们来dubug看:
一个二维数组 把数据元素都放在了二维数组中;简单来说就是:
基本类型不支持泛型化,会把整个数组当成一个元素放入新的数组
什么是fast-fail
ail-fast机制是java集合中的一种错误机制。
当使用迭代器迭代时,如果发现集合有修改,则快速失败做出响应,抛出ConcurrentModificationException异常。
这种修改有可能是其它线程的修改,也有可能是当前线程自己的修改导致的,比如迭代的过程中直接调用remove()删除元素等。
另外,并不是java中所有的集合都有fail-fast的机制。比如,像最终一致性的ConcurrentHashMap、CopyOnWriterArrayList等都是没有fast-fail的。
fail-fast是怎么实现的:
ArrayList、HashMap中都有一个属性叫modCount,每次对集合的修改这个值都会加1,在遍历前记录这个值到expectedModCount中,遍历中检查两者是否一致,如果出现不一致就说明有修改,则抛出ConcurrentModificationException异常。
ArrayList中的迭代器源码中的checkForComodification就是检查快速失败的手段,其实简单来说就是通过一个期望值和操作数来判断的
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
底层数组存/取元素效率非常的高(get/set),时间复杂度是O(1),而查找(比如:indexOf,contain),插入和删除元素效率不太高,时间复杂度为O(n)。
插入/删除元素会触发底层数组频繁拷贝,效率不高,还会造成内存空间的浪费,解决方案:linkedList
查找元素效率不高,解决方案:HashMap(红黑树)