知乎面试问题
一面
- 算法题
两个有序数组,输出第k小的数字
思想时:分别折半查找,每个数组记录自己的left,right索引,进行查找。 - hashmap
- 源码结构
- hashmap在扩容时空间创建+新旧节点的对应关系+如果扩容过程中查找该怎么查找**(分析如下)**
- 究竟哪些节点需要申请新的存储空间?哪些不需要呢?
- 需要新申请空间的节点:newTab,即为新table数组,申请空间个数为32、64、128等。
- 不需要新申请空间的节点:所有链表或者红黑树上的节点;
- 源码:resize()方法
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
- 新旧索引有什么关系?
- 因为我们使用的增长策略是2的整数次幂方式,table的容量在更改时,同一元素在table中的索引要么不变,要么移动到相对原位置而言距离2的整数次幂的一个位置。
- 旧链表中的节点如何对应到新的table索引中?
思想:对这个旧链表进行遍历,从而先生成两个链表;然后一个放在原来索引位置j处,一个放在[j+oldCap]处; - 那么生成两个链表的策略是怎样的呢?
if ((e.hash & oldCap) == 0) 条件成立的节点是放在原来索引位置j处的链表;
否则,放在j+oldCap索引处的链表;
- 查找参数包括:hash(key)和key。那么接下来如何进行查找?
- 先获得first节点的索引,其索引策略是:
first = tab[(n - 1) & hash] - 接下来如果first节点为红黑树节点,则按照红黑树查找方式查找;
- 如果first节点为链表节点,则按照链表查找方式进行查找;
- 那么问题来了,如果此时hashMap在扩容,那么tab究竟是新tab数组还是旧tab数组?
通过resize()方法的源码可以发现,有这样一步操作:
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
并且这步操作的后面,才会发生生成新的两个链表的操作。所以如果hashMap扩容过程中发生查找操作,则在table没有被重新赋值前就在旧table中查找;否则在新table中查找;但在新table的查找中,可能新的2个链表还没有生成导致存在的元素查找结果也是null;不过本来hashMap也是线程不安全的,返回并发导致的错误结果也能理解。
- 先获得first节点的索引,其索引策略是:
- 究竟哪些节点需要申请新的存储空间?哪些不需要呢?
- jvm内存模型+gc算法
- mysql索引实现
- B树和B+树的对比
- LRU cache的实现方式?自己说了链表方式,不知道还有什么方式
- LinkedHashMap的实现方式
- 一个包含3个指针节点的双向链表。节点指针为before,next,after。分别指向:
双向链表的前一个节点;
hash值冲突的逻辑链表;
双向链表的后一个节点;
参考:https://blog.csdn.net/caoxiaohong1005/article/details/79909083 - 时间复杂度
- get时,最好O(1),最差O(N);
- put时,最好O(1),最差O(N);
- 空间复杂度
O(N)
- 一个包含3个指针节点的双向链表。节点指针为before,next,after。分别指向:
- 单链表+hashmap
- 底层使用有头指针的单链表,同时用hashmap对每个key维护一个前驱节点;
- 时间复杂度
- get时,首先从hashmap中找到对应的节点,然后移到头节点,并更改hashmap的映射关系;
- set时,如果cache未满,则直接插入到头节点前面,并更改hashmap的映射关系;
- set时,如果cache满了,则将重用链表尾节点,然后挪到表头,并更改hashmap的映射关系;
- 空间复杂度
O(N)
- LinkedHashMap的实现方式
- 网络编程socket,自己说不会也就没有再问
- TCP/IP 的三次握手和四次挥手过程
二面
-
讲解实习项目架构和自己实现的模块功能
-
springAOP的实现原理
- java动态代理可以对类实现吗?不知道,只知道是对interface的实现
- java动态原理
-
使用到的class和interface包括:Proxy类和invocationHandler接口。
-
需要自己写的class和interface
1)被代理对象的interface
2)被代理对象的interfaceImpl
3)动态代理的InvocationHandler实现类 -
JVM生成动态代理类
传入参数:定义代理类的类加载器,代理类要实现的接口列表,invocationHandler实例import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * @Author: cxh * @CreateTime: 18/9/23 00:48 * @ProjectName: JavaBaseTest */ public class AopTest { public static void main(String[] args) { //定义被代理对象 Subject subjectImpl = new SubjectImpl(); //定义InvocationHandler接口实例 InvocationHandler invocationHandler = new InvocationHandlerImpl(subjectImpl); //生成动态代理实例(定义代理类的类加载器,代理类要实现的接口列表,invocationHandler实例) Subject subject = (Subject) Proxy.newProxyInstance(invocationHandler.getClass().getClassLoader(), subjectImpl.getClass().getInterfaces(), invocationHandler); System.out.println("subject.getClass().getName():" + subject.getClass().getName()); subject.before(); } } /** *被代理对象的接口 */ interface Subject { void before(); void after(); } /** * 被代理对象 */ class SubjectImpl implements Subject { @Override public void before() { System.out.println("this is method of before()."); } @Override public void after() { System.out.println("this is method of after()."); } } /** * 动态代理,必须实现InvocationHandler接口 */ class InvocationHandlerImpl implements InvocationHandler { Subject subject; /** * 构造函数参数必须包括:被代理对象,这样调用动态代理的invoke()方法时,才会调用被代理对象的方法. */ InvocationHandlerImpl(Subject subject) { this.subject = subject; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before invoke()"); System.out.println("the method is :" + method); method.invoke(subject, args); System.out.println("after invoke()"); return null; } } ----------- 输出结果: subject.getClass().getName():$Proxy0 before invoke() the method is :public abstract void Subject.before() this is method of before(). after invoke()
-
-
springIOC的实现原理 :三级缓存+提前曝光+java的对象引用原理
- Spring循环依赖的理论依据其实是Java基于引用传递,当我们获取到对象的引用时,对象的field或者或属性是可以延后设置的。Spring单例对象的初始化其实可以分为三步:
- createBeanInstance, 实例化,实际上就是调用对应的构造方法构造对象,此时只是调用了构造方法,spring xml中指定的property并没有进行populate。
- populateBean,填充属性,这步对spring xml中指定的property进行populate
- initializeBean,调用spring xml中指定的init方法,或者AfterPropertiesSet方法
会发生循环依赖的步骤集中在第一步和第二步。
- spring在解决循环依赖中使用了三级缓存
- 单例对象的cache:
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256); - 单例对象工厂的cache:
private final Map <String, ObjectFactory?> singletonFactories = new HashMap<String, ObjectFactory<?>>(16); - 提前曝光的单例对象的cache:
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
- 单例对象的cache:
- singleton初始化过程图片:
- 显然:两个循环依赖都是通过构造器函数进行依赖的,则spring无法解决此种依赖。因为提前暴露出来的原始bean需要执行完实例构造器函数。
- Spring循环依赖的理论依据其实是Java基于引用传递,当我们获取到对象的引用时,对象的field或者或属性是可以延后设置的。Spring单例对象的初始化其实可以分为三步:
-
interface 和 abstractclass 的区分和适用场景
- 区别
- 变量类型:抽象类中的成员变量可以是各种类型的,但是接口中的只能是public static final类型的。
- 单根继承:一个类只能继承一个抽象类,但可以实现多个接口;
- 实现关键字:接口需要实现implements;而抽象类需要继承extends;
- 强调内容:interface强调特定功能的实现;abstract class强调所属关系;
- 的
- 使用场景
- 接口:
- 如果想实现多继承,则使用接口;
- 用于定义mix-in类型,表示类具有某特定性能,如Serializable接口,没有任何方法,只是用于表示实现该接口的类可以序列化,当然序列话方法要自己写代码去实现。
- 抽象类:
- 如果基本功能不断改变,则用抽象类。因为不断改变功能时,用接口的话,就要改变所有实现了该接口的类。
- 某些场合下,只靠纯粹的接口不能满足类与类之间的协调,还必需类中表示状态的变量来区别不同的关系。abstract的中介作用可以很好地满足这一点.
- 接口:
- 区别
京东面试总结
一面
- 算法题
- 实习项目
- 数据库为什么要使用线程池
- 原因:各种池其实原因都一样
- 实现方式:
预先创建一定数量的线程,当有请求达到时,线程池分配一个线程提供服务,请求结束后,该线程又去服务其他请求。 - 优势
1)避免了线程和内存对象的频繁创建和释放
2)降低了服务端的并发度
3)减少了上下文切换和资源的竞争,提高资源利用效率
- 实现方式:
- MySQL的线程池改进过程
1)One-Connection-Per-Thread
每一个数据库连接,Mysql-Server都会创建一个独立的线程服务,请求结束后,销毁线程。
2)基于1)的thread-cache,将线程缓存起来,以供下次使用,避免频繁创建和释放的问题,但是无法解决高连接数的问题。1)的方式随着连接数暴增,导致需要创建同样多的服务线程,高并发线程意味着高的内存消耗,更多的上下文切换(cpu cache命中率降低)以及更多的资源竞争,导致服务出现抖动。
3)Thread-Pool实现方式
1.线程处理的最小单位是statement(语句),一个线程可以处理多个连接的请求。
2.在保证充分利用硬件资源情况下(合理设置线程池大小),可以避免瞬间连接数暴增导致的服务器抖动。 - 线程池常配置的属性
1)user和password
2)initialPoolSize、minPoolSize、maxPoolSize
3)acquireIncrement :声明当连接池中连接耗尽时再一次新生成多少个连接,默认为3个
4)maxIdleTime :超过多长时间连接自动销毁,默认为0,即永远不会自动销毁
5)maxStatements :JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量
ps:但由于预缓存的statements属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。
如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭
6)idleConnectionTestPeriod :每xx秒检查所有连接池中的空闲连接 - 线程池和连接池
- 线程池
- 线程池实现在server端,通过创建一定数量的线程服务DB请求
- 线程池服务的最小单位是语句,即一个线程可以对应多个活跃的连接
- 可以将server端的服务线程数控制在一定的范围,减少了系统资源的竞争和线程上下文切换带来的消耗,同时也避免出现高连接数导致的高并发问题
- 连接池
1)连接池通常实现在Client端,是指应用(客户端)创建预先创建一定的连接,利用这些连接服务于客户端所有的DB请求
2)如果某一个时刻,空闲的连接数小于DB的请求数,则需要将请求排队,等待空闲连接处理。通过连接池可以复用连接,避免连接的频繁创建和释放,从而减少请求的平均响应时间,并且在请求繁忙时,通过请求排队,可以缓冲应用对DB的冲击
PS:连接池和线程池相辅相成,通过连接池可以减少连接的创建和释放,提高请求的平均响应时间,并能很好地控制一个应用的DB连接数,但无法控制整个应用集群的连接数规模,从而导致高连接数,通过线程池则可以很好地应对高连接数,保证server端能提供稳定的服务。
- 原因:各种池其实原因都一样
- 毕设+神经网络
- java实现文件的导入导出用到的类有哪些?
只记得InputStream,OutputStream. - java如何实现将一个Date按照某种日期的格式显示?
SimpleDateFormat可以实现
二面
-
讲实习项目
-
Spring的功能有哪些
自己从spring组成,和各个模块及常用模块的功能都讲了一遍 -
Spring事务的隔离级别
- Spring事务的隔离级别和MySQL的事务隔离级别的关系
- Spring使用的事务隔离级别必须是MySQL能支持的,故如果Spring设定的隔离级别MySQL不支持则此Spring的设定无效。
- 如果Spring和MySQL设定了不同的隔离级别,那么以Spring会在事务开始时,根据你程序中设置的隔离级别,调整数据库隔离级别与你的设置一致。
- Spring事务的隔离级别和MySQL的事务隔离级别的关系
-
Mybatis的二级缓存
- 一级缓存
- 生命周期:基于sqlSession
- 二级缓存
- 生命周期:基于application
- 作用范围:按照每个namepace一个缓存来存贮和维护,同一个namespace放到一个缓存对象中。
- cache使用的注意事项
- 只能在【只有单表操作】的表上使用缓存
不只是要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须全部在一个namespace下。 - 在可以保证查询远远大于insert,update,delete操作的情况下使用缓存
- 只能在【只有单表操作】的表上使用缓存
- 避免使用二级缓存
多表操作不管多表操作写到哪个namespace下,都会存在某个表不在这个namespace下的情况,这会导致查询使用缓存的时候结果就是错的。
- 一级缓存
-
MySQL的索引建立原则
自己先说了索引类型,不同引擎支持的索引类型,以及索引的使用规则 -
java中的锁怎么实现的
自己讲了volatile,synchronized的实现原理,以及从jdk1.6开始synchronized对锁的处理:偏向锁,轻量级锁,重量级锁。以及锁只能升级不能降级。 -
java反射都用过什么?