Java学习之ArrayList

ArrayList集合介绍:

List接口的可调整大小的数组实现。

 

一.ArrayList底层数据结构——数组:

数组:一旦初始化长度就不可以发生改变。

add

数组的增加元素怎么做?eg:已知数组长度为4,分别是9,5,2,7,如何追加一个元素?

答:创建一个新数组,把原来4个长度的数组元素 一 一 拷贝进来,然后在新数组后面添加一个元素。

delete:

eg:已知数组长度为4,分别是9,5,2,7,如何删除索引1对应的元素?

删除索引1对应元素5,然后让这之后的元素往前移动。变成9,2,7.整个数组长度发生改变,元素位置也发生改变了。

update:

eg:已知数组长度为4,分别是9,5,2,7,如何将索引1元素修改为6?

直接根据索引就能修改。

get:

eg:已知数组长度为4,分别是9,5,2,7,如何获取数组索引1对应的元素?

直接根据索引取出对应元素。

数组优缺点:

1.数组查询快,根据地址和索引直接获取元素。

2.数组增删慢,每次都要创建新数组且移动元素位置。

二.ArrayList继承与实现关系

   4个实现:

(1)实现了Serializable标记接口。

(2)实现了Clonable标记接口。(想要克隆必须实现clonable接口,重写clone方法。克隆出来的对象和原对象地址不等,内容相等)

(3) 实现RandomAccess标记接口。

(4)实现list接口。

  1个继承:

(1)继承AbstractList类。-提供list接口的框架。

三.ArrayList随机访问和顺序访问效率对比。

随机访问效率比顺序访问(迭代器)效率高。(因为ArrayList实现了RandomAccess接口,只有实现了这个接口,才会这样)

public class Demo01 {
	public static void main(String[] args) {
		List<String>list=new ArrayList<>();
		for(int i=0;i<100000;i++)
			list.add(i+"a");
		//测随机访问时间
		//开始t
		long startTime=System.currentTimeMillis();
		for(int i=0;i<100000;i++)
			list.get(i);
		//结束t
		long endTime=System.currentTimeMillis();
		System.out.println(endTime-startTime);
		
		//测顺序访问时间
		startTime=System.currentTimeMillis();
		Iterator<String>it=list.iterator();
		while(it.hasNext())
		{
			it.next();
		}
		endTime=System.currentTimeMillis();
		System.out.println(endTime-startTime);
	}
   //最后输出为1  2
}

三.ArrayList分析 

3.1构造方法

ConstructorConstructor描述
ArrayList()构造一个初始容量为10的空列表
ArrayList(int initialCapacity)构造具有指定初始容量的空列表
ArrayList(Collection<?extends E> c)构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序

 

//DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的数组
public ArrayList(){
 this.elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

 

3.2 添加方法

方法名描述
public boolean add(E e)将指定元素追加到这个列表的末尾
public void add(int index,E element)将此列表中的指定位置插入指定的元素
public boolean addAll(Collection<? extends E>c )

按指定集合的Iterator返回的顺序将指定集合中的所有元素

追加到此列表的末尾

public boolean addAll(int index,Collection<? extends E>c)将指定集合中的所有元素插入到此列表中,从指定的位置开始

 

方法一:

public boolean add(E e)
{
   //校验扩容
   ensureCapacityInternal(size+1);
   elementData[size++]=e;
   return true;
}
private void ensureCapacityInternal(int minCapacity){
 ensureExplicitCapacity(calculateCapacity(elementData,minCapacity));
}
private static int calculateCapacity(Object[]elementData,int minCapacity){
 if(elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
  //默认容量10和前一个方法的size+1取最大值,赋值给最小容量
  minCapaticy=Math.max(DEFAULT_CAPACITY,minCapacity);
  }
 return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity){
  //修改集合的次数++
  modCount++;
  //判断容量够不够,不够就扩容
  if(minCapacity-elementData.length>0)
  grow(minCapacity);
}
private void grow(int minCapacity){
   //将存元素的数组的长度赋给旧容量
   int oldCapacity=elementData.length;
   //新容量为旧容量的1.5倍
   int newCapacity=oldCapacity+(oldCapacity>>1);
   //看新容量-最小容量是不是<0
   if(newCapacity-minCapacity<0)
    newCapacity=minCapacity
   //新值-Integer的最大值是否>0
   if(newCapacity-MAX_ARRAY_SIZE>0)
    newCapacity=hugeCapacity(minCapacity);
   //调用copyOf进行元素的一个拷贝
   elementData=Arrays.copyOf(elementData,newCapacity);
}
private static int hugeCapacity(int minCapacity){
 if(minCapacity<0)
  throw new OutOfMemoryError();
 return (minCapacity>MAX_ARRAY_SIZE)?Integer.MAX_VALUE:MAX_ARRAY_SIZE;
}

方法二:

public void add(int index,E element){
  //检验索引
  rangeCheckForAdd(index);
  //校验扩容,同上一个方法
  ensureCapacityInternal(size+1);
  //指定索引为index,从index+1拷贝,拷贝size-index个
  System.arraycopy(elementData,index,elementData,index+1,size-index);
  elementData[index]=element;
  size++;
}
private void rangeCheckForAdd(int index){
  //检验索引有没有大于数组长度或者小于0
  if(index>size||index<0)
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

方法三:

public boolean addAll(Collection<? extends E> c){
  //把集合转成数组
  Object[] a=c.toArray();
  //有数据集合的长度赋给numNew
  int numNew=a.length;
  //size是新集合的长度,numNew是c长度
  ensureCapacityInternal(size+numNew);
  //把a数组元素拷贝到elementData中
  System.arraycopy(a,0,elementData,size,numNew);
  size+=numNew;
  return numNew!=0;
}

方法四:

public boolean addAll(int index,Collection<? extends E> c){
  //检验索引
  rangeCheckForAdd(index);
  //将集合(数据源)转成数组
  Object[] a=c.toArray();
  //记录集合长度
  int numNew=a.length;
  //校验扩容
  ensureCapacityInternal(size+numNew);
  //numMoved代表要移动元素的个数=集合真实长度-索引位置
  int numMoved=size-index;
  //判断是否需要移动元素
  if(numMoved>0)
   //源数组,从哪个位置,移动到目标数组,目标数组起始位置,要复制目标数组个数
   //这一步只是移动,数据源的元素还没有复制
   System.arraycopy(elementData,index,elementData,index+numNew,numMoved);
  //这才是真正将数据源中所有数据添加到数据目的
  System.arraycopy(a,0,elementData,index,numNew);
  size+=numNew;
  return numNew!=0;
}

3.3 修改(set)方法

public E set(int index,E element):根据索引修改集合元素

public E set(int index,E element){
 //校验索引
 rangeCheck(index);
 //把索引对应元素取出来赋值给oldValue
 E oldValue=elementData(index);
 //把要修改后的值存到数组中
 elementData[index]=element;
 //返回被替换的元素
 return oldValue;
}
private void rangeCheck(int index){
  if(index>=size)
  throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

3.4 获取(get)方法

public E get (int index) :根据索引获取元素

public E get(int index){
 rangeCheck(index);
 return elementData[index];
}

3.5 toString方法

它不是ArrayList的方法,而是AbstractCollection的方法。但是ArrayList继承了AbstractList,AbstractList继承了AbstractCollection

public String toString():把集合所有数据转换成字符串

public String toString(){
  //获取迭代器
  Iterator<E>it=iterator();
  //判断迭代器是否有元素
  if(!it.hasNext())
  return "[]";
  //创建StringBuilder
  StringBuilder sb=new StringBuilder();
  sb.append('[');
  //无限循环
  for(;;){
  //调用迭代器的next方法取出元素,且向下移动
  E e=it.next();
  sb.append(e==this?"(this Collection)" :e);
  //没有元素了,追加],且把整个缓冲区数据转成字符串,且结束死循环
  if(!it.hasNext())
    return sb.append(']').toString();
  sb.append(',').append(' ');
}

3.6 迭代器

  public Iterator<E> iterator()普通迭代器

用迭代器遍历集合每个元素:

public static void main(String[] args) {
		List<String>list=new ArrayList<>();
		list.add("Hello");
		list.add("every");
		list.add("body!");
		Iterator<String>it=list.iterator();
		while(it.hasNext())
		{
			String s=it.next();
			System.out.println(s);
		}
		
	}

源码:

public Iterator<E>iterator(){
 //创建了一个对象
 return new Itr();
}

 

private class Itr implements Iterator<E>{
 int cursor;//光标,默认值0
 int lastRet=-1;
 //将集合实际修改次数赋给预期修改次数
 int expectedModCount=modCount;
 //判断集合是否有元素
 public boolean hasNext(){
  //判断光标是否不等于size
  return cursor!=size;
  }

  public E next(){
   //校验预期修改次数是否和实际修改次数一样
    checkForComodification();
   //光标赋给i
    int i=cursor;
   //判断是否大于集合size,说明没有元素了
    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];
}

通过迭代器遍历查找元素,如果有就删除(删除的是最后一个元素):

public static void main(String[] args) {
		List<String>list=new ArrayList<>();
		list.add("Hello");
		list.add("my");
		list.add("friend");
		Iterator<String>it=list.iterator();
		while(it.hasNext())
		{
			String s=it.next();
			if(s.equals("friend"))
				list.remove("friend");
		}
	}

会有并发修改异常。

为什么?

add()完以后modCount++,实际修改次数=3,预期修改次数=3,删除完元素,modCount++,实际修改次数=4,下次再遍历时,(cursor=3)!=(size=2),因此还会往下执行,调用next方法,然后校验预期修改次数和实际修改次数不一样,就会有异常。

通过迭代器遍历查找元素,如果有就删除(删除的是集合倒数第二个元素):

结果:成功,没有异常

为什么?

因为hasNext()中光标的值cursor=size的值,不会调用next(),就不会产生并发修改异常。

3.6 1 remove()

这个方法是个默认方法,jdk得达到1.8

default void remove():删除集合中元素

1.迭代器调用remove方法删除元素,实际上还是调用集合自己的remove方法删除元素

2.调用remove方法每次都给预期修改次数赋值,不会产生并发修改异常

//迭代器自带方法
public void remove(){
 if(lastRet<0)
    throw new IllegalStateException();
 checkForComodification();
 try{
  ArrayList.this.remove(lastRet);
  cursor=lastRet;
  //实际修改次数赋值给预期修改次数
  expectedModCount=modCount;
 }catch(IndexOutOfBoundsException ex){
   throw new ConcurrentModificationException();
 }
}
//集合删除的方法
public E remove(int index){
 //校验索引
 rangeCheck(index);
 //实际修改次数++
 modCount++;
 //取出修改索引的数值赋给oldValue;
 E oldValue=elementData(index);
 //移动元素个数=实际元素个数-索引之前的-这个索引的
 int numMoved=size-index-1;
 if(numMoved>0)
  System.arraycopy(elementData,index+1,elementData,index,numMoved);
 elementData[--size]=null;
 return oldValue;
}

3.7clear()

public void clear():清空集合所有数据

public void clear(){
 modCount++;
 for(int i=0;i<size;i++)
  elementData[i]=null;
 size=0;
}

3.8 contains()

public boolean contains(Object o):判断集合是否包含指定元素

public boolean contains(Object o){
  return indexOf(o)>=0;
}
public int indexOf(Object o){
 //判断要找的元素是否为null
 if(o==null){
   //遍历集合
   for(int i=0;i<size;i++)
    if(elementData[i]==null)
        //找到之后返回该元素索引
        return i;
 }else{
   //遍历集合
   for(int i=0;i<size;i++)
    //拿要找的元素和集合元素比较
    if(o.equals(elementData[i]))
       //返回该元素所在集合索引
       return i;
 }
 return -1;
}

3.9 isEmpty()

public boolean isEmpty():判断集合是否为空

public boolean isEmpty(){
  return size==0;
}

四.面试题

1.ArrayList怎么扩容的?

第一次扩容10,以后每次是原容量的1.5倍

2.ArrayList频繁扩容导致性能急剧下降,怎么办?

指定初始容量,就不用扩容了。

3.ArrayList插入或删除元素一定比LinkedList慢吗?

不一定。 

ArrayList的remove源码:

//集合删除的方法
public E remove(int index){
 //校验索引
 rangeCheck(index);
 //实际修改次数++
 modCount++;
 //取出修改索引的数值赋给oldValue;
 E oldValue=elementData(index);
 //移动元素个数=实际元素个数-索引之前的-这个索引的
 int numMoved=size-index-1;
 if(numMoved>0)
  System.arraycopy(elementData,index+1,elementData,index,numMoved);
 elementData[--size]=null;
 return oldValue;
}

LinkedList的remove源码

public E remove(int index){
 checkElementIndex(index);
 return unlink(node(index));
}
private void checkElementIndex(int index){
 if(!isElementIndex(index))
    throw new IndexOutOfBoundsException();
}
private boolean IsElementIndex(int index){
 return index>=0&&index<size;
}
//找元素的方法
Node<E>node(int index){
 //判断index是否小于集合长度的一半
 if(index<(size>>1)){
   //如果小于就把第一个节点赋给x
   Node<E>x=first;
   //从头往后找,获取下一个节点
   for(int i=0;i<index;i++)
    x=x.next;
   return x;
  }else{
   Node<E>x=last;
   //从后往前找
   for(int i=size-1;i>=0;i--)
    x=x.prev;
  return x;
}
   
E unlike(Node<E>x){
  //这个节点的值
  final E element =x.item;
  //下一个节点
  final Node<E>next=x.next;
  //上一个节点
  final Node<E>prev=x.prev;
  
  if(prev==null){
     first=next;
  }else{
   prev.next=next;
   x.prev=null;
  }
  
  if(next==null){
   last=prev;
  }else{
   next.prev=prev;
   x.next=null;
  }
  
   x.item=null;
   size--;
   modCount++;
   return element;
}

由此看来效率不见得比ArrayList快多少。

4.ArrayList是线程安全的吗?什么时候需要同步?

不是。解决方法:synchronized、Lock或者用vector。

集合不是成员变量时,不需要同步,每一个线程都有一个独立的方法,不是共享数据,就不用加锁。

反之,成员变量是被所有线程共享的,所以需要。

5.如何复制一个ArrayList到另一个ArrayList去?

(1)clone()方法

(2)ArrayList构造方法

(3)addAll()

6.已知成员变量集合存储N多用户名称,在多线程情况下,用迭代器读取集合数据时如何保证还可以正常写入数据到集合?

有个集合可以让读写分离:CopyOnWriteArrayList

7.ArrayList和LinkedList的区别?

ArrayList:

基于动态数组的数据结构。

对于随机访问get,set,ArrayList优于LinkedList

对于随机操作的add,remove,ArrayList不一定比LinkedList慢(因为ArrayList底层是基于动态数组,因此不用每次add和remove的时候都需要创建新数组)

LinkedList:

基于链表的数据结构

对于顺序操作,LinkedList不一定比ArrayList慢

对于随机操作,LinkedList效率明显比较低

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值