1.线程安全
集合都是非线程安全的,这里说的非线程安全指的是集合类作为共享变量,被多线程读写的时候是不安全的,如果要实现线程安全的集合,可以使用Collections类中以synchronized开头的相关方法, Collections实现了List、Set、Map对应的线程安全方法。下面以synchronizedList为例,来分析Collections是如何实现线程安全的,具体源码如下所示。
源码
public class Collections {
//mutex为需要锁住的对象
final Object mutex;
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
//需要保证线程安全的类
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
//List所有方法都使用了synchronized关键字进行加锁
//synchronized是一种悲观锁,能够保证同一时刻,只能有一个线程能够获得锁
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);
}
}
}
}
源码解析
从源码中可以看到Collections是通过synchronized关键字给List相关方法加锁来实现线程安全的。
2.性能
集合的单个操作,一般都没有性能问题,性能问题主要出现在批量操作上。如List和Map在新增大量数据时,建议不要使用for循环单个add和put方式进行新增,因为这种方式会有很大的扩容成本,应该使用addAll和putAll方法进行新增,下面以ArrayList为例来演示两种方案的性能对比。
public class Demo {
public static void main(String[] args) {
//准备拷贝数据
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 3000000; i++) {
list.add(i);
}
//for循环的add方式
ArrayList<Integer> list2 = new ArrayList<>();
long start1 = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
list2.add(list.get(i));
}
System.out.println("for循环新增300w数据,耗时:" + (System.currentTimeMillis() - start1) + "ms");
//批量新增addAll方式
ArrayList<Integer> list3 = new ArrayList<>();
long start2 = System.currentTimeMillis();
list3.addAll(list);
System.out.println("批量新增300w数据,耗时:" + (System.currentTimeMillis() - start2) + "ms");
}
}
程序执行结果如下所示,可以看到,批量新增方式性能是单个新增方式的100倍左右,主要原因在于批量新增,只会扩容一次,大大缩短了运行时间,而单个新增,每次到达扩容阀值时,都会进行扩容,在整个过程中就会不断的扩容,从而影响程序的执行效率。
for循环新增300w数据,耗时:1107ms
批量新增300w数据,耗时:11ms
批量新增的源码如下所示,整个批量新增的过程中,只扩容了一次,HashMap的putAll方法也是如此,整个新增过程只会扩容一次,大大缩短了批量新增的时间,提高了性能。所以在容器初始化的时候,最好能给容器赋上初始值,这样可以防止在put的过程中不断扩容。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
//确保容量充足,整个过程只会扩容一次
ensureCapacityInternal(size + numNew);
//进行数组的拷贝
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
}
3.数组转化成List
数组转化成List时,常使用Arrays的asList方法,这个方法有两点需要注意,下面通过一个示例演示,代码如下所示。
public class ArrayToList {
public static void main(String[] args) {
Integer [] array = new Integer[]{1, 2, 3, 4, 5, 6};
List<Integer> list = Arrays.asList(array);
//注意点1:修改数组的值,会直接影响原list
System.out.println("数组被修改之前,集合第一个元素为:" + list.get(0));
array[0] = 10;
System.out.println("数组被修改之后,集合第一个元素为:" + list.get(0));
//注意点2:使用add、remove等方法操作list时,会报UnsupportedOperationException异常
list.add(7);
}
}
程序执行结果如下所示。
数组被修改之前,集合第一个元素为:1
数组被修改之后,集合第一个元素为:10
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at com.leichuangkj.threads.Id.main(Id.java:18)
上述两点产生的根本原因如下面的源码所示。
源码
public class Arrays {
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
//此处new的ArrayList并不是java.util.ArrayList,而是下面的静态类
return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable{
private static final long serialVersionUID = -2764017481108945198L;
//这里的数组直接指向转换的数组
private final E[] a;
ArrayList(E[] array) {
//初始化的时候,直接持有外面的数组,所以当数组的元素发生变化时,集合元素也会跟着变化
a = Objects.requireNonNull(array);
}
}
}
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
//ArrayList并没有实现add方法,当调用add方法时,实际上调用的是AbstractList的add方法,该方法会抛出一个异常
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
}
4.List转化成数组
List转化成数组,通常使用toArray方法,这个方法有以下三点值得注意,分别是数组大小小于、等于和大于集合元素个数的情况,具体示例如下所示。
public class ListToArray {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>() {{
add(1);
add(2);
add(3);
add(4);
}};
//下面这行被注释的代码这么写编译是无法通过的,无法转化成数组的,原因是无参toArray返回的是Object[],无法向下转化成List<Integer>
// List<Integer> list2 = list.toArray();
//演示数组初始化大小小于实际所需大小时,得到元素值为null的情况
Integer[] array0 = new Integer[2];
list.toArray(array0);
System.out.println("数组大小小于集合元素个数时,array0数组下下标[0]的值是" + array0[0] + "下标[1]的值是" + array0[1]);
//演示数组初始化大小正好等于实际所需大小时,正好可以转化成数组
Integer[] array1 = new Integer[list.size()];
list.toArray(array1);
System.out.println("数组大小等于集合元素个数时,array0数组下下标[0]的值是" + array0[0] + "下标[1]的值是" + array0[1]);
//演示数组初始化大小大于实际所需大小时,也可以转化成数组
Integer[] array2 = new Integer[list.size() + 2];
list.toArray(array2);
System.out.println("数组大小大于集合元素个数时,array0数组下下标[0]的值是" + array0[0] + "下标[1]的值是" + array0[1]);
}
}
程序执行结果如下所示。
数组大小小于集合元素个数时,array0数组下下标[0]的值是null,下标[1]的值是null
数组大小等于集合元素个数时,array1数组下下标[3]的值是4
数组大小大于集合元素个数时,array2数组下下标[3]的值是4,下标[4]的值是null
toArray的无参方法,无法强转成具体类型,这个在编译的时候,就会有提醒,因此一般都会使用带有参数的toArray方法,这时就有一个坑,如果参数数组的大小不够,返回的数组值会为空,上述代码中的array0的返回值就体现了这点,但去看toArray源码,发现源码中返回的是4个都不相同的数据,返回的并不是空,具体源码如下所示。
源码
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable,java.io.Serializable {
public <T> T[] toArray(T[] a) {
//如果数组长度不够,按照List的大小进行拷贝,return的时候返回的都是正确的数组
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);
//数组长度大于List大小时,赋值为null
if (a.length > size)
a[size] = null;
return a;
}
}
源码解析
从源码中,丝毫看不出为什么array0的元素值会是null,最后通过看方法的注释可以获取答案,翻译过来的意思是说,如果返回的数组大小和申明的数组大小一致,那么就会正常返回,否则,一个新数组就会被分配返回。所以在使用有参toArray方法时,申明的数组大小一定要大于等于List的大小,如果小于的话,就会得到一个空数组。