赞
踩
JavaSE进阶01:继承、修饰符
JavaSE进阶02:多态、抽象类、接口
JavaSE进阶03:内部类、Lambda表达式
JavaSE进阶04:API中常用工具类
JavaSE进阶05:包装类、递归、数组的高级操作、异常
JavaSE进阶06:Collection集合、迭代器、List、ArrayList、LinkedList
JavaSE进阶07:泛型、Set集合、TreeSet、二叉树、红黑树
JavaSE进阶08:HashSet、Map集合、HashMap、TreeMap、可变参数、不可变集合
JavaSE进阶09:Stream流、File类
JavaSE进阶10:IO流、字节流、字节缓冲流
JavaSE进阶11:字符流、字符缓冲流、转换流、对象操作流、Properties集合
JavaSE进阶12:多线程、线程同步、线程池
JavaSE进阶13:网络编程入门、UDP通信程序、TCP通信程序、日志、枚举
JavaSE进阶14:类加载器、反射
JavaSE进阶15:XML、注解、单元测试
JavaSE进阶扩充:JDK8 HashMap底层分析(了解)
JavaSE进阶扩充:JDK8 ArrayList线程安全问题和源码分析、集合常见面试题
Java进阶作业
如果存储的数据长度经常发生改变,推荐使用集合
集合类主要由两个接口派生而出,分别是单列集合Collection和双列集合Map,它们是Java集合框架的根接口。
List
和Set
。其中,List
的特点是元素有序、元素可重复。Set
的特点是元素无序,而且不可重复。List
接口的主要实现类有ArrayList
和LinkedList
,Set
接口的主要实现类有HashSet
和TreeSet
。从上面的描述可以看出JDK中提供了丰富的集合类库,为了便于系统地学习,接下来通过一张图来描述集合类的继承体系。
图中,ArrayList,HashSet,LinkedList,TreeSet是我们经常会有用到的已实现的集合类。
集合框架的学习方式:
步骤
1、扩容:把原来的数组复制到另一个内存空间更大的数组中
2、添加元素:把新元素添加到扩容以后的数组中
先把ArrayList源码(JKD1.8)中定义的一些属性粘出来方便下面源码分析
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
*空数组实例,用于默认大小的空实例
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
*如果新建ArrayList对象时没有指定大小,那么会将
*DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData,
*并在第一次添加元素时,将列表容量设置为DEFAULT_CAPACITY
*/
transient Object[] elementData;
/**
* 列表大小,elementData中存储的元素个数
*/
private int size;
}
首先看一下ArrayList的两个构造方法:
//带初始容量参数的构造函数。(用户自己指定容量)
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//初始容量大于0
//创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {//初始容量等于0
//创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {//初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//默认构造函数,构造一个空列表(无参数构造)
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
在无参构造中,其实就是在用无参构造来创建对象的时候其实就是创建了一个空数组,长度为0,没有分配容量,当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。之后扩容会按照1.5倍增长。
在有参构造中,传入的参数是正整数就按照传入的参数来确定创建数组的大小,否则异常。
接下来重点,看扩容,扩容的方法就是 add(E e)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // list的size+1
elementData[size++] = e; // 将数据放到数组最后一个
return true;
}
通过源码可以发现,其实add方法就两步,第一步:增加长度,第二步:添加元素到数组,第二步没什么说的,我们看看ensureCapacityInternal(int minCapacity)这个增加长度的方法。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
这个地方可以看到,如果在添加的时候数组是空的,就直接给一个默认10的长度,否则的话就加1
if (minCapacity - elementData.length > 0)
grow(minCapacity);
通过上面这个判断才是真正的增加长度,当需要的长度大于原来数组长度的时候就需要扩容了,相反的则不需要扩容。
grow()方法
//要分配的最大数组大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2(如果oldCapacity是奇数,先减1再除以2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
我们来仔细分析一下:
直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容。
以上的一切都是ArrayList扩容的第一步,第二步就没啥说的了,就是把需要添加的元素添加到数组的最后一位
小结:在arraylist中增加一个对象的时候,Java会去检查arraylist,以确保已存在的数组中有足够的容量来存储这个新的对象。如果没有足够容量的话,那么就会新建一个长度更长的数组,旧的数组就会使用Arrays.copyOf方法被复制到新的数组中去,现有的数组引用指向了新的数组。
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
public class Test01 {
private static List<Integer> list = new ArrayList<Integer>();
private static ExecutorService executorService = Executors.newFixedThreadPool(500); //定长线程池500
private static class IncreaseTask extends Thread{
@Override
public void run() {
System.out.println("ThreadId:" + Thread.currentThread().getId() + " start!");
for(int i =0; i < 100; i++){
list.add(i);
}
System.out.println("ThreadId:" + Thread.currentThread().getId() + " finished!");
}
}
public static void main(String[] args){
//返回当前时间
long start = System.currentTimeMillis();
for(int i=0; i < 500; i++){ //开启500个线程
executorService.submit(new IncreaseTask());
}
//停止接收新任务,原来的任务继续执行
executorService.shutdown();
while (!executorService.isTerminated()){//所有提交的任务没完成
try {
Thread.sleep(500*10);
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("list 长度为: " + list.size());
long end = System.currentTimeMillis();
System.out.println("用时:"+(double)(end-start)/1000+"秒");
}
}
/*
打印结果应为:50000,实际测试部分结果如下:
list 长度为: 49972
list 长度为: 49901
由此可见是ArrayList做add操作时候,会丢失一些数据,所以所Array是线程不安全的。
*/
// Object[] elementData:ArrayList的数据结构是数组类型,list存放的数据就是存放在elementData里面的
// 第1步
public boolean add(E e) {
ensureCapacityInternal(size + 1); // list的size+1
elementData[size++] = e; // 将数据放到数组最后一个
return true;
}
// 第2步,判断如果将当前的新元素加到列表后面,列表的elementData数组的大小是否满足,
// 如果size + 1的这个需求长度大于了elementData这个数组的长度,那么就要对这个数组进行扩容
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 进入ensureExplicitCapacity方法
ensureExplicitCapacity(minCapacity);
}
// 第3步,元素有变化,那么就调用grow方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// elementData:list的数组元素
// minCapacity: add操作后的容量
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 第4步
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 为什么要-8,是因为有些虚拟机有一些hear的key
private void grow(int minCapacity) {
// 原始list的容量(容量不是list.size)
int oldCapacity = elementData.length;
//现在list的容量,此时是做讲原始容量扩大1.5倍,oldCapacity >> 1:2进制右位移,就是除以2的意思
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 一般不会进入hugeCapacity这个方法,
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 复制elementData返回一个新的数组对象,这个时候list.add完成
elementData = Arrays.copyOf(elementData, newCapacity);
}
由此看到List集合做add()时,第1步到第3步,都不会改变elementData对象,只有在第4步Arrays.copyOf的时候,返回一个新的数组对象。
因此:当有线程A、B同时进入grow方法,两个线程都会执行Arrays.copyOf方法,返回2个不同的elementData对象,
假如,A先返回,B后返回,那么List.elementData == A.elementData,
然后B也返回后,这时List.elementData == B.elementData
这时,B.elementData就把A.elementData数据给覆盖了。导致A.elementData被丢失
这样就出现了导致线程不安全的隐患,在多个线程进行add操作时可能会抛出并发读写异常和数据丢失,覆盖等问题。
private static List<Integer> list = Collections.synchronizedList(new ArrayList());
进行测试,结果正确,没有发现数据丢失问题。
为什么synchronizedList()方法可以解决并发问题?直接上源码(JKD1.8)
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
public boolean equals(Object o) {
synchronized(mutex) {return list.equals(o);}
}
public int hashCode() {
synchronized(mutex) {return list.hashCode();}
}
public E get(int index) {
synchronized(mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized(mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized(mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized(mutex) {return list.remove(index);}
}
public int indexOf(Object o) {
synchronized(mutex) {return list.indexOf(o);}
}
public int lastIndexOf(Object o) {
synchronized(mutex) {return list.lastIndexOf(o);}
}
public boolean addAll(int index, Collection<? extends E> c) {
synchronized(mutex) {return list.addAll(index, c);}
}
public ListIterator<E> listIterator() {
return list.listIterator(); // Must be manually synched by user
}
public ListIterator<E> listIterator(int index) {
return list.listIterator(index); // Must be manually synched by user
}
public List<E> subList(int fromIndex, int toIndex) {
synchronized(mutex) {
return new SynchronizedList<E>(list.subList(fromIndex, toIndex),mutex);
}
}
关于mutex的定义:
final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
if (c==null)
throw new NullPointerException();
this.c = c;
mutex = this;
}
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = c;
this.mutex = mutex;
}
mutex指向的就是当前对象自己,所以SynchronizedList是线程安全的根本原因是使用Synchronized对SynchronizedList的add,delete等操作进行加锁,但是这种锁的力度很大,效率比较低。
private static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<Integer>();
源码(JKD1.8):
private static final long serialVersionUID = 8673264195747942595L;
transient final ReentrantLock lock = new ReentrantLock();
private volatile transient Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
从add方法知道:CopyOnWriteArrayList底层数组的扩容方式是一个一个地增加,而且每次把原来的元素通过Arrays.copy()方法copy到新数组中,然后在尾部加上新元素e.它的底层并发安全的保证是通过ReentrantLock进行保证的,CopyOnWriteArrayList和SynchronizedList的底层实现方式是不一样的,前者是通过Lock机制进行加锁,而后者是通过Synchronized进行加锁。
ArrayList的优点是可以随机访问,因为它有下标;缺点是增删麻烦,效率低,因为它需要整体的移位,需要不断的arraycopy,很耗时间。所以,如果我们要展示的数据不需要进行排序,访问元素比插入或者是删除元素更加频繁的时候,应该使用ArrayList。如果要展示的数据需要排序或者是删除元素更加频繁或者压根就不需要访问元素的时候,那就不要用ArrayList了,而是选择选择LinkedList。
ArrayList newArray = oldArray.clone();
1.ArrayList和Vector底层是数组,LinkedList底层是链表
2.Vector线程同步,ArrayList和LinkedList线程不同步,可通过Collections.synchronizedList()实现线程同步
3.ArrayList和Vector增删慢,查找快。LinkedList增删快,查找慢。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。