校招面经(三)

77 阅读7分钟

HashMap

HashMap和HashTable的区别?

1、HashMap是线程不安全的,HashTable是线程安全的,但是不建议用HashTable,可以使用ConcurrentHashMap

2、HashMap比HashTable效率更高

3、HashMap可以存储null的key和value,但是key和value只能有一个为空。

hashTable不支持该功能

4、HashMap默认初始大小是16,每次扩容会变成原来的两倍。

HashTable默认的初始大小是11,每次扩容变成原来的2n+1倍。

给定容量初始值,HashMap会将其扩充为2的幂次方大小,HashTable则就使用给定的容量大小

5、jdk1.8之后,HashMap在解决Hash冲突发生了较大的变化。当链表长度大于8且数组长度大于64时,会将其转化成为红黑树,以减少搜索时间。而HashTable就没有。

HashMap底层实现原理?

JDK1.8 之前 HashMap 底层是数组链表结合在一起使用也就是链表散列。 HashMap 通过 keyhashCode 经过扰动函数处理过后得到hash值然后通过 (n-1) &(按位与) hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素要存入的元素hash 值以及 key 是否相同,如果相同的 话,直接覆盖,不相同就通过拉链法解决冲突

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了 防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一 个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8) (将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不 是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间

HashMap类主要是用来处理具有键值对特征的数据,随着JDK版本的更新,JDK1.8对HashMap底层也进行了优化。

HashMap是基于哈希表对Map接口的实现,HashMap具有较快的访问速度,但是便利顺序是不确定的。

HashMap提供所有可选的映射操作,并允许使用null值和null键。

 new HashMap<>().put(null, null)

HashMap并非线程安全,当存在多个线程同时写入HaspMap时,可能会导致数据的不一致

loadfactor称为负载因子,默认值为0.75,loaerFactor=size/capacity

threshold表示所能容纳的键值对的临界值

threshold计算公式为 数组长度*负载因子

size是threshold中实际存在的键值对数量

modCount字段用来记录HashMap内部结构发生变化的次数

HashMap的默认容量INITIAL_CAPACITY为16,一般第一次扩容会扩容到64,之后时2倍次幂。

在jdk1.8中,HashMap采用了数组+链表+红黑树的存储结构

数组部分称为哈希桶。当链表长度大于等于8且键值对数量大于64时,链表数据将以红黑树的形式存储,当长度降到6时,转成链表

链表的时间复杂度为O(n),红黑树的时间复杂度为O(log n)

每个Node节点用来存储用来定位数据索引位置的hash值,V值,K键以及指向链表下一个节点的Node<K,V>next节点组成。

Node时HashMap的内部类,是想了Map.Entry接口,本质是一个键值对。

当向hashmap中插入数据时,首先要确定在哈希桶数组中的位置

HashMap的扩容流程?

1、HashMap 的扩容其实就是数组的扩容,因为数组占用的事连续内存空间,要想扩容只能新创建一个数组。

2、HashMap中就是建一个2倍数组大小的数组,然后遍历老数组的每一个位置,如果这个位置上是一个链表,就把这个链表上的元素转移到新数组上去。

3、在转移的过程中就需要遍历链表, jdk7 就是简单的遍历链表上的每一个元素然后按照每个元素的HashCode结合新数组的长度重新计算得出一个下标,新得出的下标可能和之前不一样,所以就有可能会导致某个链表变短,这也就达到了扩容的目的,缩短链表长度,提高了查询效率。

4、在jdk8 中,因为涉及到红黑树,所以比较复杂。 jdk8 会用一个双向链表来维护红黑树中的元素,所以在转移某个位置的元素时,如果判断这个位置是红黑树,就会遍历这个位置的 双向链表,遍历这个双向链表来统计哪些元素在扩容之后还在原位置,哪些在新位置。遍历完双向链表之后,就会有两个子链表,一个放在原下标,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则就判断这两个子链表的长度,如果超过了8就转换为红黑树放到的对应的位置,否则就转换成单向链表放到对应位置。

5、元素转移完之后,把新数组赋值给HashMap的table属性,老数组则被回收。

HashMap扩容机制?

HashMap 的默认容量是16,加载因子是0.75f,阈值为16 * 0.75 = 12 ,当元素数量超过阈值时会触发扩容。每次扩容的容量都是之前容量的2倍 ,容量上限为(2^31) − 1。

JDK1.7 中不带参数的构造方法,使用的是默认容量、默认负载因子、默认阈值构建 HashMap内部数组是空数组。如果有参数,根据参数确定。第一次 put 时会初始化数组,其容量变为不小于指定容量的 2 的幂数,然后根据负载因子确定阈值。如果不是第一次扩容,则新容量就是原来的 2 倍,阈值为新容量 x 负载因子。

JDK1.8不带参的构造方法默认内部数组为 null ,即没有实例化。第一次调用 put ,就会初始化扩容,长度为 1 6 。如果有参数 指定容量 )),根据指定的正整数找到不小于指定容量的 2 的幂数,将这个数设置赋值给阈值。第一次调用 put ,会 将阈值赋值给容量,然后让阈值等于容量 x 负载因子(这也说明了,不管是不是我们手动设置容量,只要超过阈值都会扩容)。如果不是第一次扩容,则容量变为原来的 2 倍,阈值变为原来的 2 倍,负载因子不变。

如何确定Node的存储位置?

HashMap首先调用hashCode()方法,获取键Key的hashCode值h(101),然后对其进行高位运算

将h右移16位以取得h的高16位,与原h的低16位进行异或运算结果为(101)

最后将得到的h值与(table.length - 1)进行与运算获得该对象的保留位以计算下表

jdk1.8中hashMap.put()采用尾插法,jdk1.7采用头插法

MyBatis

#{}和${}的区别是什么?

{}是 Properties 文件中的变量占位符,它可以用于标签属性值和sql内部,属于静态文本替换 ,比如会被静态替换为com.mysql.jdbc.Driver。 #{}是 sql 的参数占位符,Mybatis 会将sql中的#{}替换为?号,在 sql 执行前会使用PreparedStatement 的参数设置方法,按序给sql的?号占位符设置参数值,比如 ps.setInt(0,parameterValue)parameterValue),#{item.name} 的 取值方式为使用反射从参数对象中获取item对象的name属性值 ,相当于param.getItem().getName() 。

#{}可以有效防止SQL注入**。

MyBatis是如何进行分页的?分页插件的原理是什么?

Mybatis使用RowBounds对象进行分页,它是针对ResultSet 结果集执行内存分页而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

MyBatis动态SQL是做什么的?都有哪些动态SQL?简述动态SQL的执行原理?

Mybatis动态sql可以让我们在xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。

Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind。

其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能