[核心工具] Collections
来源:互联网 发布:大众软件2014电子版 编辑:程序博客网 时间:2024/06/01 07:38
核心类关系图:
首先说明,这里说道的Collections是 java.lang 中相对早期就有实现的那些基础核心类,而不包括current包下的工具类。current包下的工具类会在另一篇中讲解。
List
List中最常用的有三个:ArrayList, Vector,LinkedList。
- ArrayList, Vector
基于数组实现,允许null元素。而ArrayList和Vector的区别在于,Vector所有方法都是线程同步的,而ArrayList则没有因此也是线程不安全的。创建一个同步的List还可以这样 List list = Collections.synchronizedList(new LinkedList(...));。
Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。
由于基于数组实现,导致以下几个特点:
0:size,isEmpty,get,set方法运行时间为常数。
1:Add的过程中需要校验数组大小,当数组不够用则需要扩容。扩容过程中涉及到数组复制,影响性能。添加一个元素需要O(n)的时间
2:优秀的随机访问,直接通过下标完成。
3:随机或者删除需要大量的移动数组元素,性能较差。
4:有一个容量参数可以用于指定初始大小(每次默认扩展到原来的1.5倍)。
- LinkedList
基于循环双向链表实现的,LinkedList本身即使为空也包含一个链表的Header节点。
由于基于链表实现,导致以下几个特点:
0:size需要遍历列表。
1:优秀的随机插入删除性能
2:不需要预先分配连续内存空间,也不需要容量调整
3:随机访问需要从Header节点逐一往后找,性能很差。
4:for(int I=0; I<linkedLis.size(); I++) 这样的操作非常慢,因为每次实际上都是一个随机访问。需要用forEach或者迭代器进行循环操作。
- Stack 类
Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。
RandomAccess接口。由于List有基于数组和基于链表的两种实现方法,而链表不支持随机访问。因此Java提供了RandomAccess接口,基于数组实现的都实现了该接口,而基于链表实现的都没有实现该接口。因此 RandomAccess 才适合用 for(int I=0; I<linkedLis.size(); I++) 这样的操作,否则最好用forEach或者迭代器。
Map
请注意,Map没有继承Collection接口,Map提供key到value的映射。线程不安全。
一个Map中不能包含相同的key,每个key只能映射一个value。
Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。
AbstractMap抽象类覆盖了equals()和hashCode()方法以确保两个相等键值对返回相同的哈希码。
- HashMap类
HashMap是非同步的,并且允许null,即null value和null key。HashMap基于数组 + 链表的形式来。其中数组中是Entry类,包含了:key, value, next, hash四个值。
在不冲突的情况下,直接从Key的Hash定位到数组中的位置,而在冲突的情况下,会在对应位置上的链表上增加元素。Hash定位是通过:hashCode() + hash() 方法来实现的。
当向HashMap中添加元素的时候:
首先计算元素的hashcode值,然后用这个(元素的hashcode)%(HashMap集合的大小)+1计算出这个元素的存储位置,
如果这个位置位空,就将元素添加进去;如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则找一个空位添加。
java.lnag.Object中对hashCode的约定:
1) 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话(不是所有字段, 而是equals用到的字段),则对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。
2) 如果两个对象根据equals(Object o)方法是相等的,则调用这两个对象的hashCode方法必须产生相同的整数结果。
3) 如果两个对象根据equals(Object o)方法是不相等的,则调用这两个对象中任一个对象的hashCode方法,不要求产生不同的整数结果。但如果能不同,则可能提高散列表的性能。
从这个实现可以看出,有以下几个特点:
1:hash算法和hashCode算法影响性能。HashMap的hash方法是基于位操作的,而Object的默认hashCode方法是Native方法。注意,hashCode是可以重载的,因此需要注意实现性能。
2:当 hash算法和hashCode算法很合理,而容量也足够大的时候,Value很难产生冲突,那么约等于是随机访问。但是,如果出现大量的冲突,那么则退化成几个链表。
3:既然是数组,那么就有容量参数。后面会单独分析这个过程。
- TreeMap
实现了SortedMap接口,它用来保持键的有序顺序。添加到SortedMap实现类的元素必须实现Comparable接口。TreeMap类是它的唯一一份实现。
线程不安全。
TreeMap是基于红黑树实现的,这是一种平衡查找树。
TreeMap提供了:subMap, headMap, tailMap方法,用于返回部分子树。其返回的子树还是有序的。
由于是基于树实现的,有如下的特性:
1:插入删除过程可能涉及到树的再平衡,所以可能造成性能影响,此外查找也不能直接进行下标访问。因此这些过程是一般是慢于基于数组的实现。
2:由于红黑树算法,最坏的情况下也能以o(log n)时间内做到查找删除等。
- 其他早期Map类型
Hashtable:任何非空(non-null)的对象都可作为key或者value。基于数组实现。和HashMap不同之处在于,Hashtable是同步的。
WeakHashMap:一种改进的HashMap,它对key实行“弱引用”,如果一个key没有被除了WeakHashMap之外的其他方所引用,那么在内存不足的时候该key可以被GC回收(弱引用的特点)。因为值可能被回收,不能存放重要数据,因此特别适合做缓存。
LinkedHashMap:一种改进的HashMap,为key增加了一个额外链表,用于维护插入或者读取顺序(而SortedMap维护的是比较顺序),两个顺序只能取其一。LinkedHashMap.Entry在HashMap.Enty基础上增加了before和after属性用于构成链表。如果需要保持的是读取顺序,那么每次 get 就会修改链表,将当前取得的元素移到队尾。
EnumMap:使用EnumMap时,Key必须指定为枚举类型且不能为 null。它的key为枚举元素,value自定义。在工作中我们也可以用其他的Map来实现我们关于枚举的需求,但是为什么要用这个EnumMap呢?但是使用EnumMap会更加高效:它只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值。这使得EnumMap的效率非常高。
注:迭代器和LinkedHashMap在get过程中的错误
集合在使用迭代器过程中禁止对元素内容进行修改,而一般认为get()方法是只读的。但是对于LinkedHashMap,其为了维护读取顺序,需要修改链表,所以在迭代器中也会报错。
Set
Set是一种不包含重复的元素的Collection, 确切地说,是不包含e1.equals(e2)的元素对。Set最多有一个null元素。很明显,Set的构造函数有一个约束条件,传入的Collection参数不能包含重复的元素。
请注意:必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。
Set接口有四个常见实现 HashSet、TreeSet、EnumSet、LinkedHashSet,都是基于 Map作为最终实现方式的, 分别对应 HashMap、TreeMap、EnumMap、LindexHashSet.
这两个 Set 都不是同步的, 所以可以通过 Set<String> synset = Collections.synchronizedSet(set); 来获取同步的 Set.
- Set 不重复实现原理
以HashSet为例,在HashSet中,基本的操作都是有HashMap底层实现的,因为HashSet底层是用HashMap存储数据的。
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map; // 底层使用HashMap来保存HashSet中所有元素。
private static final Object PRESENT = new Object(); // 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。
public HashSet() {
map = new HashMap<E,Object>(); //默认的无参构造器,构造一个空的HashSet。
// 实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16)); // 构造一个包含指定collection中的元素的新set。
// 实际底层使用默认的加载因子0.75和足以包含指定, collection中所有元素的初始容量来创建一个HashMap。
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<E,Object>(initialCapacity, loadFactor); // 以指定的initialCapacity和loadFactor构造一个空的HashSet。
}
public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity); // 以指定的initialCapacity构造一个空的HashSet。
// 实际底层以相应的参数及加载因子loadFactor为0.75构造一个空的HashMap。
}
// 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。
// 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}
public Iterator<E> iterator() {
return map.keySet().iterator(); // 返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。
}
// ……. 一下省略,size, isEmpty, contains, add, remove, clear 等等都是直接调用的HashMap的方法。
既然是利用 HashMap 来实现的, 那么为什么还需要 HashSet?
Set使用Map实现其功能只是一种方式罢了,CopyOnWriteArraySet可以用ArrayList实现. 实际使用中, Set 和 Map 确实不是一个场景, 利用 HashMap来模拟HashSet 的功能, 的确是可以的, 但是这样用的多了, 不如提供一个封装.Set 单独存在是有必要的, 毕竟是有特定用途的, 而 HashSet 只是恰好用 HashMap 来作为实现而已.
HashMap重要性质和优化总结
由于HashMap用的非常多,而且相对List来说结构复杂不少,同时常见的HashSet又是基于HashMap来实现的,因此需要单独讲HashMap拿出来分析。
HashMap创建到put流程基本介绍
hashMap由其名字可以知道,它使用的是哈希算法来管理存储其中的对象的,具体是用数组和链表两种数据结构管理的。
程序将利用initialCapacity计算一个新的capacity,capacity大小为大于初始容易值的最小的2的整数次幂的值(如初始容量为15,则capacity为16.初始为3,则capacity为4),
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
如果参数均未指定,则使用默认值初始化
2: put如果一个对象hash到同一个bucket,则会形成一个链表,链表查询是线性的。在对象放入map后,会检查map大小。如果map的size大于或等于threshold(capacity * load factor),注意不是在size大于capacity时扩容,则会以map两倍容量扩容(此步骤设计到重新申请空间和计算hash值,性能消耗比较大) 调整 Map 实现的大小
在哈希术语中,内部数组中的每个位置称作“存储桶”(bucket),而可用的存储桶数(即内部数组的大小)称作容量 (capacity)。为使 Map 对象有效地处理任意数目的项,Map 实现可以调整自身的大小。
但调整大小的开销很大。调整大小需要将所有元素重新插入到新数组中,这是因为不同的数组大小意味着对象现在映射到不同的索引值。先前冲突的键可能不再冲突,而先前不冲突的其他键现在可能冲突。这显然表明,如果将 Map 调整得足够大,则可以减少甚至不再需要重新调整大小,这很有可能显著提高速度。
使用负载因子
基于哈希的 Map 使用一个额外参数并粗略计算存储桶的密度。Map 在调整大小之前,使用名为“负载因子”的参数指示 Map 将承担的“负载”量,即它的负载程度。负载因子、项数(Map 大小)与容量之间的关系简单明了:
例如,如果默认负载因子为 0.75,默认容量为 11,则 11 x 0.75 = 8.25,该值向下取整为 8 个元素。因此,如果将第 8 个项添加到此 Map,则该 Map 将自身的大小调整为一个更大的值。相反,要计算避免调整大小所需的初始容量,用将要添加的项数除以负载因子,并向上取整. 例如,对于负载因子为 0.75 的 100 个项,应将容量设置为 100/0.75 = 133.33,并将结果向上取整为 134.
1.4 版后的某些 Map(如 HashMap 和 LinkedHashMap,而非 Hashtable 或 IdentityHashMap)使用需要 2 的幂容量的哈希函数,但下一个最高 2 的幂容量由这些 Map 计算,因此您不必亲自计算。
负载因子本身是空间和时间之间的调整折衷。较小的负载因子将占用更多的空间,但将降低冲突的可能性,从而将加快访问和更新的速度。使用大于 0.75 的负载因子可能是不明智的,而使用大于 1.0 的负载因子肯定是不明智的,这是因为这必定会引发一次冲突。使用小于 0.50 的负载因子好处并不大,而且将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。
参考:
http://www.tuicool.com/articles/NN7ZzqJ
http://ifeve.com/why-is-there-not-concurrent-arraylist-in-java-util-concurrent-package/
《Java程序性能优化》 葛一鸣
阅读全文
0 0
- [核心工具] Collections
- 工具类:Collections 、Arrays
- Java Collections工具类
- 集合工具类Collections
- Collections工具类示例
- 集合工具类Collections
- Java Collections工具类
- Collections集合工具类
- 集合工具类 Collections
- Collections工具类
- Collections工具类
- 20170519@Collections工具类
- Collections工具类
- java集合工具collections
- Collections工具类
- Collections工具类
- Collections工具类
- java Collections工具类
- 16秋计算机JAVA第七节课作业
- 制作具有支持设定推迟时间的定时关机和终止功能的.bat可执行文件
- 一图读懂云计算关键技术——k8s容器管理系统的架构
- 坚持#第236天~Apache之httpd与周五测试
- axure 8.0注册码
- [核心工具] Collections
- Swarm -- 搭建Docker集群
- 你真的了解Java中的Instanceof吗?
- 微信小程序如何通过js操作wxmll的wxss属性
- Redis 安装
- 安装cuDNN-8.0
- 文章标题
- 深入理解 CSS 中的行高与基线及line-height的用法详解
- 17.12.8 学习C语言两个月后的体会