16.集合源码对实际工作的帮助

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的大小,如果小于的话,就会得到一个空数组。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值