Java集合框架:ArrayList HashSet HashMap
之前一直处于糊弄自己糊弄别人的状态,从来没想过去看官方文档,马上要找工作了再这样下去不行啊。花了一些时间重新把集合看完了,在这里总结一下,加深一下记忆,也希望以后多复习一下!
ArrayList:
ArrayList的底层实现:
List 接口的大小可变数组(动态数组)的实现类,底层使用数组保存所有元素,能够动态的实现元素的增加与删除。它继承了AbstractList<E>类,实现了List、RandomAccess、Cloneable、Serializable接口。
特点:
1.元素有序
2.允许有空元素(null)
3.元素可重复
4.此实现不同步,线程不安全,解决方法:
01.使用synchronized关键字
02.使用Collections.synchronizedList();
1 //创建一个线程不安全的ArrayList
2 List<Map<String,Object>> data=new ArrayList<Map<String,Object>>();
3 //线程安全的
4 List<Map<String,Object>> data=
5 Collections.synchronizedList(new ArrayList<Map<String,Object>>());
构造方法:
1.ArrayList()
构造一个初始容量为 10 的空列表。
2.ArrayList(Collection<? Extends E> c)
构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
3.ArrayList(int initialCapacity)
构造一个具有指定初始容量的空列表。
常用方法:
1.boolean add((index),E e);
将指定的元素添加到此列表的尾部
2.void add(int index,E e);
插入元素到指定的位置
3.boolean addAll(Collection<? extends E> c)
按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。
4.get(int index)
返回此列表中指定位置上的元素。
5.boolean isEmpty()
如果此列表中没有元素,则返回 true
6.remove(int index)
移除此列表中指定位置上的元素。
7.set(int index, E element)
用指定的元素替代此列表中指定位置上的元素。
8.int size()
返回当前列表元素的数量
9.boolean contains(Object o)
- 如果此列表中包含指定的元素,则返回 true
HashMap
Map家族最常用的实现类,基于哈希表的 Map 接口的实现,底层数据结构是单链表与数组,即数组的每一个元素都是链表。其存储结构如图:
关于HashMap有几个重要的问题,花了很多时间才搞明白,记录一下:
HashMap无参构造器会生成一个初始大小为16,负载因子为0.75的实例。
01.Hashmap为什么大小是2的幂次?
HashMap源码中有这么一句话:The default initial capacity - MUST be a power of two.即HashMap的实例的容量必须(MUST)为2的倍数,但是为什么呢?又如何来保证呢?
首先解释为什么(转自:原文地址):因为效率问题,先来看看HashMap源码中获取元素的位置的方法:
1 static int indexFor(int h, int length) { 2 // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; 3 return h & (length-1); 4 }
解释:
h:为插入元素的hashcode length:为map的容量大小 &:与操作 比如 1101 & 1011=1001
如果length为2的次幂 则length-1 转化为二进制必定是11111……的形式,在和h的二进制与操作效率会非常的快,
而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在和h进行与操作后的结果最后一位都为0,
那么0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了(因为这几个地址没法查到),空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
下面解释如何保证用户通过构造器自定义容量来获取的实例的容量也是2的次幂(部分转自:原文地址):
还是源码,看一下HashMap第三个构造方法源码,其它构造方法最终调用的都是它。
1 public HashMap(int initialCapacity, float loadFactor) { 2 // 参数判断,不合法抛出运行时异常 3 if (initialCapacity < 0) 4 throw new IllegalArgumentException("Illegal initial capacity: " + 5 initialCapacity); 6 if (initialCapacity > MAXIMUM_CAPACITY) 7 initialCapacity = MAXIMUM_CAPACITY; 8 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 9 throw new IllegalArgumentException("Illegal load factor: " + 10 loadFactor); 11 12 // Find a power of 2 >= initialCapacity 13 // 这里需要注意一下 14 int capacity = 1; 15 while (capacity < initialCapacity) 16 capacity <<= 1; 17 18 // 设置加载因子 19 this.loadFactor = loadFactor; 20 // 设置下次扩容临界值 21 threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); 22 // 初始化哈希表 23 table = new Entry[capacity]; 24 useAltHashing = sun.misc.VM.isBooted() && 25 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); 26 init(); 27 }
重点在第14-16行代码,这里做了一个移位运算,保证了初始容量一定为2的幂,假如你传的是5,那么最终的初始容量为8。
02.Hashmap如何变线程安全,每种方式的优缺点
001.替换成Hashtable,Hashtable通过对整个表上锁实现线程安全,因此效率比较低
002.使用Collections类的synchronizedMap方法包装一下。方法如下:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)映射
003.使用ConcurrentHashMap,它使用分段锁来保证线程安全
通过前两种方式获得的线程安全的HashMap在读写数据的时候会对整个容器上锁,效率低,而ConcurrentHashMap并不需要对整个容器上锁,它只需要锁住要修改的部分就行了,效率最高。
HashSet
HashSet没什么好讲的,底层是HashMap的实例,通过HashMap的键存储数据,具有HashMap的基本属性。
主要说一说关于HashSet中判断重复对象的原理:
HashSet判断两个对象是否相等分两步,第一步大大的提高了数据的操作速度:
01.判断两个对象的hashcode是否相等,不相等则说明两个对象一定不相同(Object的规范),如果相等(很少出现)则进行下一步判断;
02.判断两个对象的equals方法(底层是使用‘==’判断两个对象的地址),成立则一样,不成立则不一样。
可以通过修改hashcode的生成方法与equals方法的方式来修保证HashSet能按我们要求存储数据。