Java集合框架
Java集合框架
总体框架
Java集合可划分为四部分:List列表、Set集合、Map集合、工具类(Iterator 迭代器、Enumeration 枚举类、Arrays 和Collections)。总体框架如图:
1)Collection 是一个单只接口,包含了List 列表和Set 集合量大分支。其中:
- List 是一个有序元素可重复的列表,每个元素都对应一个索引。第一个元素的索引值是0。List 的实现类有ArraysList 、LinkedList 、 Vector和Stack。(1) ArrayList 优点: 底层数据结构是数组,查询快,增删慢。 缺点: 线程不安全,效率高。(2)Vector 优点: 底层数据结构是数组,查询快,增删慢。 缺点: 线程安全,效率低。(3)LinkedList 优点: 底层数据结构是链表,查询慢,增删快。 缺点: 线程不安全,效率高。
- Set 是一个不允许有重复元素的集合。Set的实现类有HashSet和TreeSet。其中,HashSet实际上是通过HashMap实现的,底层存储数据结构是哈希表,无序,唯一;TreeSet 实际上是通过TreeMap实现的,底层数据存储结构是红黑树,有序,唯一。LinkedHashSet 底层存储结构是链表和哈希表,FIFO插入顺序排序,唯一。
2)Map 是一个映射接口,即key-value键值对。每个元素包含“一个key值”和“key对应的value值”。
- AbstractMap 是个抽象类,他实现了Map接口中的大部分API。而HashMap、LinkedHashMap、TreeMap都继承了AbstractMap。Hashtable 虽然继承于Dictionary,但他也实现了Map 接口。
3)Iterator 是遍历集合的工具,通常通过Iterator 迭代器来遍历集合。Collection依赖于Iterator 接口是因为Collection 的实现类都要实现接口中的iterator()很少,他返回以个Iterator 对象。
4)**Enumeration(枚举类)**是JDK1.0引入的抽象类。作用和Iterator一样,也是遍历集合,但是Enumeration 的功能要比Iterator 少。且只能在Hashtable 、Vector、Stack中使用。
5)Arrays 和Collections 是操作数组、集合的两个工具类。
List 总结
ArrayList 是一个数组列表,相当于动态数组。他有数组实现,随机访问效率高,随机插入、随机删除效率低。
LinkedList 是一个双向链表。他可以被当做栈、队列或双端队列进行操作。LinkedList 随机访问效率低,但随机插入、随机删除效率高。
Vector 是矢量队列,和ArrayList 一样,也是一个动态数组,有数组实现。但是LinkedList 是非线程安全的,而Vector 是线程安全的。
Stack 是栈,他继承于Vector 。他的特性是:先进后出(FILO, First In Last Out)。
Set 总结
HashSet 是基于HashMap 实现的,底层使用HashMap 来保护所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap 的相关方法来完成。基本属性如下:
//基于HashMap实现,底层使用HashMap保存所有元素 private transient HashMap<e,Object> map; //定义一个Object 对象作为HashMap的value private static final Object PRESENT = new Object();
与HashSet 完全相似的是,TreeSet 是基于TreeMap 实现的,且TreeSet 里绝大部分方法都是直接调用 TreeMap的方法来实现。
HashMap 详解
文章解释了Java.util.HashMap 的实现,描述了Java 8 实现中添加的新特性,并讨论性能、内存以及使用HashMap时的一些已知问题。内部存储、自动调整大小、线程安全、键的不变性、Java8 的改进、内存开销、性能问题。
1、内部存储
HashMap 使用了一个内部类Entry<K,V>来存储数据。这个内部类是一个简单的键值对,并带有额外两个数据:
- 一个指向其他入口(引用对象)的引用,这样HashMap 可以存储类似链接列表这样的对象。
- 一个用来代表键的哈希值,存储这个值可以避免HashMap 在没戏需要时都重新生成键所对应的哈希值。
下面是Entry<K,V>在Java 7 下的一部分代码:注意(hashMap 的key和value 可以为null,而hashtable 不可以)
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; … }
HashMap 将数据存储到多个单向Entry 链表中(有时也被称为桶bucket或者容器orbins)。所有的列表都被注册到一个Entry 数组中(Entry<K,V>[] 数组),这个内部数组的默认长度是16。
下面这幅图描述了HashMap 实例的内部存储,他包含一个nullable 对象组成的数组。每个对象都链接到另外一个对象,这样就构成了一个链表。
所有具有相同哈希值得键都会被放到同一个链表(桶)中,具有不同哈希值得键最终可能会在同台的桶中。
当用户调用put(K key, V value)或者get(Object key)时,程序会计算对象应该在桶的索引。然后,程序会迭代遍历对应的列表,来寻找具有相同键的Entry 对象(使用键的equals()方法)。
对于调用get()的情况,程序会返回值所有对应的Entry 对象(如果Entry 对象存在)。
对于调用put(K key, V value)的情况,如果Entry 对象已经存在,那么程序会将值替换为新值,否则,程序会在单向链表的表头创建一个新的Entry(从参数中的键和值)。
桶(链表)的索引,是通过map的3个步骤生成的:
- 首先获取键的散列码( int hash)。
- 程序重新散列码,来阻止针对键的糟糕的哈希函数,因为这有可能会将所有的数据都放到内部数组的相同的索引(桶)上。
- 程序拿到重新后的散列码,并对其使用数组长度(最小是1)的位掩码(bit-mask)。这个操作可以保证索引不会大于数组的大小。可以将去看做是一个经过计算的优化取模函数。
2.自动调整大小
在获取索引后,get()、put()或者remove()方法会访问对应的链表,来查看针对指定键的Entry 对象是否已经存在。在不做修改的情况下,这个机制可能会导致性能问题,因为这个方法需要迭代整个列表来查看Entry对象是否存在。
当你每次使用put(…)方法向Map 中添加一个新的键值对时,该方法会检查是否需要增加内部数组的长度。为了实现这一点,Map存储了2个数据:
- Map的大小:他代表HashMap中记录的条数。我们在向HashMap中插入或者删除值时更新他。
- 阀值:他等于内部数组的长度* loadFactor,在每次调整内部数组的长度时,该阀值也会同时更新。
在添加新的Entry 对象之前,put(…)方法会检查当前Map 的大小是否大于阀值。如果大于阀值,他会创建一个新的数组,数组长度是当前内部数组的两倍。因为新数组的大小已经发生了改变,所以索引函数(就是返回“键的哈希值&(数组长度-1)”的位运算结果)也随之改变。调整数组的大小会创建两个新的桶(链表),并且将所有现存Entry 对象重新分配到桶上。调整数组大小的目标在于降低链表的大小,从而降低put()、get()和remove()方法的执行时间。对于具有相同哈希值的键所对应的所有Entry 对象来说,他们会在调整大小后分配到相同的桶中。但是,如果两个Entry 对象的键的哈希值不一样,但他们之前在同一个桶上,那么在调整以后,并不能保证他们依然在同一个桶上。
这幅图描述了调整前和调整后的内部数组的情况。 在调整数组长度之前,为了得到Entry 对象E,Map 需要迭代遍历一个包含5个元素的链表。在调整数组长度以后,同样的get()方法则只需要遍历一个包好2个元素的链表,这样get()方法在调整数组长度后的运行速度提高了2倍。
3.线程安全
HashMap是不是线程安全的,为什么呢??例如假设你哟一个Writer线程,他只会想Map中插入已经存在的数据,一个Reader线程,他会从Map中读取数据,那么他为什么不工作呢??
因为在自动调整大小的机制下,如果线程试着去添加或者获取一个对象,Map可能会使用旧的索引值,这样既不会找到Entry 对象所在的新桶。
HashMap提供了一个线程安全的实现,可以阻止上述情况的发生。但是,所有同步的CRUD操作都非常慢。
从Java5 开始,二面就拥有了一个更好的、保证线程安全的HashMap实现:ConcurrentHashMap。对于ConcurrentHashMap来说,只有桶是同步的,这样如果多个线程不使用同一个桶或者调整内部数组的大小,他们可以同时调用get()、remove()或者put()方法。在一个多线程应用程序中这种方式是更好的选择。
4、键的不变性
为什么将字符串和证书作为HashMap的键是一种很好的实现?主要是因为他们是不可变的!如果你选择自己创建一个类作为键,但不能保证这个类是不可变的,那么你可能会在HashMap内部丢失数据。
LinkedHashMap 详解
1.存储结构
LinkedHashMap继承了HashMap,也就是继承了HashMap的结构,但和HashMap不同的是LinkedHashMap 迭代输出的结构保持了插入顺序。是什么样的结构使得LinkedHashMap 具有如此特性呢?如图所示:
HashMap有其自己的变脸header,并且在Entry 中新增了before和after两个指向前/后Entry的指针。依靠着和双向两边保证了迭代顺序是插入的顺序。同时LinkedHashMap定义了boolean AccessOrder,默认false即按照插入顺序,为true时按照访问顺序。
2.相关操作
存数据:
//LinkedHashMap没有put(K key, V value)方法,只重写了被put调用的addEntry方法 //1是HashMap里原有的逻辑,2和3是LinkedHashMap特有的 void addEntry(int hash, K key, V value, int bucketIndex) { createEntry(hash, key, value, bucketIndex); Entry eldest = header.after; //3.如果有必要,移除LRU里面最老的Entry,否则判断是否该resize if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); } else { if (size >= threshold) resize(2 * table.length); } } void createEntry(int hash, K key, V value, int bucketIndex) { //1.同HashMap一样:在Entry数组+next链表结构里面加入Entry HashMap.Entry old = table[bucketIndex]; Entry e = new Entry(hash, key, value, old); table[bucketIndex] = e; //2.把新Entry也加到header链表结构里面去 e.addBefore(header); size++; } //默认是false,我们可以重写此方法 protected boolean removeEldestEntry(Map.Entry eldest) { return false; }
取数据:
//重写了get(Object key)方法 public V get(Object key) { //1.调用HashMap的getEntry方法得到e Entry e = (Entry) getEntry(key); if (e == null) return null; //2.LinkedHashMap牛B的地方 e.recordAccess(this); return e.value; } // 继承了HashMap.Entry private static class Entry extends HashMap.Entry { //1.此方法提供了LRU的实现 //2.通过1和2两步,把最近使用的当前Entry移到header的before位置,而LinkedHashIterator遍历的方式是从header.after开始遍历,先得到最近使用的Entry //3.最近使用:accessOrder为true时,get(Object key)方法会导致Entry最近使用;put(K key, V value)/putForNullKey(value)只有是覆盖操作时会导致Entry最近使用。它们都会触发recordAccess方法从而导致Entry最近使用 //4.总结LinkedHashMap迭代方式:accessOrder=false时,迭代出的数据按插入顺序;accessOrder=true时,迭代出的数据按LRU顺序+插入顺序。而HashMap迭代方式:横向数组 * 竖向next链表 void recordAccess(HashMap m) { LinkedHashMap lm = (LinkedHashMap) m; //如果使用LRU算法 if (lm.accessOrder) { lm.modCount++; //1.从header链表里面移除当前Entry remove(); //2.把当前Entry移到header的before位置 addBefore(lm.header); } } //让当前Entry从header链表消失 private void remove() { before.after = after; after.before = before; } }
删数据:
// 继承了HashMap.Entry private static class Entry extends HashMap.Entry { //LinkedHashMap没有重写remove(Object key)方法,重写了被remove调用的recordRemoval方法 //这个方法的设计也和精髓,也是模板方法模式 //HahsMap remove(Object key)把数据从横向数组 * 竖向next链表里面移除之后(就已经完成工作了,所以HashMap里面recordRemoval是空的实现调用了此方法 //但在LinkedHashMap里面,还需要移除header链表里面Entry的after和before关系 void recordRemoval(HashMap m) { remove(); } //让当前Entry从header链表消失 private void remove() { before.after = after; after.before = before; } }
3.总结
1)LinkedHashMap继承了HashMap,左边结构里数据的变化交给了HashMap就行了。
2)中间结构里数据结构的变化就由LinkedHashMap里重写的方法去实现。
3)简言之:LinkedHashMap比HashMap多维护了一个链表。
TreeMap详解
红黑树的5条规则强制了他的关键性质:从跟到叶子的最长的可能路径部队与最短的可能路径的两倍长。结果是这棵树大致上是平衡的。因为操作比如插入、删除和查找某个值得最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是搞笑的,而不同于腹痛的二叉查找树。所以红黑树他是浮渣而搞笑的,其检索效率O(log n)。对于红黑二叉树而言他主要包括三大基本操作:左旋、右旋、着色。
- Java集合框架
- Java集合框架(set)
- Java集合框架之迭代器(if与while语句)
- 详解Java集合框架
- Java集合框架
- Java集合框架源码剖析:LinkedHashSet 和 LinkedHashMap
- Java集合框架
- java集合框架05——ArrayList和LinkedList的区别
- Java集合框架之Map
- Java集合框架认识
- Java集合框架的一些了解和基本用法
- java集合框架初学 ——List、Set、Map
- java集合框架总结(四)
- JAVA集合框架——图
- java集合框架
- java集合框架概述
- java集合框架之Collections类
- Java集合框架详解(二)——Collection源码分析
- 【Java集合源代码剖析】Java集合框架
- Java集合框架综述