接口Collection有两个派生接口,一个List,一个Set,Queue继承自Collection,通常用LinkedList实现
接口List的实现类有ArrayList(基于动态数组),LinkedList(基于双向链表),Vector(线程安全的)
栈Stack继承自Vector,基本的push和pop方法,peek得到栈顶元素,empty判断堆栈是否为空,search检测一个元素在堆栈中的位置
接口Set的实现类有HashSet,LinkedHashSet,TreeSet 特点:无序,元素不重复
接口Map有一个派生接口ConcurrentMap,两个实现类,HashTable,HashMap
1、HashMap
-
hash算法:
static final int hash(Object key){ int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
在hash表中查找下标时使用的是:(length - 1)& hash = hash % length, 其中length是hash表的容量
将hashCode值无符号右移16位,也就是 int 类型的一半,刚好将该二进制数对半切开,并使用按位异或,则尽量打乱了hashCode的低16位,从而降低hash冲突,当table的length较小时,使得高16位也参与了运算。
-
自动将容量控制为 2 的 n 次幂
static final int tableSizefor(int cap){ int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n+1; }
其中MAXIMUM_CAPACITY = 2^30, 之所以不是2^31,是因为二进制中数字中的最高位是符号位
int n = cap - 1; 之所以要先减一,是为了保证传入的参数 cap 本就为 2 的 n 次幂的情况下,不会变为原来的2倍。该方法会在实例化的时候调用。
jdk1.7中使用数组+链表实现,使用Entry来代表每个节点
jdk1.8中使用数组+链表/红黑树实现,使用Node来代表节点,含有hash,key,value,next属性,当转换为红黑树时,用TreeNode
-
与hashtable的比较
- hashtable不要求底层数组的容量一定是2的 n 次幂,初始容量是11;hashmap则有要求,初始容量为16
- 扩容时hashtable变为原来的2倍+1(oldCap*2+1);hashmap变为原来的2倍(newCap = oldCap<<1)
- 确定下标时,hashtable用的是取模运算;hashmap用的是&操作,效率高
- hashtable直接取的key.hashCode,而hashmap重新计算了key的hash值,能更有效的避免hash冲突
- hashtable中的key-value都不允许为null;hashmap可以,但是只能有一个null的key,value可以有多个
- hashtable是线程安全的,是因为都是synchronized的方法;hashmap是非线程安全的
2、ConcurrentHashMap
-
jdk1.7中的实现原理:
采用数组 + segment + 分段锁实现
segment是类似于hashmap的结构,每一个分段都类似于一个hashmap,分段锁继承自ReentrantLock
ConcurrentHashMap定位一个元素需要进行两次hash操作,第一次定位到segment,第二次定位到元素所在的链表的头部
优点:对元素所在的segment加锁,从而提高了并发能力
缺点:hash过程比普通的hashmap要长
-
jdk1.8中的实现原理:
参考了hashmap,采用数组 + 链表/红黑树
内部采用CAS + Synchronized来保证线程安全,1.8中彻底放弃了segment,而采用Node来存储节点。其中val 和 next 使用volatile来修饰,保证并发的可见性。
而且1.8中的锁粒度更小,锁每个组;1.7中锁的是segment
CAS:是一种乐观锁的实现方式,包含三个操作数 内存位置(V) 预期原值(A) 和 新值(B)
若 V == A,则将 V 更新为 B,否则通过无限循环来获取数据
CAS缺点:
- cpu 开销大,在并发量比较高的情况下,如果许多线程循环往复尝试更新某一个变量,却又一直更新不成功,循环往复,,会给cpu带来很大的压力
- 不能保证代码块的原子性,只能保证一个变量的原子操作
3、ArrayList
-
默认容量为10;其实在初始化的时候,若不指定容量大小,其实创建的是一个空的对象数组。真正的指定默认容量为10,是在进行add操作时,先进行容量判断。若初始为空数组,则将容量设置为默认容量10。然后再去判断是否需要进行扩容。
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);//扩容为 1.5倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); } //由于在进行进行扩容时,需要进行元素的复制,比较消耗性能,所以在创建时应该指定合适的大小
-
与LinkedList比较
- ArrayList是基于动态数组实现;LinkedList基于双向链表实现
- 随机访问元素时,ArrayList的效率高于LinkeList,LinkedList是线性存储,查找需要遍历
- 对数据进行增删操作时,LinkedList效率高于ArrayList,ArrayList是数组,增删操作需要进行数据的移动