对于这些专题的详解,专门作了一个983页的PDF版本,以下java
(更多完整项目下载。未完待续。源码。图文知识后续上传github。)
能够点击关于我联系我获取
( VX:mm14525201314)
数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素间的关系组成。经常使用的数据有:数组、栈、队列、链表、树、图、堆、散列表。mysql
1)数组:在内存中连续存储多个元素的结构。数组元素经过下标访问,下标从0开始。优势:访问速度快;缺点:数组大小固定后没法扩容,只能存储一种类型的数据,添加删除操做慢。适用场景:适用于需频繁查找,对存储空间要求不高,不多添加删除。git
2)栈:一种特殊的线性表,只能够在栈顶操做,先进后出,从栈顶放入元素叫入栈,从栈顶取出元素叫出栈。应用场景:用于实现递归功能,如斐波那契数列。github
3)队列:一种线性表,在列表一端添加元素,另外一端取出,先进先出。使用场景:多线程阻塞队列管理中。算法
4)链表:物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是经过链表的指针地址实现,每一个元素包含两个结点,一个是存储元素的数据域,一个是指向下一个结点地址的指针域。有单链表、双向链表、循环链表。优势:能够任意加减元素,不须要初始化容量,添加删除元素只需改变先后两个元素结点的指针域便可。缺点:由于含有大量指针域,固占用空间大,查找耗时。适用场景:数据量小,需频繁增长删除操做。sql
5)树:由n个有限节点组成一种具备层次关系的集合。二叉树(每一个结点最多有两个子树,结点的度最大为2,左子树和右子树有顺序)、红黑树(HashMap底层源码)、B+树(mysql的数据库索引结构)数据库
6)散列表(哈希表):根据键值对来存储访问。数组
7)堆:堆中某个节点的值老是不大于或不小于其父节点的值,堆老是一棵彻底二叉树。安全
8)图:由结点的有穷集合V和边的集合E组成。数据结构
1)并发List,包括Vector和CopyOnWriteArrayList是两个线程安全的List,Vector读写操做都用了同步,CopyOnWriteArrayList在写的时候会复制一个副本,对副本写,写完用副本替换原值,读时不须要同步。
2)并发Set,CopyOnWriteArraySet基于CopyOnWriteArrayList来实现的,不容许存在重复的对象。
3)并发Map,ConcurrentHashMap,内部实现了锁分离,get操做是无锁的。
4)并发Queue,ConcurrentLinkedQueue适用于高并发场景下的队列,经过无锁方式实现。 BlockingQueue阻塞队列,应用场景,生产者-消费者模式,若生产快于消费,生产队列装满时会阻塞,等待消费。
5)并发Deque, LinkedBlockingDueue没有进行读写锁分离,同一时间只能有一个线程对其操做。
6)并发锁重入锁ReentrantLock,互斥锁,一次最多只能一个线程拿到锁。
7)读写锁ReadWriteLock,有读取和写入锁两种,读取容许多个读取线程同时持有,而写入只能有一个线程持有。
1)Collection接口:集合框架的根接口,它是集合类框架中最具通常性的顶层接口。
2)Map接口:提供了键值对的映射关系的集合,关键字不能有重复值,每一个关键字至多可映射一个值。HashMap(经过散列机制,用于快速访问),TreeMap(保持key处于排序状态,访问速度不如hashmap), LinkedHashMap(保持元素的插入顺序)
3)Set接口:可包含重复的元素,LinkedHashSet TreeSet(用红黑树来存储元素) HashSet
4)List接口:可经过索引对元素进行精准的插入和查找,实现类有ArrayList LinkedList
5)Queue接口:继承自Collection接口,LinkedList实现了Queue接口,提供了支持队列的行为。
6)Iterator接口:为了迭代集合
7)Comparable接口:用于比较
Set是一个无序的集合,不能包含重复的元素;
list是一个有序的集合能够包含重复的元素,提供了按索引访问的方式;
map包含了key-value对,map中key必须惟一,value能够重复。
1)数据结构
jdk1.7及之前,HashMap由数组+链表组成,数组Entry是HashMap的主体,Entry是HashMap中的一个静态内部类,每个Entry包含一个key-value键值对,链表是为解决哈希冲突而存在。
从jdk1.8起,HashMap是由数组+链表/红黑树组成,当某个bucket位置的链表长度达到阀值8时,这个链表就转变成红黑树。
2)HashMap是线程不安全的,存储比较快,能接受null值,HashMap经过put(key, value)来储存元素,经过get(key)来获得value值,经过hash算法来计算hashcode值,用hashcode标识Entry在bucket中存储的位置。
3)HashMap中为何要使用加载因子,为何要进行扩容
加载因子是指当HashMap中存储的元素/最大空间值的阀值,若是超过这个值,就会进行扩容。加载因子是为了让空间获得充分利用,若是加载因子太大,虽对空间利用更充分,但查找效率会下降;若是加载因子过小,表中的数据过于稀疏,不少空间还没用就开始扩容,就会对空间形成浪费。
至于为何要扩容,若是不扩容,HashMap中数组处的链表会愈来愈长,这样查找效率就会大大下降。
当咱们使用put(key, value)存储对象到HashMap中时,具体实现步骤以下:
get(key)方法获取key的hash值,计算hash&(n-1)获得在链表数组中的位置first=table[hash&(n-1)],先判断first(即数组中的那个)的key是否与参数key相等,不等的话,判断结点是不是TreeNode类型,是则调用getTreeNode(hash, key)从二叉树中查找结点,不是TreeNode类型说明仍是链表型,就遍历链表找到相同的key值返回对应的value值便可。
当两个对象的hashcode相同,它们的bucket位置相同,hashMap会用链表或是红黑树来存储对象。Entry类里有一个next属性,做用是指向下一个Entry。第一个键值对A进来,经过计算其key的hash获得index,记作Entry[index]=A。一会又进来一个键值对B,经过计算其key的hash也是index,HashMap会将B.next=A, Entry[index]=B.若是又进来C,其key的hash也是index,会将C.next=B, Entry[index]=C.这样bucket为index的地方存放了ABC三个键值对,它们能过next属性链在一块儿。数组中存储的是最后插入的元素,其余元素都在后面的链表里。
当调用get方法时,hashmap会使用键对象的hashcode找到bucket位置,找到bucket位置后,会调用key.equals()方法去找到链表中正确的节点,最终找到值对象。
HashMap默认负载由于是0.75,当一个map填满了75%的bucket时,和其余集合类同样,将会建立原来HashMap大小两倍的bucket数组,来从新调整HashMap的大小,并将原来的对象放入新的bucket数组中。
在jdk1.7及之前,多线程扩容可能出现死循环。由于在调整大小过程当中,存储在某个bucket位置中的链表元素次序会反过来,而多线程状况下可能某个线程翻转完链表,另一个线程又开始翻转,条件竞争发生了,那么就死循环了。
而在jdk1.8中,会将原来链表结构保存至节点e中,将原来数组中的位置设为null,而后依次遍历e,根据hash&n是否为0分红两条支链,保存在新数组中。若是多线程状况可能会取到null值形成数据丢失。
1)jdk1.7及之前:一个ConcurrentHashMap由一个segment数组和多个HashEntry组成,每个segment都包含一个HashEntry数组, Segment继承ReentrantLock用来充当锁角色,每个segment包含了对本身的HashEntry的操做,如getputreplace操做,这些操做发生时,对本身的HashEntry进行锁定。因为每个segment写操做只锁定本身的HashEntry,能够存在多个线程同时写的状况。
jdk1.8之后:ConcurrentHashMap取消了segments字段,采用transient volatile HashEntry<K, V> table保存数据,采用table数组元素做为锁,实现对每个数组数据进行加锁,进一小减小并发冲突几率。ConcurrentHashMap是用Node数组+链表+红黑树数据结构来实现的,并发制定用synchronized和CAS操做。
2)Segment实现了ReentrantLock重入锁,当执行put操做,会进行第一次key的hash来定位Segment的位置,若该Segment尚未初始化,会经过CAS操做进行赋值,再进行第二次hash操做,找到相应的HashEntry位置。
1)存储方式不同,HashMap内部有一个Node<K,V>[]对象,每一个键值对都会存储到这个对象里,当用put方法添加键值对时,会new一个Node对象,tab[i] = newNode(hash, key, value, next);
ArrayMap存储则是由两个数组来维护,int[] mHashes; Object[] mArray; mHashes数组中保存的是每一项的HashCode值,mArray存的是键值对,每两个元素表明一个键值对,前面保存key,后面保存value。mHashes[index]=hash; mArray[index<<1]=key; mArray[(index<<1)+1]=value;
ArrayMap相对于HashMap,无需为每一个键值对建立Node对象,且在数组中连续存放,更省空间。
2)添加数据时扩容处理不同,进行了new操做,从新建立对象,开销很大;而ArrayMap用的是copy数据,全部效率相对高些;
3)ArrayMap提供了数组收缩功能,在clear或remove后,会从新收缩数组,释放空间;
4)ArrayMap采用二分法查找,mHashes中的hash值是按照从小到大的顺序连续存放的,经过二分查找来获取对应hash下标index,去mArray中查找键值对。mHashes中的index2是mArray中的key下标,index2+1为value的下标,因为存在hash碰撞状况,二分查找到的下标多是多个连续相同的hash值中的任意一个,此时须要用equals比对命中的key对象是否相等,不相等,应当从当前index先向后再向前遍历全部相同hash值。
5)sparseArray比ArrayMap进一步优化空间,SparseArray专门对基本类型作了优化,Key只能是可排序的基本类型,如intlong,对value,除了泛型Value,还对每种基本类型有单独实现,如SparseBooleanArraySparseLongArray等。无需包装,直接使用基本类型值,无需hash,直接使用基本类型值索引和判断相等,无碰撞,无需调用hashCode方法,无需equals比较。SparseArray延迟删除。
Hashtable中的无参构造方法Hashtable()中调用了this(11, 0.75f),说明它默认容量是11,加载因子是0.75,在构造方法上会new HashtableEntry<?, ?>[initialCapacity]; 会新建一个容量是初始容量的HashtableEntry数组。
HashtableEntry数组中包含hashKeyValuenext变量,链表形式,重写了hashCode和equals方法。Hashtable全部public方法都在方法体上加上了synchronized锁操做,说明它是线程安全的。
它还实现了Serializable接口中的writeObject和readObject方法,分别实现了逐行读取和写入的功能,而且加了synchronized锁操做。
根所key.hashCode(),计算它在table表中的位置,(hash&0x7FFFFFFF)%tab.length,遍历该索引处表的位置中是否有值,是否存在链表,再判断是key值和hash值是否相等,相等则返回对应的value值。
1)Hashtable是个线程安全的类,在对外方法都添加了synchronized方法,序列化方法上也添加了synchronized同步锁方法,而HashMap非线程安全。这也致使Hashtable的读写等操做比HashMap慢。
2)Hashtable不容许值和键为空,若为空会抛出空指针。而HashMap容许键和值为空;
3)Hashtable根据key值的hashCode计算索引,(hash&0x7FFFFFFF)%tab.length,保证hash值始终为正数且不超过表的长度。而HashMap中计算索引值是经过hash(key)&(tab.length-1),是经过与操做,计算出在表中的位置会比Hashtable快。
4)Hashtable容量能为任意大于等于1的正数,而HashMap的容量必须为2^n,Hashtable默认容量为11,HashMap初始容量为16
5)Hashtable每次扩容,新容量为旧容量的2倍+1,而HashMap为旧容量的2倍。
HashSet底层实现是HashMap,内部包含一个HashMap<E, Ojbect> map变量
private transient HashMap<E,Object> map;
一个Object PRESENT变量(当成插入map中的value值)
private static final Object PRESENT = new Object();
HashSet中元素都存到HashMap键值对的Key上面。具体能够查看HashSet的add方法,直接调用了HashMap的put方法,将值做为HashMap的键,值用一个固定的PRESENT值。
public boolean add(E e) { return map.put(e, PRESENT)==null; }
HashSet没有单独的get方法,用的是HashMap的。HashSet实现了Set接口,不容许集合中出现重复元素,将对象存储进HashSet前,要先确保对象重写了hashCode()和equals方法,以保证放入set对象是惟一的。
HashMap在放入key-value键值对是,先经过key计算其hashCode()值,再与tab.length-1作与操做,肯定下标index处是否有值,若是有值,再调用key对象的equals方法,对象不一样则插入到表头,相同则覆盖;
HashSet是将数据存放到HashMap的key中,HashMap是key-value形式的数据结构,它的key是惟一的,HashSet利用此原理保证放入的对象惟一性。
HashSet底层实现是HashMap,HashMap若是两个不一样Key对象的hashCode()值相等,会用链表存储,HashSet也同样。
ArrayList底层是用数组实现的,随着元素添加,其大小是动态增大的;在内存中是连续存放的;若是在集合末尾添加或删除元素,所用时间是一致的,若是在列表中间添加或删除元素,所用时间会大大增长。经过索引查找元素速度很快。适合场合:查询比较多的场景
LinkedList底层是经过双向链表实现的,LinkedList和ArrayList相比,增删速度快,但查询和修改值速度慢。在内存中不是连续内存。场景:增删操做比较多的场景。
(更多完整项目下载。未完待续。源码。图文知识后续上传github。)
领取完整版PDF
![]()
![]()