一.集合框架底层数据结构
Collection
1.List(有序、可重复)
List里存放的对象是有序的,同时也是可以重复的,List关注的是索引,拥有一系列和索引相关的方法,查询速度快。因为往list集合里插入或删除数据时,会伴随着后面数据的移动,所有插入删除数据速度慢。List是列表类型,以线性方式存储对象。
ArrayList:Object数组
Vector:Object数组
LinckedList:双向循环链表
2.Set(无序、不能重复)
Set里存放的对象是无序,不能重复的,集合中的对象不按特定的方式排序,只是简单地把对象加入集合中。
HashSet:无序,唯一,底层基于HashMap实现,采用HashMap保存元素
LinkedHashSet:LinkedHashSet继承于HashSet,并且其内部是通过LinkedHashMap实现
TreeSet:有序,唯一,红黑树(自平衡的排序二叉树)
Map(键值对、键唯一、值不唯一)
Map集合中存储的是键值对,键不能重复,值可以重复。根据键得到值,对Map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。Map接口提供了将键映射到值的对象,一个映射不能包含重复的键,每个键最多只能映射一个值。
HashMap:JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突),JDK1.8之后在解决哈希冲突时有了较大变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少检索时间。
LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层结构仍然是基于拉链式散列结构(即由数组+链表或红黑树组成)。另外LinkedHashMap在此结构的基础上,增加了一条双向链表,使得此结构可以保持键值对的插入顺序,同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
TreeMap: 红黑树(自平衡的排序二叉树)。
二.线程安全的集合类
Vector:就比ArrayList多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
Stack:堆栈类,先进后出。
HashTable:就比HashMap多了个线程安全。
Enumeration:枚举,相当于迭代器。
三.确保集合不能被修改
可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
List<String> list = new ArrayList<>();
list.add("x");
Collection<String> clist = Collections.unmodifiableCollection(list);
clist.add("y"); // 运行时此行报错
System.out.println(list. size());
四.迭代器 Iterator
Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
List<String> list = new ArrayList<>();
Iterator<String> it = list.iterator();
while(it.hasNext()){
String obj = it.next();
System.out.println(obj);
}
五.ArrayList 的优缺点
优点:
ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
ArrayList 在顺序添加一个元素的时候非常方便。
缺点:
删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
插入元素的时候,也需要做一次元素复制操作,缺点同上。
六.插入数据时,ArrayList、LinkedList、Vector速度比较,ArrayList、Vector、LinkedList 的存储性能和特性。
ArrayList、LinkedList、Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。
Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差。
LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快。
七.多线程场景下使用 ArrayList
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}
八.HashSet元素重复校验机制
向HashSet 中add()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles方法比较。HashSet 中的add()方法会使用HashMap的put()方法。
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。
HashSet 部分源码:
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
return map.put(e, PRESENT)==null;
}
hashCode()与equals()的相关规定:
如果两个对象相等,则hashcode一定也是相同的;
两个对象相等,对两个equals方法返回true;
两个对象有相同的hashcode值,它们也不一定是相等的;
综上,equals方法被覆盖过,则hashCode方法也必须被覆盖;
hashCode()的默认行为是对堆上的对象产生独特值,如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
==与equals的区别
==是判断两个变量或实例是不是指向同一个内存空间 ;equals是判断两个变量或实例所指向的内存空间的值是不是相同;
==是指对内存地址进行比较;equals()是对字符串的内容进行比较;
==指引用是否相同;equals()指的是值是否相同。
九.BlockingQueue
java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
十.HashMap的实现原理
HashMap概述:HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap的数据结构:在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
HashMap 基于Hash算法实现的
当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标。
存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中;获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。需要注意Jdk1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)。
在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8之后相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
JDK1.8主要解决或优化了一下问题:
- resize 扩容优化
- 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
- 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
不同 | JDK 1.7 | JDK 1.8 |
存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
初始化方式 | 单独函数:inflateTable() | 直接集成到了扩容函数resize() 中 |
hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 |
存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 |
插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) |
扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + |
HashMap是怎么解决Hash冲突的?
Hash:一般翻译为“散列”或音译为“哈希”,就是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值或哈希值。这种转换是一种压缩映射,也就是散列值的空间通常远小于输入的空间,不同的输入可能会散列出相同的输出,所以不可能从散列值来判定唯一且确定的输入值。简单的说就是一种任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有一个基本特性:根据同一散列函数计算的散列值如果不同,那么输入值肯定也不同,但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
哈希冲突:当两个不同的输入值,根据同一散列函数计算出相同的散列值得现象,就称之为哈希冲突或碰撞。在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点:寻址容易,增删困难;链表的特点:寻址困难,增删容易。所以我们将数组和链表结合在一起,发挥各自优势,使用一种交链地址法的方式解决哈希冲突。
HashMap的数据结构示意:
这样我们就可以将拥有相同hash值得对象组织成一个链表放在hash值对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始容量大小:DEFAULT_INITIAL_CAPACITY = 1 << 4
(即2的四次方16)远小于int类型的范围,所以我们只要单纯的用hashCode取余来获取对应的bucket,这将会大大增加哈希碰撞的概率,并且最坏的情况下还会将HashMap变成一个单链表,所以还需要对hashCode做一定优化。
hash()函数:上面提到的问题,只要是因为如果使用hashCode取余,name相当于参与运算的只有hashCode的低位,高位没有起到任何作用,所以我们的思路就是让hashCode值的高位也参与运算,进一步降低hash碰撞的概率,是的数据分布更平均,我们把这样的操作成为扰动,在JDK1.8中的hash()函数如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}
这比在JDK1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在JDK1.8中,只进行了1次位运算和1次异或运算(2次扰动)。
JDK1.8新增红黑树:
通过上面的链地址法(使用散列表)和扰动函数,我们成功让我们的数据分布更均匀,哈希碰撞较少,但是当我们的HashMap中存在大量数据时,加入某个bucket下对应的链表有n个元素,那么遍历的时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);
总结:HashMap是使用了哪些方法来有效解决哈希冲突的?
1.使用链地址法(使用散列表)来连接拥有相同hash值得数据;
2.使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
3.引入红黑树进一步降低遍历的时间复杂度,使得遍历更快。
如果使用Object最为HashMap的Key,应该怎么办呢?
重写hashCode()和equals()方法。
1.重写hashCode()是因为需要计算数据的存储位置,需要注意不要试图从散列码中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的哈希碰撞。
2.重写equals()方法,需要遵守自反性,对称性,传递性,一致性以及对于任何非null的引用值x,x.equals(null)必须返回false有这几个特性,目的是为了保证key在哈希表中的唯一性。
十一.ConcurrentHashMap和HashTable的区别
ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。
HashMap 没有考虑同步,HashTable 考虑了同步的问题。
但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
十二.TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?
TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口,从而根据键对元素进行排序。
Collections工具类的sort方法有两种重载的形式:
第一种要求传入的待排序容器中存放的对象实现Comparable接口以实现元素的比较。
第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用。