中软
无笔试,问的问题都很基础,但是自己都没答好,脑壳是懵的。一刚开始答得不对,面试官也不多做解释,越答越没信心。
1、equals 和 hashCode 的区别。
我答成了 String 的 == 与 equals 的区别。
... ...
变形问题:Set/HashMap 是如何去重的?
Object 的 equals 方法默认是两个对象的引用的比较,和 == 是一样的,意思就是指向同一内存,地址则相等,否则不相等。
public boolean equals(Object obj) {
return (this == obj);
}
Object 的 hashCode 是一个本地方法。
public native int hashCode();
This is typically implemented by converting the internal address of the object into an
integer, but this implementation technique is not required by the Java™ programming language.
这句话看得我有点懵,Java 中的默认 hashCode 到底是不是地址转换出来的,有很多人把默认 toString 打印出来的 @ 后面当做地址。还是说默认是地址,但不是必须这样实现?我觉得应该是后者,默认就是地址,所以大家都不一样,但是允许重写。不管一不一样,Hash 取模就会有冲突。
总结: equals 比较两个对象是否相等,默认是比较地址,可以重写,应该具有对称性、反射性、传递性、一致性。比如 String 重写后是比较值。
hashCode 主要用于 hash 计算,不同对象的 hashCode 可以相同。要求 equals 为 true 的两个对象的 hashCode 一定相等。通常重写 equals 方法也要重写 hashCode 方法。
有人就喜欢钻牛角尖,说不一定要重写 hashCode 。是的,是不一定,因为 hashCode 主要用于 set/map 集合判重逻辑,只要你保证不用于 set/map 集合,不重写也不会有问题。
对于变形题目,Set/Map 怎么判重的:HashSet 的内部是实现 HashMap。
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
重新计算 hash 值:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
把 key 的 hashCode 高16位低16位进行异或,目的是为了保证高16位也参与运算,减少冲突。
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
这是1.8版本的,hash 取模看对应桶是否为空,为空直接加入;不为空从第一个开始比较 hash 值是否相同,再比较地址是否相同或者 key != null 比较 equals。
1.8 加入了红黑树,所以这里有个节点类型的判断。
2、Spring MVC 的执行过程。
虽然源码自己也看了很多遍,还是说的不利索。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 根据请求的 url 找 Handler,返回一个 HandlerExecutionChain。
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
// 没找到,发出异常
noHandlerFound(processedRequest, response);
return;
}
// 为 handler 找 adapter 适配器 。
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 循环执行 pre 拦截器
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 真正调用 handler,返回 modelAndView,如果使用 @RestController 注解,返回空.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 执行 post 拦截器
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 渲染 view,返回结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
简单的说:找 handler、找 adaptor、执行 pre 拦截器、调用 handler 返回 modelAndView、执行 post 拦截器、渲染 view。
(1) 客户端通过url发送请求
(2-3) DispatcherServlet 接收到请求,通过 HandlerMapping 到对应的handler,并将 URI 映射的控制器 controller 组成处理器执行链返回给 Dispatcher Servlet。
(4) 找 HandlerAdapter
(5-7)由找到的 HandlerAdapter 调用相应的 Handler 进行处理并返回 ModelAndView 给 DispatcherServlet
(8-9)DispatcherServlet 将获取的 ModelAndView 传递给视图解析器解析,返回具体 View
(10)DispatcherServlet 对 View 进行渲染视图(即将模型数据填充至视图中)
(11)将页面响应给用户
组件:
DispatcherServlet:前端控制器
用户请求到达前端控制器,它就相当于mvc模式中的c,DispatcherServlet 是整个流程控制的中心,由它调用其它组件处理用户的请求,DispatcherServlet 的存在降低了组件之间的耦合性。HandlerMapping:处理器映射器
HandlerMapping 负责根据用户请求 URL 找到 Handler 即处理器,Spring MVC 提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。Handler:处理器
Handler 是继 DispatcherServlet 前端控制器的后端控制器,在 DispatcherServlet的控制下Handler对具体的用户请求进行处理。由于 Handler 涉及到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发 Handler。HandlerAdapter:处理器适配器
通过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。ViewResolver:视图解析器
View Resolver 负责将处理结果生成 View 视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果通过页面展示给用户。View:视图
springmvc框架提供了很多的View视图类型的支持,包括:jstlView、freemarkerView、pdfView等。我们最常用的视图就是jsp。
一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由程序员根据业务需求开发具体的页面。
3、HashMap 先扩容还是先转红黑树。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
初始化桶大小16,转红黑树有两个条件,一个是冲突的 key 超过8个,并且桶的数量大于等于64。0.75是负载因子,当前有0.75的桶都有值了,就会进行扩容,扩大为当前的2倍,再进行 reHash。
所以对于这道题,判断冲突的key大于等于8个时,尝试转红黑树,如果桶的数量小于64,则扩容;否则转红黑树。
for (int binCount = 0; ; ++binCount) {
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
binCount 从 0 开始的,所以要减 1。
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode hd = null, tl = null;
do {
TreeNode p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
这是转红黑树的方法,先判断桶的大小,小于 64 就扩容。
4、@Controller 与 @RestController
@RestController 注解相当于@ResponseBody + @Controller合在一起的作用。
如果只是使用 @RestController 注解 Controller,则 Controller 中的方法无法返回 jsp 页面,或者 html,在上面 Spring MVC 调用过程中 adapter 调用 handler 的时候 modelAndView 就会返回 null,直接把 return 的内容放到 response 里了。
如果需要返回到指定页面,则需要用 @Controller配合视图解析器InternalResourceViewResolver才行。
如果需要返回JSON,XML或自定义mediaType内容到页面,则需要在对应的方法上加上@ResponseBody注解。
5、Spring Cloud 有哪些组件?
Eureka
Ribbon
Feign
Config
Hystrix
Hystrix DashBoard
Bus
Data Stream
6、JVM 内存模型
不要答成运行时数据区域了。
内存模型
下面这个是运行时数据区域:
虚拟机
7、ConcurrentHashMap
ConcurrentHashMap
ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。
1.8 采用 CAS。
8、Mysql 查询优化
EXPLAIN
9、InnoDB 与 MyISAM 的区别。
对 MyISAM 不是很了解。
主要区别是 MyISAM 不支持事务。
MyISAM
10、volatile 有什么作用,是不是原子性?
保证可见性: 修饰变量使其修改之后能够立马被其他线程看见。原理是 volatile 修饰的变量读取时必须从主存中获取,修改后要立马写入主存,从而保证了修改之后能够立马被其他线程看见。
保证有序性:防止编译器重排。会加入一个内存屏障,防止后面的执行排到前面。
不保证原子性:单一的读/写是具备原子性的,复合操作,比如 i++ 不具备原子性。