1.算法复杂度
2. ArrayList相关
2.1 底层原理
-
底层数据结构:ArrayList底层是用动态的数组实现的
-
初始容量:ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
-
扩容逻辑:ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
-
添加逻辑
-
确保数组已使用长度(size)加1之后足够存下下一个数据
-
计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
-
确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
-
返回添加成功布尔值。
-
2.2 面试题-ArrayList list=new ArrayList(10)中的list扩容几次
该语句只是声明和实例了一个 ArrayList,指定了容量为 10,未扩容
2.3 面试题-如何实现数组和List之间的转换
参考回答:
-
数组转List ,使用JDK中java.util.Arrays工具类的asList方法
-
List转数组,使用List的toArray方法。无参toArray方法返回 Object数组,传入初始化长度的数组对象,返回该对象数组
再问
-
用Arrays.asList转List后,如果修改了数组内容,list受影响吗
Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址,并且list不能进行增删操作。如果需要不受影响,则把list传给ArrayList构建一个新的list。
-
List用toArray转数组后,如果修改了List内容,数组受影响吗
list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响
3. 面试题-ArrayList和LinkedList的区别是什么?
-
底层数据结构
-
ArrayList 是动态数组的数据结构实现
-
LinkedList 是双向链表的数据结构实现
-
-
操作数据效率
- ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公式】, LinkedList不支持下标查询
- 查找(未知索引): ArrayList需要遍历,链表也需要链表,时间复杂度都是O(n)
- 新增和删除
- ArrayList添加和删除时间复杂度是O(n)
- LinkedList添加和删除都是O(1),前提是先找到这个元素
-
内存空间占用
-
ArrayList底层是数组,内存连续,节省内存
-
LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
-
-
线程安全
-
ArrayList和LinkedList都不是线程安全的
-
如果需要保证线程安全,有两种方案:
- 在方法内使用,局部变量则是线程安全的
- 使用线程安全的ArrayList和LinkedList
-
4. 红黑树相关
5. 说一下HashMap的实现原理?
HashMap的数据结构: 底层使用hash表数据结构,即数组和链表或红黑树
-
当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
-
存储时,如果出现hash值相同的key,此时有两种情况。
-
如果key相同,则覆盖原始值;
-
如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
-
-
获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
面试官追问:HashMap的jdk1.7和jdk1.8有什么区别
-
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
-
jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为8) 时并且数组长度大于等于64时,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表
-
JDK1.7是头插法,JDK1.8是尾插法
6. HashMap的put方法的具体流程
-
HashMap是懒惰加载,在创建对象时并没有初始化数组
-
在无参的构造函数中,设置了默认的加载因子是0.75
添加数据流程图
请注意:
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
具体的源码:
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//判断数组是否未初始化if ((tab = table) == null || (n = tab.length) == 0)//如果未初始化,调用resize方法 进行初始化n = (tab = resize()).length;//通过 & 运算求出该数据(key)的数组下标并判断该下标位置是否有数据if ((p = tab[i = (n - 1) & hash]) == null)//如果没有,直接将数据放在该下标位置tab[i] = newNode(hash, key, value, null);//该数组下标有数据的情况else {Node<K,V> e; K k;//判断该位置数据的key和新来的数据是否一样if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//如果一样,证明为修改操作,该节点的数据赋值给e,后边会用到e = p;//判断是不是红黑树else if (p instanceof TreeNode)//如果是红黑树的话,进行红黑树的操作e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//新数据和当前数组既不相同,也不是红黑树节点,证明是链表else {//遍历链表for (int binCount = 0; ; ++binCount) {//判断next节点,如果为空的话,证明遍历到链表尾部了if ((e = p.next) == null) {//把新值放入链表尾部p.next = newNode(hash, key, value, null);//因为新插入了一条数据,所以判断链表长度是不是大于等于8if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//如果是,进行转换红黑树操作treeifyBin(tab, hash);break;}//判断链表当中有数据相同的值,如果一样,证明为修改操作if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;//把下一个节点赋值为当前节点p = e;}}//判断e是否为空(e值为修改操作存放原数据的变量)if (e != null) { // existing mapping for key//不为空的话证明是修改操作,取出老值V oldValue = e.value;//一定会执行 onlyIfAbsent传进来的是falseif (!onlyIfAbsent || oldValue == null)//将新值赋值当前节点e.value = value;afterNodeAccess(e);//返回老值return oldValue;}}//计数器,计算当前节点的修改次数++modCount;//当前数组中的数据数量如果大于扩容阈值if (++size > threshold)//进行扩容操作resize();//空方法afterNodeInsertion(evict);//添加操作时 返回空值return null;
}
7. HashMap寻址算法
在putVal方法中,有一个hash(key)方法,这个方法就是来去计算key的hash值的,看下面的代码
首先获取key的hashCode值,然后右移16位 异或运算 原来的hashCode值,主要作用就是使原来的hash值更加均匀,减少hash冲突
有了hash值之后,就很方便的去计算当前key的在数组中存储的下标,看下面的代码:
(n-1)&hash : 得到数组中的索引,代替取模,性能更好,数组长度必须是2的n次幂
关于hash值的其他面试题:为何HashMap的数组长度一定是2的次幂?
-
计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
-
扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
8. hashmap在1.7情况下的多线程死循环问题
jdk7的的数据结构是:数组+链表
在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环
-
变量e指向的是需要迁移的对象
-
变量next指向的是下一个需要迁移的对象
-
Jdk1.7中的链表采用的头插法
-
在数据迁移的过程中并没有新的对象产生,只是改变了对象的引用
产生死循环的过程:
线程1和线程2的变量e和next都引用了这个两个节点
线程2扩容后,由于头插法,链表顺序颠倒,但是线程1的临时变量e和next还引用了这两个节点
第一次循环
由于线程2迁移的时候,已经把B的next执行了A
第二次循环
第三次循环
参考回答:
在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环
比如说,现在有两个线程
线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入
线程二:也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
线程一:继续执行的时候就会出现死循环的问题。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,
所以B->A->B,形成循环。
当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。
9. hashmap是线程安全的吗?
面试官:好的,hashmap是线程安全的吗?
候选人:不是线程安全的
面试官:那我们想要使用线程安全的map该怎么做呢?
候选人:我们可以采用ConcurrentHashMap进行使用,它是一个线程安全的HashMap
面试官:那你能聊一下ConcurrentHashMap的原理吗?
候选人:好的,请参考《多线程相关面试题》中的ConcurrentHashMap部分的讲解
10. HashSet与HashMap的区别?
HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法. 依靠HashMap来存储元素值,(利用hashMap的key键进行存储), 而value值默认为Object对象. 所以HashSet也不允许出现重复值, 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true.
11. HashTable与HashMap的区别
第一,数据结构不一样,hashtable是数组+链表,hashmap在1.8之后改为了数组+链表+红黑树
第二,hashtable存储数据的时候都不能为null,而hashmap是可以的
第三,hash算法不同,hashtable是用本地修饰的hashcode值,而hashmap经常了二次hash
第四,扩容方式不同,hashtable是当前容量翻倍+1,hashmap是当前容量翻倍
第五,hashtable是线程安全的,操作数据的时候加了锁synchronized,hashmap不是线程安全的,效率更高一些
在实际开中不建议使用HashTable,在多线程环境下可以使用ConcurrentHashMap类