什么是集合
通俗的说,集合就是一个放数据的容器,准确的说是放数据对象引用的容器。
说到这里就会想到数组它事和集合一样也是放数据的容器,面试的时候会有经常问到的一个面试题集合和数组的区别
1,数组可以存储基本数据类型,也可以存储引用数据类型,集合只能存储引用数据类型(类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型例如,String
类型就是引用类型,简单来说,所有的非基本数据类型都是引用数据类型)
2,数组的缺点是长度是固定的数组,没有办法动态扩展,集合的长度是可变的可以动态的扩展
3,数组中存储的是统一的类型的元素(比如int,String等是固定死的)可以存储基本数据类型值(Java的基本数据类型分四类八种
· 逻辑类型:boolean
·整数类型:byte、short、int、long
·浮点类型 :float、double
·字符类型:char
),集合存储的都是对象,集合容器因为内部的数据结构不同,有多种不同的容器对象,这些容器对象不断的向上抽取,就形成了集合框架,在开发中一般当对象多的时候,使用集合进行存储。
集合的架构体系
Map双列集合和Collection单列集合区别
Map集合存储元素是成对出现的,Map集合的键是唯一的,值是可重复的,简单来说这个理解为:夫妻对,属于双列集合
Collection集合存储元素是单独出现的,Collection的儿子Set是唯一的,List是 可重复的。可以把这个理解为:光棍,属于单列集合
Conllction 单列集合父类
conllection类是所有单列集合的顶层父类,底下有list 和set接口
List 接口
元素存储和取出元素顺序相同,可存储重复元素,有索引,可用for循环遍历
Arraylist 集合 底层原理: Arraylist的底层数据结构就是一个(Objiect[ ] elementData )elementData Object类型的数组,底层在不指定数组初始容量的空列表的时候默认长度是10,可指定Arraylist集合数组初始容量的空列表,底层会先判断 这个新加的数组长度是否大于0,如果大于0就把当前指定的数据长度复制给底层的这个 Objiect[ ] elementData Objectl类型的数组,再判断如果等于0会设置清空元素数据 ,元素数组为空,即将EMPTY_ELEMENTDATA赋给elementData,最后上面两种条件都不满足的情况直接抛出异常(非法的容量) (如图1)
图1:
ArrayList底层扩容机制
ArrayList的扩容主要发生在向ArrayList集合中添加元素在调用add()方法的分析可知添加前必须确保集合的容量能够放下添加的元素,通过底层会发现add()()方法中调用ensureCapacityInternal(size + 1)方法来确定集合确保添加元素最小集合容量int minCapacity的值,首选会判断元素数组是否为空数组,如果是,minCapacity(最小容量)与集合默认容量大小进行比较取最大值
图1:
调用ensureExplicitCapacity(minCapacity)方法来确定集合为了确保添加元素成功是否需要对现有的元素数组进行扩容,首先将modCount++;(modCount 这个是父类AbstractList集合的一个属性,作用是用来记录集合的数据机构的修改次数,增删改都会用这个属性去记录次数) 结构性修改计数加一,然后判断minCapacity与当前元素数组的长度的大小,如果minCapacity最小容量比当前元素数组的长度的大小大于0的时候调用 grow(minCapacity)方法逻辑进行扩容。
图2;
如果需要对现有的元素数组进行扩容,则调用grow(minCapacity)方法,参数minCapacity表示集合为了确保添加元素成功的最小容量,在扩容的时候,首先将原元素数组的长度(>>1)扩容到原来的1.5倍(oldCapacity + (oldCapacity >> 1))创建一个新的newCapacity 新的容量,然后对扩容后的容量与minCapacity进行比较如果 新容量小于minCapacity最小容量,则将新容量设为minCapacity最小容量,提供新容量大于minCapacity最小容量,则指定新容量,最后调用Arrays.copyOf()方法将旧数组拷贝到扩容后的新数组中进行数组扩容。
图3:
其实看到说这里我们可以看下Arrrays.copyOf这个方法底层调用的是System.arraycopy() 这个方法,这个方法被native修饰的本地方法,到了这种底层一般都不是java自己执行,都是委托给其他语言执行 被native修饰方法的实现由非java语言实现,比如C。
图4:
优点:底层数据结构是数组,查询快,增删慢 ,效率高
为什么数组就查询快增删慢呢?
我的理解是数组在内存中连续使用数组之前,必须事先固定数组长度,不支持动态改变数组大小,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中,而查询的时候数组会根据索引下标直接找到对应的数据效率是非常高的 ,链表恰好相反,链表中的元素不是连续有顺序存储的,而是通过存在的元素中的指针联系到一起,比如:上一个元素有个指针指到下一个元素,直到最后一个元素,如果要访问链表其中的一个元素,需要从第一个元素开始,一直找到需要的元素位置 那么查询的效率就非常低,如果想要增加或者删除只需要修改某个元素中的指针就可以了
简单来说, 数组就像一列火车,有着车厢编号,每一节车厢里又有座位,座位也有编号,你想找到哪节车厢的哪个位置的乘客就很方便,而火车每一节之间是固定的,不便于拆分插入新的车厢,如果插入新的车厢,受影响的车厢都要更改编号,这很像新增或修改数组的脚标。
简单俩说链表更像是我们排队,只需要记住我在谁的后边,谁在我的后边就可以了,基于指针,方便插入和删除数据,因为链表的元素可以在内存中不连续,全部依赖于指针
缺点: 线程不安全
既然ArrayList是线程不安全的,但如果需要在多线程中使用,可以采用list<Object> list =Collections.synchronizedList(new ArrayList<Object>)来创建一个ArrayList对象。
//开发中建议使用带参构造器创建ArrayList(int capacity),避免频繁扩容。
Linkedlist集合 底层原理:Linkedlist集合底层的数据结构是基于双向链表的 每个节点有prev和next两个前、后指针,同时用first和last指针分别指向链表的头尾节点,方便对链表收尾进行插入删除等操作,所以LinkedList集合默认没有初始长度。
我们再来看Linkedlist集合add(E e)方法 获得当前最后一个节点最为当前节点的前置节点,同样把当前节点设置为前置节点的后置节点,然后把当前节点作为最后一个节点,因为只需要创建一个节点与前一个节点建立前后关系即可,时间复杂度是O(1)。
总结:
其实相比于ArrayList集合
来说,LinkedList结合
其实内部原理要简单的多,就是一个双端链表,所有的逻辑操作就是对链表上某一节点的前置节点和后置节点之间的关系变更,虽然看上去很绕,但是只要熟悉链表理解起来并不难因为是实现了Deque
接口的缘故,有很多队列的方法,很多方法只是不同名字而已,只要记住一个原则,就是所有对链表头部和尾部的操作时间复杂度一定是O,因为内部有两个变量记录了头尾两个节点,但是只要涉及到是对链表中部做操作,查询、插入和删除都必定是O(n),因为这些操作都需要先进行查找,而链表的查找只能是遍历,因为是链表结构,所以整个集合也就没有大小限制了,也不会有什么初始化值和扩容了。
底层是双向链表、查询慢,增删快,线程不安全
Vector集合 底层原理: Vector集合底层数据结构是数组底层逻辑跟ArrayList底层差不多,不同的是Vector集合每个方法都添加了synchronized关键字来保证同步所以它是线程安全的,扩容机制默认是当前数组大小的2倍 ,这里就不多做解释了。
Set接口
元素是无序的存储和取出元素顺序可能是不一致的,不允许重复的的元素,最多可以存储一个null值
HashSet集合 底层原理 : HashSet集合子类是LinkedHashSet集合,底层是哈希表数据结构在JDK8之前,底层采用数组+链表实现,JDK8之后底层进行了优化,引入了红黑树数据结构由数组+链表+红黑树实现,看底层代码我们能了解到HashSet底层其实是一个HashMap容器能够继承HashMap的所有特性,所以不能使用get方法,key也不允许重复,但支持null对象作为key,数组的长度默认为16,加载因子为0.75
特点:底层是哈希表结构,元素是无序的允许有null值存在,不是线程安全的集合没有带索引的方法,所以不能使用普通for循环来遍历(可以使用迭代器和增强for遍历)
LinkedHashSet集合 底层原理:LinkedHashSet集合是 HashSet集合的子类有着 HashSet集合的所有特性 有序的不允许重复,无索引
TreeSet 集合 底层原理:TreeSet是基于TreeMap实现的,说到这个就去看看下面TreeMap的底层原理吧
特点:元素是有序的,不能允许重复。
Map双列集合父类
Map接口 map接口是以Key-Value的形式存在的, key不允许重复允许有一个null键,多个null值,键值对存在一对一的关系,可以通过指定的key找到对应的value值 ,线程不安全集合
特点:
-
key(键):元素是无序,没有无索引,元素不可重复(唯一)
-
value(值):元素是无序,没有索引,元素允许重复
HashMap集合
通过查看源码我们可以看到HashMap继承了AbstractMap类 实现了Map接口, HashMap集合底层是哈希表数据结构在JDK8之前,底层采用数组+链表实现,JDK8之后底层进行了优化,引入了红黑树数据结构由数组+链表+红黑树实现 ,HashMap底层采用一个Entry[]数组来保存所有的key-value键值对,通过查看HashMap底层源码发现,默认长度是1<<4等于16,最大长度是1 << 30算下来是(1073741824)10多亿左右,默认加载系数(填充因子)0.75f,它有一个 TREEIFY_THRESHOLD(树形阈值)为8,当树型阀值超过8是由链表变为红黑树,扩容为原来的两倍,并且将原来的数据复制过来,底层依赖hashCode方法和equals方法保证键的唯一
特点:以键值对形式,无序,无索引,线程不安全
HashMap的put方法(保证key的唯一性)
HashMap在put的数据的时候,在底层代码中会先通过判断put的值的key是否为null,如果为null,会固定存放到table[0]下面,如果不为null,会通过hash()方法计算出key对应的hash地址,通过hash地址去寻找数据应存放的table的指定索引下,找到之后会判断put的key在链表中是否存在(地址值和具体值都要判断)如果存在则为替换,如果不存在则为新增
哈希冲突
哈希算法其实是一个复杂的运算,它的输入可以是字符串,可以是数据,可以是任何文件,经过哈希运算后,变成一个固定长度的输出,该输出就是哈希值
当两个不同的数经过哈希函数计算后得到了同一个结果,即他们会被映射到哈希表的同一个位置时,即称为发生了哈希冲突
简单来说:如果两个不同对象的hashCode相同,这种现象称为哈希冲突。大白话我的理解是我认为哈希表其实就是一个存放哈希值的一个数组,哈希值是通过哈希函数计算出来的,那么哈希冲突就是两个不同值的东西,通过哈希函数计算出来的哈希值相同,这样他们存在数组中的时候就会发生冲突,这就是哈希冲突。比如这个就像是我们平时坐高铁买座位一般是一人一座的,但是突然系统可能出了问题,两个人可能买到了同一个座位的票,那么这时候就发生了冲突。
如何解决哈希冲突
一般解决哈希冲突有以下四个方法:
1.开放定址法
我们在遇到哈希冲突时,去寻找一个新的空闲的哈希地址
大白话就说当我们去教室上课,这时候发现该位置已经存在人了,这时候我们应该怎么办应该寻找新的位子坐下
又分为 线性探测法 和 平方探测法(二次探测)
线性探索法的原理是, 当我们的所需要存放值的位置被占了,我们就往后面一直加1并对m取模直到存在一个空余的地址供我们存放值,取模是为了保证找到的位置在0~m-1的有效空间之中。
公式:h(x)=(Hash(x)+i)mod (Hashtable.length);(i会逐渐递增加1)
平方探测法(二次探测)的原理是:当我们的所需要存放值的位置被占了,会前后寻找而不是单独方向的寻找。
公式:h(x)=(Hash(x) +i)mod (Hashtable.length);(i依次为+(i^2)和-(i^2))
2.再哈希法
原理是同时构造多个不同的哈希函数,等发生哈希冲突时就使用第二个、第三个……等其他的哈希函数计算地址,直到不发生冲突为止。虽然不易发生聚集,但是增加了计算时间。
3.链地址法
原理是将所有哈希地址相同的记录都链接在同一链表中。
4.建立公共溢出区
原理是将哈希表分为基本表和溢出表,将发生冲突的都存放在溢出表中。
以后的解决方式只是大体概括描述下原理如果有兴趣的话可以深入去了解,这里就不做过多的解释了
LinkedHashMap集合
LinkedHashMap是HashMap类的一个子类,底层原理和HashMap差不多,LinkedHashMap相对于HashMap,增加了双链表的结果(即节点中增加了前before、后after指针,LinkedHashMap中增加了head、tail指正),其他处理逻辑与HashMap一致,同样也没有锁保护,多线程使用存在风险是线程不安全的集合
HashTable集合 底层原理,通过查看HashTable底层源码我们可以看到Hashtable继承了Dictionary类实现了Map接口,HashTable和HashMap的底层逻辑原理是差不多,相对于HashMap的最大特点就是线程安全,所有的操作都是被synchronized锁保护的
TreeMap集合 通过查看源码我们可以看到TreeMap继承了AbstractMap类 实现了NavigableMap接口,底层原理TreeMap底层是红黑树数据结构,TreeMap是线程不安全的
特点:.元素是无序,不允许重复
底层数据结构
/**
* TreeMap
*/
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
// 创建Map传进去的比较器,不传为null
private final Comparator<? super K> comparator;
// 存储根节点
private transient TreeMapEntry<K,V> root;
// 大小
private transient int size = 0;
// 修改次数
private transient int modCount = 0;
}
TreeMapEntry的数据结构,形成红黑树和存储最重要的实体类
/**
* TreeMap的静态内部类TreeMapEntry
*/
static final class TreeMapEntry<K,V> implements Map.Entry<K,V> {
// 保存我们存储的Key
K key;
// 保存我们存储的Value
V value;
// 当前节点的左子节点
TreeMapEntry<K,V> left;
// 当前节点右子节点
TreeMapEntry<K,V> right;
// 当前节点父节点
TreeMapEntry<K,V> parent;
// 红黑树的节点颜色
boolean color = BLACK;
// 构造方法,KV及父节点
TreeMapEntry(K key, V value, TreeMapEntry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
}
TreeMap提供了四个构造方法,实现了方法的重载
/**
* TreeMap构造方法
*/
// 无参构造方法,
public TreeMap() {
comparator = null;
}
// 带有比较器comparator的构造方法
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
// 原集合需要添加到新集合的构造方法
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
// 参数为SortedMap的构造方法
// 采用m的比较器排序
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
自然排序:TreeMap的所有key必须实现Comparable接口,所有的key都是同一个类的对象。
定制排序:创建TreeMap对象传入了一个Comparator对象,该对象负责对TreeMap中所有的key进行排序,采用定制排序不要求Map的key实现Comparable接口
put方法
/**
* TreeMap类的put方法
*/
public V put(K key, V value) {
// 获取成员变量保存的根节点,赋值给t
TreeMapEntry<K,V> t = root;
// 根节点还没有
if (t == null) {
compare(key, key); // type (and possibly null) check
// 创建一个根节点,赋值给成员变量root,根节点的parent必然是null啦
root = new TreeMapEntry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
TreeMapEntry<K,V> parent;
// 获取创建TreeMap时设置的比较器
Comparator<? super K> cpr = comparator;
if (cpr != null) {
// 有定制的比较器
// 通过do-while循环的方式,借助定制的比较器,来找出需要插入的这个key的parent是谁
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
// 没有定制的比较器,自然排序的情况
// 这种情况是不允许我们插入null的key
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
// 获取key本身的比较器Comparable
// 这也是为什么上面说的,key必须要实现Comparable接口
Comparable<? super K> k = (Comparable<? super K>) key;
do {
// 逻辑和定制比较器部分一致
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
// 找到要插入的KV的parent后,创建一个TreeMapEntry对象存储这些数据信息
TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
// 根据和父节点的比较结果,是放在父节点的左边还是右边
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 这个方法就比较复杂了,新插入节点后重新调整红黑树
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
TreeMap插入数据后,红黑树重新调整的方法
/**
* TreeMap插入数据后,红黑树重新调整的方法
*/
private void fixAfterInsertion(TreeMapEntry<K,V> x) {
// 插入的节点默认是红色的
x.color = RED;
//(1)新添加节点N为根:涂黑完事(因为根节点必须为黑)
//(2)新添加的节点的父节点是黑的:啥事不用管,添加进来就行
while (x != null && x != root && x.parent.color == RED) {
// 这里面就是处理我那篇博客里的(3)和(4.a)(4.b)的情况
// 判断x的节点的父节点位置,是否属于左孩子
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
// 获取叔叔节点
TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
// 属于情况(3)
// (3)新添加节点的父红和叔红:父/叔涂黑,祖父涂红,然后把祖父当成新的平衡节点递归处理
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
// 父红,叔黑
if (x == rightOf(parentOf(x))) {
// (4.a)的情况,不在同一边,先旋转扭转
x = parentOf(x);
rotateLeft(x);
}
// (4.b)的情况,在同一边,然后以祖父为中心旋转
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
// 右旋
rotateRight(parentOf(parentOf(x)));
}
} else {
// 父节点是属于右孩子的情况
// 获取叔叔
TreeMapEntry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
// 叔叔是红的
// 情况(3)
// (3)新添加节点的父红和叔红:父/叔涂黑,祖父涂红,然后把祖父当成新的平衡节点递归处理
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
// 父红,叔黑
if (x == leftOf(parentOf(x))) {
// (4.a)的情况,不在同一边,先旋转扭转
x = parentOf(x);
rotateRight(x);
}
// (4.b)的情况,在同一边,然后以祖父为中心旋转
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
// 左旋
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
集合种常见的面试题
1. HashMap的底层实现原理?
hashmap的底层是哈希表,是基于hash算法实现的,hashmap通过put(key,value)存储,通过get(key)获取,当传入key时,hashmap会调用key.hashcode()方法计算出hash值,根据 hash 值将 value 保存在 bucket 里。当计算出的 hash 值相同时,我们称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的value。当 hash 冲突的个数少于等于8个时,使用链表否则使用红黑树。
2. HashMap和Hashtable的区别
HashMap继承了AbstractMap类 实现了Map接口 ,Hashtable继承了Dictionary类实现了Map接口,HashMap线程不安全的效率高,Hashtable线程安全效率底
3.Collection 和 Collections 有什么区别?
Collection 提供了对集合对象进行基本操作的通用接口方法。其直接继承接口有List与Set
Collections是工具类,主要功能有用于对集合中元素进行排序、搜索以及线程安全等包含了很多静态方法,不能被实例化,比如提供的排序方法:Collections. sort(list)
4,ArrayList和LLinkedlist的区别
ArrayList底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是双向循环链表,不支持随机访问,使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)
5.HashMap和HashTable的区别
HashMap允许空键值,HashTable不允许,HashMap线程不安全(效率高),HashTable线程安全。
6.ArrayList和Vector 的区别是什么?
Vector使用了synchronized来实现线程同步,是线程安全的,而ArrayList是非线程安全的,Vector扩容每次会增加1倍,而ArrayList只会增加0.5倍。