集合框架中包含了一系列不一样数据结构(线性表,查找表...),是用来保存一组数据的结构。html
整个集合框架关系展示
java
原图出处:http://pierrchen.blogspot.com/2014/03/java-collections-framework-cheat-sheet.htmlnode
处于图片左上角的那一块灰色里面的四个类(Dictionary、HashTable、Vector、Stack)都是线程安全的,但是它们都是JDK的老的遗留类。现在都有了相应的取代类。面试
当中Map接口是用来取代图片中左上角的那个Dictionary抽象类。算法
HashTable,官方推荐ConcurrentHashMap来取代。接着如下的Vector是List如下的一个实现类。数组
最上面的粉红色部分是集合类所有接口关系图。其中Collection有三个继承接口:List、Queue和Set。缓存
绿色部分则是集合类的主要实现类,也是咱们经常使用的集合类。安全
在这里,集合类分为了Map和Collection两个大的类别。数据结构
1) Collectionapp
一组"对立"的元素,一般这些元素都服从某种规则
1.1) List必须保持元素特定的顺序
1.2) Set不能有重复元素
1.3) Queue保持一个队列(先进先出)的顺序
2) Map
一组成对的"键值对"对象
集合分类:
依照实现接口分类:
实现Map接口的有:EnumMap、IdentityHashMap、HashMap、LinkedHashMap、WeakHashMap、TreeMap
实现List接口的有:ArrayList、LinkedList
实现Set接口的有:HashSet、LinkedHashSet、TreeSet
实现Queue接口的有:PriorityQueue、LinkedList、ArrayQueue
依据底层实现的数据结构分类:
底层以数组的形式实现:EnumMap、ArrayList、ArrayQueue
底层以链表的形式实现:LinkedHashSet、LinkedList、LinkedHashMap
底层以hash table的形式实现:HashMap、HashSet、LinkedHashMap、LinkedHashSet、WeakHashMap、IdentityHashMap
底层以红黑树的形式实现:TreeMap、TreeSet
底层以二叉堆的形式实现:PriorityQueue
Collection经常使用方法
int size():返回集合里边包含的对象个数
boolean isEmpty():是否为空(不是null而是里边没有元素)
boolean contains(Object o):是否包含指定对象
boolean clear():清空集合
boolean add(E e):向集合中添加对象
boolean remove(Object o):移出某个对象
boolean addAll(Collection <?extends E> c):将另外一个集合中的全部元素添加到集合中。
boolean removeAll(Collection<?> c):移出集合中与另外一个集合中相同的所有元素。
Iterator<E> iterator():返回该集合的对应的迭代器。
list经常使用方法
List除了继承Collection定义的方法外,还根据线性表的数据结构定义了一系列方法。
1)get(int index)方法,获取集合中索引的元素。
注:这个方法是List中独有的,返回的是Object
2)Object set(int index,Object obj):将给定的元素替换集合中索引为index的元素,返回的是被替换的元素。
3)add和remove有方法重载
add(int index, Object obj):将给定的元素插入索引处,原位置上及后面的元素顺序向后移(插队)。
Object remove(int index):删除指定索引处的元素,该方法的返回只是被删除的元素。
List还提供相似String的indexOf和lastIndexOf方法,用于在集合中检索某个对象,其判断逻辑为:(o==null?get(i)==null:o.equals(get(i)))
1)int indexOf(Object obj):返回首次在集合中出现该元素的索引值。
2)lastIndexOf(Object obj):返回最后一次在集合中出现该元素的索引值。
还有能够将集合转换为数组的方法:
3)toArray():将集合转化为数组。这里参数仅仅是告知集合要转换的数组类型,并不会使用咱们提供的数组,因此不须要给长度。
集合中的元素应为同一个类型。
String[] array = (String[])list.toArray(new String[0]);
ArrayList底层实现方式
ArrayList底层是用数组实现的存储。 特色:查询效率高,增删效率低,线程不安全。
ArrayList底层使用Object数组来存储元素数据。全部的方法,都围绕这个核心的Object数组来操做。
可是,数组长度是有限的,而ArrayList是能够存听任意数量的对象,长度不受限制。
其本质上就是经过定义新的更大的数组,将旧数组中的内容拷贝到新数组,来实现扩容。 ArrayList的Object数组初始化长度为10,若是咱们存储满了这个数组,须要存储第11个对象,就会定义新的长度更大的数组,并将原数组内容和新的元素一块儿加入到新数组中。
LinkedList底层实现
LinkedList底层用双向链表实现的存储。特色:查询效率低,增删效率高,线程不安全。
双向链表也叫双链表,是链表的一种,它的每一个数据节点中都有两个指针,分别指向前一个节点和后一个节点。 因此,从双向链表中的任意一个节点开始,均可以很方便地找到全部节点。每一个节点都应该有3部份内容:
class Node { Node previous; //前一个节点 Object element; //本节点保存的数据 Node next; //后一个节点 }
private static class Node<E> { //业务数据 E item; //指向下个node Node<E> next; //指向上个node Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
若是原来firstNode为空的话,说明这个list为空,那么这时FirstNode也就是lastNode,这个链表只有一个node。
首节点的prev和lastNode的next为null
HashMap实现原理
Map就是用来存储“键(key)-值(value) 对”的。 Map类中存储的“键值对”经过键来标识,因此“键对象”不能重复。
哈希表
哈希表(hash table)也叫散列表,是一种很是重要的数据结构,应用场景及其丰富,许多缓存技术(好比memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也经常出如今各种的面试题中,重要性可见一斑。
HashMap底层实现采用了哈希表,这是一种很是重要的数据结构。
数据结构中由数组和链表来实现对数据的存储,他们各有特色。
(1) 数组:占用空间连续。 寻址容易,查询速度快。可是,增长和删除效率很是低。
(2) 链表:占用空间不连续。 寻址困难,查询速度慢。可是,增长和删除效率很是高。
而“哈希表”具有了数组和链表的优势。 哈希表的本质就是“数组+链表”。
在哈希表中进行添加,删除,查找等操做,性能十分之高,不考虑哈希冲突的状况下,仅需一次定位便可完成,时间复杂度为O(1),接下来咱们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。
数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在数组中根据下标查找某个元素,一次定位就能够达到,哈希表利用了这种特性,哈希表的主干就是数组。
好比咱们要新增或查找某个元素,咱们经过把当前元素的关键码经过某个函数映射到数组中的某个位置,经过数组下标一次定位就可完成操做。
存储位置 = f(关键码)
其中,这个函数f通常称为哈希函数,经过关键码就能够直接定位到元素的存储位置。
哈希冲突
若是两个不一样的元素,经过哈希函数得出的实际存储地址相同怎么办?也就是说,当咱们对某个元素进行哈希运算,获得一个存储地址,而后要进行插入的时候,发现已经被其余元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。哈希函数的设计相当重要,好的哈希函数会尽量地保证 计算简单和散列地址分布均匀。可是,咱们须要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证获得的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap便是采用了链地址法,也就是数组+链表的方式。
或者
HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每个Entry包含一个key-value键值对。
一个Entry对象存储了:
1. key:键对象 value:值对象
2. next:下一个节点
3. hash: 键对象的hash值
存储数据
咱们的目的是将”key-value两个对象”成对存放到HashMap的Entry[]数组中。
(1) 得到key对象的hashcode
首先调用key对象的hashcode()方法,得到hashcode。
(2) 根据hashcode计算出hash值(要求在[0, 数组长度-1]区间)
hashcode是一个整数,咱们须要将它转化成[0, 数组长度-1]的范围。咱们要求转化后的hash值尽可能均匀地分布在[0,数组长度-1]这个区间,减小“hash冲突”
i. 一种极端简单和低下的算法是:
hash值 = hashcode/hashcode;
也就是说,hash值老是1。意味着,键值对对象都会存储到数组索引1位置,这样就造成一个很是长的链表。至关于每存储一个对象都会发生“hash冲突”,HashMap也退化成了一个“链表”。
ii. 一种简单和经常使用的算法是(相除取余算法):
hash值 = hashcode%数组长度
这种算法可让hash值均匀的分布在[0,数组长度-1]的区间。 早期的HashTable就是采用这种算法。可是,这种算法因为使用了“除法”,效率低下。JDK后来改进了算法。首先约定数组长度必须为2的整数幂,这样采用位运算便可实现取余的效果:hash值 = hashcode&(数组长度-1)。
(3) 生成Entry对象
如上所述,一个Entry对象包含4部分:key对象、value对象、hash值、指向下一个Entry对象的引用。咱们如今算出了hash值。下一个Entry对象的引用为null。
(4) 将Entry对象放到table数组中
若是本Entry对象对应的数组索引位置尚未放Entry对象,则直接将Entry对象存储进数组。若是对应索引位置已经有Entry对象,则将已有Entry对象的next指向本Entry对象,造成链表。
总结:
当添加一个元素(key-value)时,首先计算key的hash值,以此肯定插入数组中的位置,可是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就造成了链表,同一个链表上的Hash值是相同的,因此说数组存放的是链表。
▪ 取数据过程get(key)
咱们须要经过key对象得到“键值对”对象,进而返回value对象。明白了存储数据过程,取数据就比较简单了,参见如下步骤:
(1) 得到key的hashcode,经过hash()散列算法获得hash值,进而定位到数组的位置。
(2) 在链表上挨个比较key对象。 调用equals()方法,将key对象和链表上全部节点的key对象进行比较,直到碰到返回true的节点对象为止。
(3) 返回equals()为true的节点对象的value对象。
明白了存取数据的过程,咱们再来看一下hashcode()和equals方法的关系:
Java中规定,两个内容相同(equals()为true)的对象必须具备相等的hashCode。由于若是equals()为true而两个对象的hashcode不一样;那在整个存储过程当中就发生了悖论。
▪ 扩容问题
HashMap的位桶数组,初始大小为16。实际使用时,显然大小是可变的。若是位桶数组中的元素达到(0.75*数组 length), 就从新调整数组大小变为原来2倍大小。
扩容很耗时。扩容的本质是定义新的更大的数组,并将旧数组内容挨个拷贝到新数组中。
▪ JDK8将链表在大于8状况下变为红黑二叉树
JDK8中,HashMap在存储一个元素时,当对应链表长度大于8时,链表就转换为红黑树,这样又大大提升了查找的效率。
HashMap原理借鉴https://www.cnblogs.com/chengxiao/p/6059914.html
TreeMap原理实现
首先介绍一下二叉树和红黑二叉树
二叉树的定义
二叉树是树形结构的一个重要类型。 许多实际问题抽象出来的数据结构每每是二叉树的形式,即便是通常的树也能简单地转换为二叉树,并且二叉树的存储结构及其算法都较为简单,所以二叉树显得特别重要。
二叉树(BinaryTree)由一个节点及两棵互不相交的、分别称做这个根的左子树和右子树的二叉树组成。下图中展示了五种不一样基本形态的二叉树。
(a) 为空树。
(b) 为仅有一个结点的二叉树。
(c) 是仅有左子树而右子树为空的二叉树。
(d) 是仅有右子树而左子树为空的二叉树。
(e) 是左、右子树均非空的二叉树。
注:二叉树的左子树和右子树是严格区分而且不能随意颠倒的,图 (c) 与图 (d) 就是两棵不一样的二叉树。
排序二叉树特性以下:
(1) 左子树上全部节点的值均小于它的根节点的值。
(2) 右子树上全部节点的值均大于它的根节点的值。
好比:咱们要将数据【14,12,23,4,16,13, 8,,3】存储到排序二叉树中,以下图所示:
排序二叉树自己实现了排序功能,能够快速检索。但若是插入的节点集自己就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后获得的排序二叉树将变成普通的链表,其检索效率就会不好。 好比上面的数据【14,12,23,4,16,13, 8,,3】,咱们先进行排序变成:【3,4,8,12,13,14,16,23】,而后存储到排序二叉树中,显然就变成了链表,以下图所示:
平衡二叉树(AVL)
为了不出现上述一边倒的存储,科学家提出了“平衡二叉树”。
在平衡二叉树中任何节点的两个子树的高度最大差异为1,因此它也被称为高度平衡树。 增长和删除节点可能须要经过一次或屡次树旋转来从新平衡这个树。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子一、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并须要从新平衡这个树。
好比,咱们存储排好序的数据【3,4,8,12,13,14,16,23】,增长节点若是出现不平衡,则经过节点的左旋或右旋,从新平衡树结构,最终平衡二叉树以下图所示:
平衡二叉树追求绝对平衡,实现起来比较麻烦,每次插入新节点须要作的旋转操做次数不能预知。
红黑二叉树
红黑二叉树(简称:红黑树),它首先是一棵二叉树,同时也是一棵自平衡的排序二叉树。
红黑树在原有的排序二叉树增长了以下几个要求:
1. 每一个节点要么是红色,要么是黑色。
2. 根节点永远是黑色的。
3. 全部的叶节点都是空节点(即 null),而且是黑色的。
4. 每一个红色节点的两个子节点都是黑色。(从每一个叶子到根的路径上不会有两个连续的红色节点)
5. 从任一节点到其子树中每一个叶子节点的路径都包含相同数量的黑色节点。
这些约束强化了红黑树的关键性质:从根到叶子的最长的可能路径很少于最短的可能路径的两倍长。这样就让树大体上是平衡的。
红黑树是一个更高效的检索二叉树,JDK 提供的集合类 TreeMap、TreeSet 自己就是一个红黑树的实现。
红黑树的基本操做:插入、删除、左旋、右旋、着色。 每插入或者删除一个节点,可能会致使树不在符合红黑树的特征,须要进行修复,进行 “左旋、右旋、着色”操做,使树继续保持红黑树的特性。
TreeMap是红黑二叉树的典型实现
private transient Entry<K,V> root = null;
root用来存储整个树的根节点。咱们继续跟踪Entry(是TreeMap的内部类)的代码:
能够看到里面存储了自己数据、左节点、右节点、父节点、以及节点颜色。
TreeMap的put()/remove()方法大量使用了红黑树的理论。
TreeMap和HashMap实现了一样的接口Map,所以,用法对于调用者来讲没有区别。HashMap效率高于TreeMap;在须要排序的Map时才选用TreeMap。
HashSet实现原理
HashSet是采用哈希算法实现,底层实际是用HashMap实现的(HashSet本质就是一个简化版的HashMap),所以,查询效率和增删效率都比较高。发现里面有个map属性,这就是HashSet的核心秘密。咱们再看add()方法,发现增长一个元素说白了就是在map中增长一个键值对,键对象就是这个元素,值对象是名为PRESENT的Object对象。
本质就是把这个元素做为key加入到了内部的map中”。
因为map中key都是不可重复的,所以,Set自然具备“不可重复”的特性。
TreeSet实现原理
TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,经过key来存储Set的元素。 TreeSet内部须要对存储的元素进行排序,所以,咱们对应的类须要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
(1) 因为是二叉树,须要对元素作内部排序。 若是要放入TreeSet中的类没有实现Comparable接口,则会抛出异常:java.lang.ClassCastException。
(2) TreeSet中不能放入null元素。