面试问题

项目经历相关

  1. 项目为什么不是安卓相关但是后面转为 Kotlin 开发?
    因为我开发的是一个游戏模组,必须使用 Java8 进行开发,但是我觉得 Java8 的一些语法比较繁琐,所以后面选择了使用 Kotlin 进行项目开发,我选择 Kotlin 主要是看中了它的语法。
  2. 你编写富文本编辑器时,你认为难点主要在哪里?
    主要是业务逻辑很麻烦,对页面的操作很复杂。
  3. 你为什么要做这个富文本编辑器?
    因为我想在我的博客中添加这一个功能,虽然有现有的第三方库可以使用,但是我想借此机会挑战一下自己,尝试一下能否自己开发出来一个富文本编辑器。
  4. 你的博客页面是你自己写的还是开源的?
    是部分开源的。一部分内容是主题的,然后我是在主题的基础上进行的修改。

技术知识

网络

  1. 我在浏览器上输入一个网址,它大概会经历一个怎么样的流程?
    当时这个问题回答的不怎么好,这里直接写答案吧。
    1. 如果输入的是域名的话,首先需要从 DNS 服务器解析以得到服务器的 IP 地址;
    2. 拿到 IP 地址后会与服务器建立 TCP 连接,期间需要进行三次握手(如果懂的话可以展开说说
    3. 客户端发送请求
    4. 服务器处理请求并返回响应
    5. 浏览器解析 HTML、CSS 和 JS 文件并渲染和执行
  2. 为什么有了 MAC 地址后还需要 IP 地址?
    这里我是说的 IP 地址是动态的方便使用和进行管理。
    面试官的意思是各个厂商的 MAC 地址分配方法不太一样,没有统一的具体标准,然后如果使用 MAC 去查找设备的话很难确定去哪里找。
  3. 如何优化网页加载速度?
    首先最根本的是优化服务器,比较常用的手段就是使用 CDN。CDN 可以把客户端对服务器的请求分摊到对客户端最优的节点(理想情况下),从而加快文件的拉取速度。
    其次就是对网站的资源进行压缩,常见的文件中一般 JS 和 CSS 文件压缩掉的空间是比较多的。
    然后还可以使用客户端缓存来进行优化,我是使用 ServiceWorker 进行的缓存控制,对于 GET 请求可以把满足要求的文件在拉取后缓存到本地,下一次访问时就不需要向服务器发送请求了。
    然后面试官又问了一下缓存的具体细节,比如缓存的时候我是怎么确定文件类型的。这里我是感觉面试官不太了解 ServiceWorker 是如何实现缓存控制的,当时就简单说了一下使用 Cache API,应该具体说一下 ServiceWorker 的运行方式的。
  4. Keepalive 是什么?
    当时脑子宕机了没答上来。
    keepalive 可以在连接空闲时使双方保持连接而不关闭,从而使得下一次发起请求时无需重新建立连接。优点是重复利用了连接从而提高了性能且资源占用较小,缺点是如果请求已经结束,keepalive 会导致连接不能及时关闭。

Java

  1. 数据结构中链表和数组的区别是什么?
    太简单了就不赘述了,面试官后面还问了 Java 中 ArrayList 扩容的实现方法。
  2. ArrayList的泛型在底层是怎么实现的?
    Java 会对泛型进行擦除,代码运行时很多地方是无法获取泛型信息的。ArrayList内部实际上是存储了一个Object数组,在用户从其中读取数据的时候,会再强转回泛型对应的类型。
  3. DFS 和 BFS 在数据结构上的差别是什么?
    这里没太理解面试官想问什么,所以就简单说了下。后面面试官又问了下二叉树的层序遍历(又学到个新名词)、前序遍历、后序遍历、中序遍历应该使用哪种搜索方法,因为当时脑子转不过来了中序遍历没说出来,又问了我一下三个遍历方法具体是怎么遍历的。
  4. 线程和进程的区别是什么?
    线程是调度和执行的基本单位,进程是拥有资源的基本单位,前者成本相对小一些,一个进程中可以有多个线程。
    后面面试官又追问了线程和进程在内存上的关系,线程崩溃的时候进程是否会崩溃,没答出来。
    每个进程都有自己独立的内存空间,线程的内存是在进程中分配的,每个线程都有自己独立的程序计数器、栈等空间。当一个线程崩溃时,由于内存和进程是共享的,很容易导致内存出现不一致的情况,从而导致崩溃。当线程崩溃时,为了保护整个进程的稳定性和安全性,操作系统往往会选择终止整个进程。
  5. 如果一个线程在写一个数据,另一个线程在读会发生什么?
    如果两个线程直接没有做同步处理的话,那么发生什么都有可能(比如读取到旧数据、新数据甚至是没有完全修改完毕的数据)。
    如果进行了同步操作,那么一般都可以正常工作。
    Java 中最简单的方法就是加锁,这样可以保证在写数据的时候只有一个线程在访问这个变量,同时可以允许多个线程进行读取操作。另外还可以使用自旋操作,这样可以在不加锁的情况下避免上面提到的问题。
    后面面试官又问了 Javavolatile关键字的作用,这个属于是我上面没有仔细说自旋操作了,当时应该一口气直接全说完的。
    每个线程都有一个缓存空间,对变量的读写都在缓存空间中进行,volatile可以强制每次写操作后都将值同步到主存中,读操作前先从主存中同步值,从而保证每个线程都能看到变量的最新的值。同时volatile还可以禁用指令重排序(当时没想起来这个名词,不过解释了一下面试官还是知道我在说啥的),可以避免操作提前或延后执行,从而避免一些问题。(这里应该详细说为“禁止将对变量读写操作后的代码重排序到读写前,也禁止将对变量读写前的操作重排序到读写后”,当时没转过来。)
  6. 我现在写了一个 Hello World,Java 中是如何执行这个文件的?
    JVM 会从硬盘中加载(字节码)文件,进行解析、验证、加载等操作,完成类加载后就可以执行main函数了。
  7. 假如我自己写了一个String类,能否让其在 JDK 的String之前被加载?
    这里我是说 JVM 不允许用户编写javajavax包内的类,所以肯定是没办法自己编写一个String的,然后面试官问我 JVM 具体是怎么阻止这个操作的,我是忘了这个操作具体是在哪个类加载器中实现的了,就提了一嘴双亲委派模型。看了下录像发现当时面试官是引导我往引导类加载器上走了但是我没注意……
    实际上这个操作是不可行的,因为String会在虚拟机启动时使用引导类加载器进行加载,无论如何用户的代码也不会在引导类之前被加载。

git

  1. git 的 merge 和 rebase 有什么区别?
    没答上来
    合并操作是将两个分支的提交从最近公共提交后方开始合并为一个新的提交,然后将这个提交添加到当前分支中;而变基则是从两个分支最近公共提交的后方,将当前分支的提交删除,随后创建和原提交相同的提交并添加到另一个分支的最新的提交的后方,然后将另一个分支的提交移动过来。两者的区别是合并操作会保留原有的提交的时间顺序和结构,而变基则会打乱时间顺序,并完全抹除掉原有的结构,所以只建议在本地分支上使用变基操作。

SQL

  1. SQL 中group by是什么意思?
    group by是按照指定的列对数据进行分组。
  2. SQL 中sumcount的区别是什么?
    sum用于求和,count用于统计数量。(或许当时应该提一下count会忽略值为空的行。)

JS

  1. 你学一门语言估计需要多久?
    如果只是学习语法的话几天时间就可以。
  2. JS 是单线程还是多线程的?
    通常是单线程的,但是也有办法创建新的线程。
  3. JS 如何实现后台队列?
    不会。后面面试官紧接着又问了setTimeout,不太清楚具体是想问什么。
  4. JS 任务队列了解吗?
    不了解。
  5. JS 是解释执行吗?
    是。后面我又提到了解释器也有即时编译器,可以把代码编译成机器码执行。但是面试官又跟着问了一些解释器的东西,不了解。
  6. 你的博客为什么使用原生 JS 而不使用 VUE 等框架?
    因为我是基于主题进行二次开发,主题是使用原生 JS 进行开发的。

开放性问题

  1. 现在有一个网站,但是发现用户在点击一个连接后到页面完全显示之间流失了很多,你认为可能是因为什么?如何排查?
    这里我是说首先考虑是网站加载速度过慢,然后面试官又问我在团队中如何证明是这个原因,我说在不同设备不同网络下做测试,但是面试官的意思好像是只能在公司网络测试?这个问题聊了挺多的,先不做总结了。
  2. 现在已经确定是因为网络错误导致的,下一步你会干什么?
    我说的是利用一些网站提供的分布式测量的服务进行测试,确定是服务器的问题还是其它什么。剩下的不知道了。
  3. 实习相关
    省略

算法

  说是考算法,结果就是让用 Java 写了一个单例:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {

/** @noinspection InstantiationOfUtilityClass*/
private static final Main INSTANCE = new Main();

public static Main getInstance() {
return INSTANCE;
}

private Main() {}

}

  但是面试官说这个代码是线程不安全的,当时给我整不自信了,后来仔细想了想确实是线程安全的,有实例就是用static语意实现线程安全的懒加载的。

  我猜面试官是想让我写 DSL 那种线程安全懒加载的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {

private static volatile Main INSTANCE;

public static Main getInstance() {
if (INSTANCE != null) return INSTANCE;
synchronized (Main) {
if (INSTANCE == null)
//noinspection InstantiationOfUtilityClass
INSTANCE = new Main();
}
return INSTANCE;
}

}

补充

TCP 握手

  1. 客户端向服务端发送一个 SYN 包,请求建立连接
  2. 服务器回复一个 SYN + ACK 包
  3. 客户端回复一个 ACK 包

  这就是 TCP 三次握手的过程,那为什么不是两次握手而是三次呢?

  假设以下场景:客户端向服务端发送了 SYN 包,但由于某个网络中间节点暂时堵塞导致服务器未收到 SYN 包,客户端超时后就会重新发送 SYN 包。然后两个 SYN 包先后到达服务器。

  此时服务器就会认为产生了两个连接,而客户端会认为只建立了一个,就浪费了服务端的资源。所以添加了第三次握手,以此要求客户端确认连接建立,防止浪费服务端资源。