文章目录
- 一、Collection相关
- 二、HashSet相关
- 三、HashMap相关
-
- 3.说说List,Set,Map三者的区别?
- 3.HashMap与HashTable的区别?
- 3.HashMap和HashSet的区别
- 3.比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
- 3.ConcurrentHashMap和Hashtable的区别?
- 3.ConcurrentHashMap线程安全的底层具体实现
- 3.HashMap 默认的初始化长度是多少?
- 3.谈谈对HashMap 构造方法中初始容量、加载因子的理解
- 3.HashMap底层原理:
- 3.Hashmap如何添加一个元素
- 3.HashMap扩容机制:
- 3.为何HashMap的数组长度一定是2的次幂?
- 3.HashMap在Java7和Java8中的实现有什么不同?
- 3.为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者不是20呢?
- 3.哈希表如何解决Hash冲突?
- 3.为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化
- 3.为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键
- 3.HashMap 中的 key若 Object类型, 则需实现哪些方法?
- 3.HashMap1.7是如何形成死循环的(头插法导致的)?
- 3.Map注意事项:
- 四、LinkedHashSet相关
- 五、hashcode
- 六、ArrayList
- 七、LinkedList相关
- 八、其它
一、Collection相关
1.数组和集合的比较
注意,我们现在说的数组可不是ArrayList,而且new double[10]
的那个数组
数组的问题:
1)长度开始时必须指定,而且一旦指定,不能更改
2)保存的必须为同一类型的元素
3)使用数组进行增加或者删除元素的示意代码-比较麻烦
比如你一开始创建了一个大小为3的int数组new Int[3],当你需要扩容的时候你必须new Int[4]然后使用for循环把原先数组的数据拷贝过去。
所以数组的问题就是长度必须指定而且一旦指定就不能修改。而且保存的数据必须是同一类型的元素。
所以我们要使用集合,集合有哪些好处呢?
集合的好处
1)可以动态保存任意多个对象,使用比较方便
2)提供了一系列方便的操作对象的方法:add、remove、set,get等
3)使用集合添加,删除新元素的示意代码-简洁了
1.Collections类是什么?
Java.util.Collections是一个工具类仅包含静态方法,它们操作或返回集合。它包含操作集合的多态算法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜索、排序、混编和逆序等。
1.为何Collection不从Cloneable和Serializable接口继承?
Collection接口指定一组对象,对象即为它的元素。如何维护这些元素由Collection的具体实现决定。例如,一些如List的Collection实现允许重复的元素,而其它的如Set就不允许。
当与集合的具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以,集合的具体实现应该决定如何对它进行克隆或序列化,或它是否可以被克隆或序列化。而不是让整个集合类都从Cloneable和Serializable接口继承。
在所有的实现中授权克隆和序列化,最终导致更少的灵活性和更多的限制。特定的实现应该决定它是否可以被克隆和序列化。
1.为何Map接口不继承Collection接口?
1.首先Map提供的是键值对映射(即Key和value的映射),而collection提供的是一组数据(并不是键值对映射)。如果map继承了collection接口,那么所有实现了map接口的类到底是用map的键值对映射数据还是用collection的一组数据呢。
2.Map和List、set不同,Map放的是键值对,list、set放的是一个个的对象。说到底是因为数据结构不同,数据结构不同,操作就不一样,所以接口是分开的。
1.常见的集合有哪些?
Collection接口的子接口包括:Set接口和List接口
List接口的实现类主要有:ArrayList、LinkedList、Vector以及Stack等
Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
Map接口的实现类主要有:HashMap、TreeMap、LinkedHashMap、Hashtable、Properties等
1.常见的集合底层实现
List:
ArrayList底层是数组。
Vector底层是数组。
LinkedList底层是双向链表。
Set:
HashSet底层是HashMap。
TreeSet底层是红黑树。
LinkedHashSet底层是LinkedHashMap。
Map:
HashMap在jdk1.7是数组+链表,jdk1.8后是数组+链表+红黑树
LinkedHashMap底层修改自HashMap,底层虽然也是数组+链表+红黑树,但它包含一个维护插入顺序的双向链表。
HashTable底层是数组+单项链表组成的哈希表。
TreeMap底层是红黑树
1.如何选用集合?
主要根据集合的特点来选用,
比如我们需要根据键值获取到元素值时就选用 Map 接口下的集合:
需要排序时选择 TreeMap,不需要排序时就选择 HashMap,
需要保证线程安全就选用 ConcurrentHashMap。
当我们只需要存放元素值时,就选择实现Collection 接口的集合:
需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet,
不需要就选择实现 List 接口的比如 ArrayList 或 LinkedList,然后再根据实现这些接口的集合的特点来选用。
1.哪些集合类提供对元素的随机访问?
ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。
1.Java集合框架是什么?说出一些集合框架的优点?
集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:
- 接口:是代表集合的抽象数据类型。例如 Collection、List、Set、Map等。之所以定义多个接口,是为了以不同的方式操作集合对象
- 实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
- 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。
除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。
集合框架的部分优点如下:
(1) Java集合框架为程序员提供了预先包装的数据结构和算法来操纵他们。
(2)使用核心集合类降低开发成本,而非实现我们自己的集合类。
(3)随着使用经过严格测试的集合框架类,代码质量会得到提高。
(4)通过使用JDK附带的集合类,可以降低代码维护成本。
(5)复用性和可操作性。
1.集合框架中的泛型有什么优点?
你把方法写成泛型,这样就不用针对不同的数据类型(比如int,double,float)分别写方法,只要写一个方法就可以了,提高了代码的复用性,减少了工作量。
泛型允许我们为集合提供一个可以容纳的对象类型,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException
1.队列和栈是什么,列出它们的区别?
栈和队列两者都被用来预存储数据。java.util.Queue是一个接口,它的实现类在Java并发包中。
队列允许先进先出(FIFO)检索元素,但并非总是这样。Deque接口允许从两端检索元素。
栈与队列很相似,但它允许对元素进行后进先出(LIFO)进行检索。
Stack是一个扩展自Vector的类,而Queue是一个接口。
二、HashSet相关
2.HashSet
(1)HashSet 的底层
HashSet 的底层结构就是 HashMap
思考: 但是为什么我调用 HashSet.add() 的方法,只需要传递一个元素,而 HashMap 是需要传递 key-value 键值对 ?
首先我们查看 hashSet 的 add 方法
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
我们能发现但我们调用 add 的时候,存储一个值进入map中,只是作为key进行存储,而 value 存储的是一个Object 类型的常量,也就是说 HashSet 只关心key,而不关心 value 。
(2)关于HashSet的几件事
1)可以存放null值,但是只能有一个null(相同的元素不能存放多个)
2) HashSet不保证存放元素的顺序和取出顺序一致,但是无论取多少次每次取出的顺序是一样的
3)不能有重复元素/对象
4)在存储对象进HashSet时,对象重写equals方法一定要重写hashCode方法(老生常谈了,重写了equals后必须重写hashcode)
set = new HashSet();
set.add("Lucy");//添加成功
set.add("Lucy");//添加失败(重复的东西只能添加一次)
set.add(new Dog("tom"));//添加成功
set.add(new Dog("tom"));//添加成功(每次使用new都会new出一个新的对象,所以二者并没有冲突了)
set.add(new String("aaa"));//添加成功
set.add(new String("aaa"));//添加失败(重复的东西只能添加一次)
String它是重写了equals()方法的:它规定如果两个String名字相同那么它们就相等;
但是你new一个Dog对象时由于你并没有重写equals()方法,并没有规定两只狗怎么样才算相等,所以两个对象自然也就不一样;
但是如果你要重写Dog对象的equals方法一定要重写hashCode方法(老生常谈了,重写了equals后必须重写hashcode)
3.HashMap底层原理:
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就按照相应的方法插入。
JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
3.HashMap如何添加一个元素
HashSet底层是HashMap,HashMap添加一个元素时,
第一步,先执行hash(Object key),把key转为对应的hashcode
第二步,执行putVal(hashcode, key, value)方法,具体如下:
先根据数组长度和hashcode执行一次路由运算 得到index,根据index找node数组中的元素,
情况①:node数组中该索引下没有元素,那太好了,我直接把k-v键值对扔进去
情况②:node数组中该索引下有元素,而且很巧的是当前桶位中的元素的key与你要插入的元素的key完全一致,那就执行替换操作;
情况③:node数组中该索引下有元素,而且当前桶位中的元素的key与你要插入的元素的key不一致,那就判断它下面是链表结构还是树结构,如果是链表,那就遍历链表,如果如果到末尾了还是没有找到这个key那就把这个k-v键值对插到最后一位。把元素添加到链表后判断如果当前链表的大小大于8个节点,就调用树化方法,但是在进行树化时里面还会判断总元素数量是否小于64,如果小于64的话就不会把他变为红黑树而是先对表进行扩容;
情况④:已经树化了,另说。
第三步,因为你插入了新元素所以在函数最后还判断了现在散列表所有元素个数是否大于扩容阈值,如果大于扩容阈值那就执行 resize(),进行扩容
2.HashSet扩容机制:
1.第一次添加时,table数组扩容到16,临界值(threshold)是16 乘以 加载因子(loadFactor)0.75 = 12
2.如果table数组使用到了临界值12,就会扩容到16 x 2= 32,新的临界值就是32 x 0.75= 24,依次类推
3.在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_ CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
4.如果单条链表节点超过了8但是总的节点数没达到64的话,就扩容table表(按2倍扩容)
案例:
重写HashSet的hashcode方法,让其返回一个固定在100,也就是说所有节点的hashcode都是100,那么你添加12个元素由于它们的hashcode相同所有都会被添加到同一条链上。
用Debug启动,一开始table表长度为16,
当你添加完第8个元素后,由于单条链表节点超过了8但是总的节点数没达到64,所以扩容table表为32,但是这8个元素由于hashcode一模一样导致即使扩容后它们还是在同一条链上;
当你添加完第9个元素后,由于单条链表节点超过了8但是总的节点数没达到64,所以扩容table表为64,但是这9个元素由于hashcode一模一样导致即使扩容后它们还是在同一条链上;
当你添加完第10个元素后,由于单条链表节点超过了8而且总的节点数达到64,所以该链表变为红黑树结构
具体实现:
1.回顾HashMap 的四种构造方法
新建一个HashMap 有四种构造方法
1.new HashMap(int initialCapacity, float loadFactor);
2.new HashMap(int initialCapacity);
3.new HashMap(Map);
4.new HashMap();
前三种扩容阈值和负载因子均不为空
,第四种负载因子loadFactor不空但扩容阈值threshold为空
2.扩容的具体过程:
第一步,先计算newCap与newThr
①oldCap > 0 说明扩容前的哈希表大于0,表示hashMap散列表已经初始化过,是正常扩容。那就继续判断:
如果扩容之前的table数组已经大小达到最大容量后,则不扩容,且设置扩容条件threshold为Integer.MAX_VALUE(一个无穷大的数);
如果扩容之前的table数组没有达到最大扩容,那就扩容翻倍,如何扩容翻倍呢?用位运算,让oldCap左移一位就实现了翻倍(newCap = oldCap << 1),同时newThr = oldThr << 1也就是说下次再次触发扩容的条件也翻倍了。
总结:oldCap > 0 ,要么不扩容,要扩容的话newCap = oldCap << 1而且newThr = oldThr << 1
补充:
上面分析了oldCap > 0 大于0的情况,
而如果oldCap = 0说明hashMap中的散列表是null,散列表是null我们还要判断扩容阈值是不是空的。
②oldCap = 0时,如果oldThr > 0,那么设置newCap设置为oldThr的大小,设置newThr的大小为(nweCap)乘以(负载因子loadFactor)
补充:什么时候就会出现oldThr > 0但是oldCap = 0的情况呢?
由于oldThr=threshold,oldThr > 0说明threshold > 0,在使用前三种构造HashMap的构造方法构造出的HashMap就是一个扩容阈值threshold > 0的HashMap,构造完HashMap还没有初始化所以就出现了oldCap = 0且oldThr > 0的情况了
③oldCap = 0时,如果oldThr = 0,那么newCap设置为默认的数组大小(16)
,newThr设置为“(16*0.75=12)”
补充:什么时候就会出现oldCap = 0而且oldThr = 0的情况呢?
在使用第四种构造HashMap的构造方法构造出的HashMap就是一个扩容阈值threshold为空的HashMap,构造完HashMap还没有初始化所以就出现了oldCap > = 0且oldThr = 0的情况了
第二步:进行扩容
新索引要么与原索引相同要么就是原索引加上原数组的长度得到新索引。
先创建一个更大更长的哈希表,
如果扩容前的哈希表不为空,首先根据index找到对应的桶,
①桶里面如果有数据,那就判断桶的下一位是否为空,如果为空那说明桶里面就它一个元素,那就使用寻址算法找到该元素在新的哈希表的index,然后设置进去;然后把旧的哈希表里面的桶元素置空(让JVM去回收)。
②桶里面如果有数据,桶的下一位不为空而且桶里面的数据已经树化,那就按照树的方式去处理;
③桶里面如果有数据,桶的下一位不为空而且桶里面的数据还是链表,那就开始寻址了:
因为table是2倍扩容,所以只需要看当前元素的hash值与扩容前数组的长度做"与运算",结果为0,那么还是原来的index;否则index = index + oldCap;
(下面会解释这句话)
我们知道15号桶的hash值的后五位要么是1 1111,要么是0 1111
假如它的hash值的后五位是1 1111,扩容前table数组的长度是16(2进制为1 0000),二者做与运算结果要么是0要么是1,显然此时为1,那么它现在的索引就是15+16=31;
假如它的hash值的后五位是0 1111,扩容前table数组的长度是16(2进制为1 0000),二者做与运算结果要么是0要么是1,显然此时为0,那么它现在的索引还是15;
2.table数组初始容量为16,那么元素达到12个就会扩容,12是什么意思?它是指table数组的桶的个数是12呢还是说全部元素是12呢?
我们来看源码
++modCount;
if (++size > threshold)
resize();
加入一个元素的时候,不管是加载table表的某一个位置还是说table表的某一条链表上,都会执行size++
就是下面这段代码,先加的7个A在一条链表上面,然后加7个B到另一条链上,那么,我们在加到第5个B的时候,链表就会扩容了,这说明当总元素达到12个的时候就发生了扩容,所以你千万不要错误的以为是table表被占用了12个
for(int i = 1; i <= 7; i++) {
//在table的某一条链表上添加了 7个A对象
hashSet.add(new A(i));//
}
for(int i = 1; i <= 7; i++) {
//在table的另外一条链表上添加了 7个B对象
hashSet.add(new B(i));//
}
2.HashSet如何检查重复
当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。
三、HashMap相关
3.说说List,Set,Map三者的区别?
List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。
Map(用 Key 来搜索的专家): 使用键值对(kye-value)存储;Key 是无序的、不可重复的;value 是无序的、可重复的,每个键最多映射到一个值。
3.HashMap与HashTable的区别?
- 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过synchronized 修饰。
- 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。
- 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
- 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
3.HashMap和HashSet的区别
HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
3.比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
HashSet 是 Set 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;
LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历;
TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。
3.ConcurrentHashMap和Hashtable的区别?
- 底层数据结构:
JDK1.7 的ConcurrentHashMap
底层采用分段的数组+链表
实现,JDK1.8 采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑二叉树
。Hashtable
和JDK1.8
之前的 HashMap 的底层数据结构类似都是采用数组+链表
的形式。 - 实现线程安全的方式(重要):
①在 JDK1.7 的时候,ConcurrentHashMap
(分段锁) 对整个桶数组进行了分割分段
(Segment),然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 到了 JDK1.8 的时候已经摒弃了分割分段
的概念,而是直接用Node 数组+链表+红黑树
的数据结构来实现,并发控制使用synchronized 和 CAS
来操作。synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
3.ConcurrentHashMap线程安全的底层具体实现
①在 JDK1.7 的时候,ConcurrentHashMap
(分段锁) 对整个桶数组进行了分割分段
(Segment),然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 到了 JDK1.8 的时候已经摒弃了分割分段
的概念,而是直接用 Node 数组+链表+红黑树
的数据结构来实现,并发控制使用 synchronized 和 CAS
来操作。synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
3.HashMap 默认的初始化长度是多少?
在JDK中默认长度是16,并且默认长度和扩容后的长度都必须是 2 的幂。
3.谈谈对HashMap 构造方法中初始容量、加载因子的理解
初始容量代表了哈希表中桶的初始数量,即 Entry< K,V>[] table 数组的初始长度。
加载因子是哈希表在其容量自动增加之前可以达到多满的一种饱和度百分比,其衡量了一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
3.HashMap底层原理:
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就按照相应的方法插入。
JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
3.Hashmap如何添加一个元素
HashMap添加一个元素时,
第一步,先执行hash(Object key),把key转为对应的hashcode
第二步,执行putVal(hashcode, key, value)方法,具体如下:
先根据数组长度和hashcode执行一次路由运算 得到index,根据index找node数组中的元素,
情况①:node数组中该索引下没有元素,那太好了,我直接把k-v键值对扔进去
情况②:node数组中该索引下有元素,而且很巧的是当前桶位中的元素的key与你要插入的元素的key完全