来源 | 经授权转自(ID:)
作者|Guide
今天分享的是一位华中师范大学同学分享的饿了么 Java 一面面经,主要是 Java 基础考察和对项目(烂大街的黑马点评)的一些拷打,比较简单。
1、Java 基础有哪些核心模块
这里简单对我觉得 Java 基础比较核心的模块做一下总结:
2、异常顶层是什么,有哪些接口实现类
Java 异常类层次结构图概览:
Java 异常类层次结构图
在 Java 中,所有的异常都有一个共同的祖先java.lang包中的类。类有两个重要的子类:
3、集合顶层是什么,各个接口实现类有哪些
Java 集合框架架构图如下图所示:
Java 集合框架概览
List接口实现类:
Set接口实现类:
Map接口实现类:
Queue接口实现类:
4、 结构JDK1.8 之前
JDK1.8 之前底层是数组和链表结合在一起使用也就是链表散列。 通过 key 的经过扰动函数处理过后得到 hash 值,然后通过(n - 1) & hash判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 的hash方法。使用hash方法也就是扰动函数是为了防止一些实现比较差的()方法 换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8 的 hash 方法源码:
JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^:按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对比一下 JDK1.7 的 的 hash 方法源码.
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓“拉链法”就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
jdk1.8 之前的内部结构-.8 之后
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
jdk1.8之后的内部结构-
、 以及 JDK1.8 之后的 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
我们来结合源码分析一下链表到红黑树的转换。
1、方法中执行链表转红黑树的判断逻辑。
链表的长度大于 8 的时候,就执行(转换红黑树)的逻辑。
// 遍历链表
for (int binCount = 0; ; ++binCount) {
// 遍历到链表最后一个节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果链表元素个数大于等于TREEIFY_THRESHOLD(8)
if (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;
}
2、方法中判断是否真的转换为红黑树。
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
// 判断当前数组的长度是否小于 64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 如果当前数组的长度小于 64,那么会选择先进行数组扩容
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,那么会选择先进行数组扩容,而不是转换为红黑树。
5、IO 流有了解吗
IO 即Input/,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
6、字节流和字符流有什么区别?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
个人认为主要有两点原因:
7、我看你用了 Redis,为什么用它,说出你的理由?
下面我们主要从“高性能”和“高并发”这两点来回答这个问题。
1、高性能
假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢?那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
2、高并发
一般像 MySQL 这类的数据库的 QPS 大概都在 2k~4k 左右(4 核 8g,单机) ,但是使用 Redis 缓存之后很容易达到 5w+,甚至最高能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。
QPS(Query Per ):服务器每秒可以执行的查询次数;
由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。
8、Redis 和 MySQL 的流量承受能力是多少?
上一个问题实际已经提到了这个问题的答案。
9、Redis 数据结构用过吗,有哪些?底层是什么?
Redis 中比较常见的数据类型有下面这些:
Redis 5 种基本数据类型其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、(双向链表)、Dict(哈希表/字典)、(跳跃表)、(整数集合)、(压缩列表)、(快速列表)。
Redis 5 种基本数据类型对应的底层数据结构实现如下表所示:
t
SDS
//
Dict、
Dict、
、
Redis 3.2 之前,List 底层实现是 或者 。Redis 3.2 之后,引入了 和 的结合 ,List 的底层实现变为 。从 Redis 7.0 开始, 被 取代。
除了上面提到的之外,还有一些其他的比如、(位域)。
关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看 Redis 官方文档对 Redis 数据类型的介绍(地址:)和我写的这篇文章:。
10、看你项目用了 MQ,为什么用它?
通常来说,使用消息队列能为我们的系统带来下面三点好处:
通过异步处理提高系统性能(减少响应所需时间)
削峰/限流
降低系统耦合性。
如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。
11、MQ 的解耦举个具体的场景
使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小java异或,这样系统的可扩展性无疑更好一些。还是直接上图吧:
解耦
生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合,这显然也提高了系统的扩展性。
消息队列使用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。从上图可以看到消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计。
例如,我们商城系统分为用户、订单、财务、仓储、消息通知、物流、风控等多个服务。用户在完成下单后,需要调用财务(扣款)、仓储(库存管理)、物流(发货)、消息通知(通知用户发货)、风控(风险评估)等服务。使用消息队列后,下单操作和后续的扣款、发货、通知等操作就解耦了,下单完成发送一个消息到消息队列,需要用到的地方去订阅这个消息进行消息即可。
12、怎么创建线程?
一般来说,创建线程有很多种方式,例如继承类、实现接口、实现接口、使用线程池、使用类等等。
不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。
严格来说,Java 就只有一种方式可以创建线程java异或,那就是通过new ().start()创建。不管是哪种方式,最终还是依赖于new ().start()。
关于这个问题的详细分析可以查看这篇文章:。
13、线程池的参数有哪些?最大线程数和核心线程数有什么区别?
个最重要的参数:
其他常见参数 :
下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》):
线程池各个参数的关系
当任务队列未满时,最多可以同时运行的线程数量就是核心线程数。任务队列中存放的任务满了之后,最多可以同时运行的线程数量就是最大线程数。
1、
2、
3、
4、
5、