您的位置:首页 > 编程语言 > Java开发

Java集合框架

2019-07-18 15:16 169 查看

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)。对于红黑二叉树而言他主要包括三大基本操作:左旋、右旋、着色。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: