说到集合,通常初学者会想到数组,可以说数组和集合还是有着根本的区别。初学者不妨了解一下这些区别:
一、数组声明了它容纳的元素的类型,而集合不声明,如
int[] array = new int[3] 或者 int[] array = new int[]{1,2,3};
二、数组是静态的,一个数组实例具有固定的大小,一旦创建了就无法改变容量了。而集合是可以动态扩展容量,可以根据需要动态改变大小,集合提供更多的成员方法,能满足更多的需求。
三、数组的存放的类型只能是一种(基本类型/引用类型),集合存放的类型可以不是一种(不加泛型时添加的类型是Object)。
四、数组是java语言中内置的数据类型,是线性排列的,执行效率或者类型检查都是最快的。
让我们直接深入主题,所有集合都是位于java.util包下,他们主要可以由两个接口派生成的:Collection和Map,Collection和Map是Java集合的框架的根接口,Collection框架接口树形图如下图所示:
其中Set和List接口分别代表了无序集合和有序集合,Queue是java提供的队列的实现。
而Map框架的树形图如图下所示:
下面我们就来一一介绍这些集合。
Set
HashSet类
HashSet是Set接口典型的实现,我们一般要使用到Set集合的时候就用HashSet这个类,HashSet按Hash算法存储集合中的元素,具有很好的存取和查找性能。HashSet具有以下特点。
1.无序,不会按照添加的顺序排列;
2.线程不安全,多个线程操作hashSet会产生线程安全问题;
3.无重复数据,添加多个相同的对象(equals()和hashCode()都相等),只保留一个值;
4.集合元素可以是null;
当向HashSet集合存入一个元素时,HashSet会根据这个元素的hashCode()的值来确定该元素在hashSet中的位置,如果两个元素通过equals()比较相等,但是hashCode()不相等,那么hashSet会将这两个元素保存在不同的位置。如果两个元素的hashCode()相等,但是equals()不相等,hashSet会用链式结构来保存多个元素。如果HashSet有两个以上的元素具有相同的hashCode()值,这会很影响HashSet的性能。 对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成。
LinkedHashSet
由上图可知linkedHashSet是hashSet的一个子类,和hashSet类似,也是通过hashCode()来确定元素的位置,唯一一点不同的是linkedHashSet使用链表来维护了元素的顺序,使元素插入linkedHashSet集合的顺序有保证,linkedHashSet的性能略低于HashSet,但是迭代访问它时会有很好的性能,因为它是有序的,底层用LinkedHashMap实现。
TreeSet
TreeSet是Set接口下的SortedSet的子类,TreeSet可以保证集合中的元素处于排序状态。和linkedHashSet相比较,treeSet采用红黑树的数据结构来存储集合元素。treeSet支持两种排序方式:自然排序和定制排序。默认为自然排序。(TreeSet不能添加null元素)
自然排序:java提供一个接口comparable,该接口定义一个方法compareTo(Object object);该方法返回一个整数值。当想要把某个对象加入treeSet集合中时,这个对象必须需要实现comparable接口,treeSet会根据加入的对象和集合中对象做conpareTo()比较,如果等于0,则相等(不会做任何添加动作),如果小于0,则往前排,如大于0,则往后排,以此类推。
定制排序:定制排序需要在创建TreeSet时,提供一个Comparator对象和TreeSet关联,由Comparator完成集合内部的排序逻辑。如:
Set<String> set = new TreeSet<>((o1,o2)->{ String a = (String)o1; String b = (String)o2; return a.length()>b.length()?-1:a.length()<b.length()?-1:0; });
EnumSet
EnumSet是一个专门为枚举设计的类,EnumSet中的元素必须是枚举类型,集合中的元素也是有序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序。EnumSet内部是以位向量的形式存储的,这种存储形式非常的高效,紧凑,因为EnumSet的运行速度比较快,占用内存很小。
Set的比较和分析
HashSet和TreeSet时Set接口中两个典型的实现,总体上HashSet的性能要优于TreeSet(特别是添加和查询的操作),TreeSet额外的红黑树算法维护集合元素的次序。只有当需要使用到保证set次序时用TreeSet,否则都应该使用HashSet。HashSet的子类LinkedHashSet对于普通的插入删除的操作比HashSet慢一点,这是需要额外维护链表造成的,但是有了链表,linkedHashSet的遍历更快。EnumSet的性能时所有Set中最好的,但是EnumSet只能保存同一个枚举类的集合元素。(HashSet,TreeSet,EnumSet都不是线程安全的,多线程的时候谨慎)
List
ArrayList
ArrayList是我们工作中使用的比较多的一个集合,是一个可储存有序可重复元素的集合,一般用add方法将对象存进集合中,内部是用数组实现,当我们new ArrayList()时,实际上是new Object[10]来存储我们add的对象。一般面试中常问的Arraylist和Vector的区别:其实Vector是JKD1.2之前的集合,一些方法和之后的ArraysList都有重复,ArrayList是线程不安全的,Vector是线程安全的,线程不安全性能当然要高些。还有就是Vector下提供一个Stack子类,相当于栈的功能(后进先出)。
Queue
queue是相当于队列数据结构的集合,通常是先进先出操作顺序,一般使用Queue接口下面的PriorityQueue实现类。除此之外Queue还有一个接口Deque(双端队列),可以在对头和队尾双端进行操作,增加了很多灵活性。
LinkedList
LinkedList类是List接口下的一个实现类,同时也实现了Deque接口,这意味着LinkedList既可以通过索引来访问集合中的对象,又可以通过offer(),push(),peekFirst(),peekLast()等API实现压栈弹栈等功能,LinkedList和ArrayList ArrayDeque的实现方式不同,ArrayList ArrayDeque都是通过数组的形式来保存数据的,因此他们随机访问集合数据时性能很好;而LinkList是以链表的形式来保存元素,所以查找功能差一些,但是增删改的性能很好。
List性能比较和分析
List是一个线性表接口,ArrayList和LinkedList分别是数组的线性表和链的线性表。而Queue代表着队列,PriorityQueue是单端队列,而Deque是双端队列。一般来说,由于数组以一块连续内存区来保存所有数组元素,所以数组的随机访问性能最好,而内部以链表实现的集合对于增删的性能较好。但总体来说ArrayList的性能比LinkedList要好,这也是我们在工作中使用比较多的原因。如果有多个线程访问List集合元素,可以使用Collections将集合包装成线程安全集合。
Map
Map用于保存具有映射关系的数据,一个key对应一个value,key不能重复(注意:实现类中有的key可以为null,比如hashmap,有的不能为null 如hashtable。)。其中可以将Map中所有的key看作一个set集合,value看作一个List集合,只不过key和value之间必须一一对应。
HashMap
在Jdk7的时候hashMap使用数组实现的,jdk8之后,改用了红黑树实现。hashMap也是我们工作中使用比较多的,他不能保证key-value的顺序,类似于hashSet,判断两个key相等的标准也是:两个key通过equals()方法比较返回true,两个key的hashcode值也想等。如果使用可变对象作为hashMap的key,并且修改了作为key的可变对象,也会出现程序再也无法精准访问map中被修改过的key。
源码很重要(原文:https://blog.csdn.net/goosson/article/details/81029729 ):
put方法:
//对外开发使用 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } //存值的真正执行者 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //定义一个数组,一个链表,n永远存放数组长度,i用于存放key的hash计算后的值,即key在数组中的索引 Node<K,V>[] tab; Node<K,V> p; int n, i; //判断table是否为空或数组长度为0,如果为空则通过resize()实例化一个数组并让tab作为其引用,并且让n等于实例化tab后的长度 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根据key经过hash()方法得到的hash值与数组最大索引做与运算得到当前key所在的索引值,并且将当前索引上的Node赋予给p并判断是否该Node是否存在 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);//若tab[i]不存在,则直接将key-value插入该位置上。 //该位置存在数据的情况 else { Node<K,V> e; K k; //重新定义一个Node,和一个k // 该位置上数据Key计算后的hash等于要存放的Key计算后的hash并且该位置上的Key等于要存放的Key if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) e = p; //true,将该位置的Node赋予给e else if (p instanceof TreeNode) //判断当前桶类型是否是TreeNode //ture,进行红黑树插值法,写入数据 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //false, 遍历当前位置链表 for (int binCount = 0; ; ++binCount) { //查找当前位置链表上的表尾,表尾的next节点必然为null,找到表尾将数据赋给下一个节点 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //是,直接将数据写到下个节点 // 如果此时已经到第八个了,还没找个表尾,那么从第八个开始就要进行红黑树操作 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); //红黑树插值具体操作 break; } //如果当前位置的key与要存放的key的相同,直接跳出,不做任何操作 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //将下一个给到p进行逐个查找节点为空的Node p = e; } } //如果e不为空,即找到了一个去存储Key-value的Node if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //当最后一次调整之后Size大于了临界值,需要调整数组的容量 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
get()方法:
//对外公开方法 public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } //实际逻辑控制方法 final Node<K,V> getNode(int hash, Object key) { //定义相关变量 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //保证Map中的数组不为空,并且存储的有值,并且查找的key对应的索引位置上有值 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // always check first node 第一次就找到了对应的值 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; //判断下一个节点是否存在 if ((e = first.next) != null) { //true,检测是否是TreeNode if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //通过TreeNode的get方法获取值 //否,遍历链表 do { //判断下一个节点是否是要查找的对象 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; }while ((e = e.next) != null); } }//未找到,返回null return null; }
resize()扩容方法:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; //未扩容时数组的容量 int oldThr = threshold; int newCap, newThr = 0;//定义新的容量和临界值 //当前Map容量大于零,非第一次put值 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { //超过最大容量:2^30 //临界值等于Integer类型的最大值 0x7fffffff=2^31-1 threshold = Integer.MAX_VALUE; return oldTab; } //当前容量在默认值和最大值的一半之间 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; //新临界值为当前临界值的两倍 } //当前容量为0,但是当前临界值不为0,让新的容量等于当前临界值 else if (oldThr > 0) newCap = oldThr; //当前容量和临界值都为0,让新的容量为默认值,临界值=初始容量*默认加载因子 else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //如果新的临界值为0 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //临界值赋值 threshold = newThr; //扩容table Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e;//此时newCap = oldCap*2 else if (e instanceof TreeNode) //节点为红黑树,进行切割操作 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { //链表的下一个节点还有值,但节点位置又没有超过8 //lo就是扩容后仍然在原地的元素链表 //hi就是扩容后下标为 原位置+原容量 的元素链表,从而不需要重新计算hash。 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; //循环链表直到链表末再无节点 do { next = e.next; //e.hash&oldCap == 0 判断元素位置是否还在原位置 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); //循环链表结束,通过判断loTail是否为空来拷贝整个链表到扩容后table if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
Hashtable
hashtable总是被拿来和hashMap作比较,hashtable是很早就出现的类,他和hashMap最主要的区别就是他是线程安全的,并且不允许把null作为他的key和value,一般情况下如果多线程下要保证线程安全,可以用hashtable,否则都用hashMap。与hashMap一般,不要使用可变对象作为hashMap,hashtable的key,尽量不要在程序中修改作为key的可变对象。
LinkedHashMap
与linkedHashSet类是,HashMap也有一个LinkedHashMap子类,LinkedHashMap也是使用双向链表来维护key-value元素的次序的,只需要考虑key的次序,该链表负责维护Map的迭代顺序,迭代顺序与插入时的顺序保持一致。LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但是他是与链表来维护内部顺序,所以迭代访问map里面元素时有较好的性能。
TreeMap
与TreeSet一样,Set接口派生出SortedSet子接口,SortedSet有一个TreeSet实现类一样,Map接口也派生出一个SortedMap子接口,SortedMap也有一个TreeMap实现类。TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点,TreeMap可以保证所有的key-value对处于有序状态。TreeMap也有两种排序方式:
自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的kye应该是同一个类的对象,否则会报错。
定制排序:创建TreeMap时,传入一个Comparator对象对有个的key进行比较排序,采用定制排序时不要求key必须实现Comparable接口。
WeakHashMap
WeakHashMap与HashMap的用法基本相似,与HashMap不同的是hashMap的key保留了对实际对象的强引用,这意味着只要该HashMap对象不被销毁,该HashMap的key所引用的对象就不会被回收,hashMap也不会删除这些key所对应的key-value对;但WeakHashMap的key只保留了对实际对象的弱引用,这就意味着如果WeakHashMap对象的key所引用对象没有被其他强引用变量所引用,则这些key所引用的对象可能会被垃圾回收,WeakHashMap也可能删除这些key所对应的key-value对。
IdentityHashMap
IdentityHashmap和HashMap基本相似,但它在处理两个key相等时比较独特:在IdenityHashMap中,当且仅当两个key严格相等时(key1==key2)时,IdentityHashMap才认为两个key相等;但是HashMap只要key1和key2通过equals()方法比较返回true,且他们的hashcode值相等即可。
EnumMap
EnumMap是一个枚举Map,他的所有key必须是单个的枚举类的枚举值,创建EnumMap时必须显式或者隐式指定它对应的枚举类。特性如下:
1.EnumMap在内部以数组形式保存,所以个他非常高效,紧凑。
2.EnumMap根据key的自然顺序来维护key-value对的顺序。即枚举值在枚举类中定义的顺序。
3.EnumMap不允许null作为key值,但是可以作为value值。
Map性能分析
一般来说,程序应该多考虑使用HashMap,因为HashMap正是为了快讯查询设计的,即使对于线程来说,也没有必要使用HashTable,可以使用Collections 工具类把HashMap变成线程安全的。TreeMap通常比HashMap,Hashtable要慢,特别是插入,删除时更慢,但是TreeMap中key-value总处于有序状态,无需专门的排序操作,需要对key-value中的key进行排序时,可以使用TreeMap,LinkedHashMap性能比HashMap要慢一些,因为他需要维护链表来保持map中key-value时的添加顺序。IdentityHashMap也不常用,唯一和HashMap不同之处是采用==而不是equals()方法判断元素相等。EnumMap的性能最好,但它只能使用同一个枚举类的枚举作为key。
Hash的解读
对于hashSet,hashMap,Hashtbale而言,他们采用hash算法决定几何中元素的存储位置,并通过hash算法来控制集合的大小。
hash表里面可以存储元素的位置被称为“桶”,通常情况下,单个“桶”里面存储一个元素,此时有最好的性能:hash算法可以根据hashCode计算出“桶”的存储位置,并且取出数据。但是当发生hash冲突时,单个桶会有多个元素,这些元素以链表形式存储,想得到数据必须按hash出桶位置,然后顺序搜索才能找到数据。如图:
HashSet,HashMap,Hashtable都是使用hash算法来决定元素的存储,因为都包含如下元素:
容量(capacity):hash表中桶的数量。
初始化容量(initial capacity):创建hash表时桶的数量。hashMap和hashSet都允许在构造器中制定初始化容量。
尺寸(size):当前hash表中记录的数量。
负载因子(load factor):负载因子等于“siza/capacity”。负载因为为0,表示空的hash表,0.5表示半满的hash表。HashSet,HashMap,Hashtable的默认负载因子是0.75。
(本文全手打,请忽略因为引文大小写。谢谢)
来源:oschina
链接:https://my.oschina.net/u/4100220/blog/3054545