Java
作业帮 Java ⾯试
我整理了作业帮 25 届开发岗位的校招薪资,情况如下:
22k * 15 = 33w ,base 北京
21k * 15 = 31.5w ,base 武汉
20k * 15 = 30w ,base 武汉
18k * 15 = 27w ,base 武汉
17k * 15 = 25.5w ,base 武汉在武汉拿到 25-30w 年薪还是⾮常的不错的,通常⼆线城市是⼀线城市薪资的 8 折,假设在⼆线城
市是 20k ⽉薪的话,等同于在⼀线是 24-25k 的薪资。
在秋招的时候,也有训练营同学拿到作业帮 offer ,在武汉开了 27w 年薪,还是在⽼家,其实算不
错的 offer ,不过今年互联⽹⼤⼚薪资都通常⽐较⾼,动不动都是 40w 年薪的,对⽐⼀下就会觉得
27w 年薪不多,所以同学先是签了保底,后续会继续冲更好的互联⽹公司。
这次来看看 作业帮Java 后端的校招⾯经,这场⾯上共⾯试了 60 分钟,并且也 是有算法⼿撕环节。
考察的知识点不算多,主要是 Java 和项⽬相关的知识。看到好⼏位同学的作业帮⾯经都是⼿撕快
排,⾯作业帮的同学⼀定要在⾯试之前好好 练习快速排序算法

Java
为什么 选择Java ,其优势在哪⼉?
我的项⽬是 web 应⽤场景,Java 在这⽅⾯⽣态⾮常成熟了,有很多开源框架可⽤,⽐如
springboot 、spring 、mybatis 等等 ,除此之外, Java 语⾔还有下⾯这些优势:
跨平台性:Java 通过 Java 虚拟机(JVM )来实现跨平台。Java 源代码被编译成字节码(.class⽂件),字节码可以在任何 安装了 JVM 的操作系统上运⾏。例如,⼀段 Java 程序在 Windows
系统上编译⽣成字节码后,⽆需修改就可以在 Linux 系统或者 macOS 系统的 JVM 上运⾏。
⾯向对象特性:Java 是⼀种纯粹的⾯向对象编程语⾔,⽀持封装、继承和多态等 OOP 特性。
封装允许将数据和操作数据的⽅法封装在⼀个类中,隐藏内部实现细节,提⾼代码的安全性和可维护性。继承使得⼦类可以继承⽗类的属性和⽅法,实现代码的复⽤。多态则允许不同的⼦
类对象对相同的⽅法调⽤做出不同的响应,增强了程序的灵活性。
内存管理:Java 有⾃动的垃圾 回收机制。GC 会⾃动检测和回收不再被使⽤的对象所占⽤的内存
空间,开发者不需要⼿动释放内存。例如,当⼀个对象没有任何 引⽤指向它时,GC 会在适当的
时候回收该对象占⽤的内存。
对⾯向对象的理解?
⾯向对象是⼀种编程范式,它将现实世界中的事物抽象为对象,对象具有属性(称为字段或属性)
和⾏为(称为⽅法)。⾯向对象编程的设计 思想是以对象为中 ⼼,通过对象之间的交互 来完成程序
的功能,具有灵活性和可 扩展性,通过封装和继承可以更好地应对需求变化 。
Java ⾯向对象的三⼤特性包括:封装、继承、多态:
封装:封装是指将对 象的属性(数据)和⾏为(⽅法)结合在⼀起,对外隐藏对象的内部细
节,仅通过对象提供的接⼝与外界交互 。封装的⽬的是增强安全性和简化编程,使得对象更加
独⽴。
继承:继承是⼀种可以使 得⼦类⾃动共享⽗类数据结构和⽅法的机制。它是代码复⽤的重要⼿
段,通过继承可以建⽴类与类之间的层次关系,使得结构更 加清晰。
多态:多态是指允许不同类的对象对同⼀消息作出响应。即同⼀个接⼝,使⽤不同的实例⽽执
⾏不同操作。多态性 可以分为编译时多态(重载)和运⾏时多态(重写)。它使得程序具有良好
的灵活性和扩展性。
继承和多态有什么 区别?
继承主要是⼀种类与类之间的关系,是代码复⽤的⼀种机制。它允许创建⼀个新的类(⼦类),
这个⼦类可以继承⽗类的属性(成员变量)和⽅法。⼦类是对⽗类的⼀种扩展,它可以在⽗类
的基础上添加新的属性和⽅法,也可以修改⽗类中原有的⽅法。例如,有⼀个 “动物” 类作为⽗
类,它有属性 “体重” 和⽅法 “进⻝”。然后 “猫” 类作为⼦类继承 “动物” 类,除了继承 “体重” 属
性和 “进⻝” ⽅法外,还可以添加新的属性如 “⽑⾊”,新的⽅法如 “抓⽼⿏”。
多态是强调的是同⼀个⾏为(⽅法)在不同对象中有不同的实现⽅式。多态是通过⽅法重写
(在继承关系中)或者接⼝实现来体现的。例如,有⼀个 “交通⼯具” 接⼝,定义了 “移动” ⽅
法。“汽⻋” 类和 “⾃⾏⻋” 类都实现了这个接⼝,它们对 “移动” ⽅法有不同的实现,汽⻋是通过
发动机驱动⻋轮移动,⾃⾏⻋是通过脚蹬使⻋轮转 动来移动。
Java 的垃圾 回收机制?
Java 的垃圾 回收负责 回收那些不 再被程序使⽤的对象所占⽤的内存空间,使程序员不需要⼿动释
放内存,从⽽避免了因⼿动内存管理不当⽽导致的内存泄漏和悬空指针等问题,常⻅的垃圾 回收
算法如下:
标记-清除算法:标记-清除算法分为“标记”和“清除”两个 阶段,⾸先通过可达性分析,标记出所
有需要回收的对象,然后统⼀回收所有被标记的对象。标记-清除算法有两个 缺陷,⼀个是效率
问题,标记和清除的过程效率都不⾼,另外⼀个就是,清除结束后会造成⼤量的碎⽚空间。有
可能会造成在申请⼤块内存的时候因为没有⾜够的连续空间导致再次 GC 。
复制算法:为了 解决碎⽚空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两
块,每次 申请内存时都使⽤其中的⼀块,当内存不够时,将这⼀块内存中所有存活的复制到 另
⼀块上。然后将然后再把已使⽤的内存整个清理掉。复制算法解决了空间碎⽚的问题。但是也
带来了新的问题。因为每次 在申请内存时,都只能使⽤⼀半的内存空间。内存利⽤率严重不
⾜。
标记-整理算法:复制算法在 GC 之后存活对象较少的情况下效率⽐较⾼,但如果存活对象⽐较
多时,会执⾏较多的复制操作,效率就会下降。⽽⽼年代的对象在 GC 之后的存活率就⽐较
⾼,所以就有⼈提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记
过程⼀致,但标记之后不会直接清理。⽽是将所有存活对象都移动到 内存的⼀端。移动结束后
直接清理掉剩余部分。
分代回收算法:分代收集是将内存划分 成了新⽣代和⽼年代。分配的依据是对象的⽣存周期,
或者说经历过的 GC 次数。对象创建时,⼀般在新⽣代申请内存,当经历⼀次 GC 之后如果对还
存活,那么对象的年龄 +1 。当年龄超过⼀定值(默认是 15 ,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进⼊⽼年代。Java 的垃圾 ⾃动回收是怎么判断的?
在Java 中,判断对象是否为垃圾 (即不再被使⽤,可以被垃圾 回收器回收)主要依据两种主流的垃圾回收算法来实现:引⽤计数法和可 达性分析算法。
引⽤计数法(Reference Counting )
原理:为每个对象分配⼀个引⽤计数器, 每当有⼀个地⽅引⽤它时,计数器加1;当引 ⽤失效
时,计数器减1。当计数器为0时,表⽰对象不再被任何 变量引⽤,可以被回收。
缺点:不能解决循环引⽤的问题,即两个 对象相互引⽤,但不再被其他任何 对象引⽤,这时引
⽤计数器不会为0,导致对象⽆法被回收。
可达性分析算法(Reachability Analysis )

Java 虚拟机主要采⽤此算法来判断对象是否为垃圾 。
原理:从⼀组称为GC Roots (垃圾 收集根)的对象出发,向下追溯它们引⽤的对象,以及这些
对象引⽤的其他对象,以此类推。如果⼀个对象到GC Roots 没有任何 引⽤链相连(即从GC Roots 到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。GC Roots 对象包
括:虚拟机栈(栈帧中的本地变量表)中引⽤的对象、⽅法区中类静态属性引⽤的对象、本地
⽅法栈中JNI (Java Native Interface )引⽤的对象、活跃线程的引⽤等。
Java 的内存管理是怎么做的?
根据 JVM8 规范,JVM 运⾏时内存共分为虚拟机栈、堆、元空间、程序计数器、本地⽅法栈五个
部分。还有⼀部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。

JVM 的内存结构主要分为以下⼏个部分:
元空间:元空间的本质和永久代类似,都是对JVM 规范中⽅法区的实现。不过元空间与永久代
之间最⼤的区别在于:元空间并不在虚拟机中,⽽是使⽤本地内存。
Java 虚拟机栈:每个线程有⼀个私有的栈,随着线程的创建⽽创建。栈⾥⾯存着的是⼀种叫“栈
帧”的东西,每个⽅法会创建⼀个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引
⽤)、操作数栈、⽅法出⼝等信息。栈的⼤⼩可以固定也可以动态扩展。
本地⽅法栈:与虚拟机栈类似,区别是虚拟机栈执⾏Java ⽅法,本地⽅法站执⾏native ⽅法。在
虚拟机规范中对本地⽅法栈中⽅法使⽤的语⾔、使⽤⽅法与数据结构没有强制规定,因此虚拟
机可以⾃由实现它。
程序计数器:程序计数器可以看成是当前线程所执 ⾏的字节码的⾏号指⽰器。在任何 ⼀个确定
的时刻,⼀个处理器( 对于多内核来说是⼀个内核)都只会执⾏⼀条线程中的指令。因此,为了线程切换后能恢复到正确的执⾏位置,每条线程都需要⼀个独⽴的程序计数器, 我们称这类
内存区域为“线程私有”内存。
堆内存:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和
数组都在堆上进⾏分配。这部分空间可通过 GC 进⾏回收。当申请不到空间时会抛出
OutOfMemoryError 。堆是JVM 内存占⽤最⼤,管理最复杂的⼀个区域。其唯⼀的⽤途就是存放
对象实例:所有的对象实例及数组都在对上进⾏分配。jdk1.8 后,字符串常量池从永久代中剥离
出来,存放在队中。
直接内存:直接内存并不是虚拟机运⾏时数据区的⼀部分,也不 是Java 虚拟机规范中定义的内
存区域。在JDK1.4 中新加⼊了NIO(New Input/Output) 类,引⼊了⼀种基于通道 (Channel) 与缓冲区(Buffer )的I/O ⽅式,它可以使 ⽤native 函数库直接分配堆外内存,然后通脱⼀个存储在
Java 堆中的DirectByteBuffer 对象作为这块内存的引⽤进⾏操作。这样能在⼀些场景中显著提⾼
性能,因为避免了在Java 堆和Native 堆中来回复制数据。
全局静态变量与临 时变量在内存的什么 地⽅存储?
局部变量:⽅法中的局部变量存在于栈内存。每当程序调⽤⼀个⽅法时,系统都会为该⽅法建
⽴⼀个⽅法栈,其所在⽅法中声明的变量就放在⽅法栈中,当⽅法结束系统会释放⽅法栈,其
对应在该⽅法中声明的变量随着栈的销毁⽽结束,这就局 部变量只能在⽅法中有效的原因。
成员变量:对象实例的引⽤存储在栈内存中,对象实例存储在堆内存中。所以,对象中声明的
成员变量存储在堆中。(成员变量不会随着某个⽅法执⾏结束⽽销毁)
全局静态变量:全局静态变量存储在⽅法区(在 Java 8 之后为元空间)。⽅法区主要⽤于存储已
被虚拟机加载的类信息,包括类的版本、字段、⽅法、接⼝等,以及常量、静态变量、即时编
译器编译后的代码等。全局静态变量的⽣命周 期是从类被加载开始,⼀直到类被卸载结束。只
要类被加载到 Java 虚拟机中,静态变量就会⼀直存在于内存中,它可以被类的所有实例共享访
问。
int 和long 是多少位,多少字节的?
int 类型是 32 位(bit ),占 4 个字节(byte ),int 是有符号整数 类型,其取值范围是从 -2^31
到 2^31-1 。例如,在⼀个简单的计数器程序中,如果使⽤ int 类型来存储计数值,它可以表
⽰的最⼤正数是 2,147,483,647 。如果计数值超过这 个范围,就会发⽣溢出,导致结果不符合预
期。
long 类型是 64 位,占 8 个字节, long 类型也是有符号整数 类型,它的取值范围是从 -2^63
到 2^63 -1 ,在处理较⼤的整数数 值时,果 int 类型的取值范围不够,就需要使⽤ long 类
型。例如,在⼀个⽂件传 输程序中,⽂件的⼤⼩可能会很⼤,使⽤ int 类型可能⽆法准确表
⽰,⽽ long 类型就可以很好地处理这种情况。
其他
⼿机和电脑是分别 是多少位的?
现在的⼿机和电脑基本都是 64 位的了。
你知道⼿机的架构 吗,有哪些?
不太了解
Redis
为什么 ⽤redis ,解决了什么 问题?
主要是因为 Redis 具备「⾼性能」和「⾼并发」两种特性。
1、Redis 具备⾼性能
假如⽤⼾第⼀次访问 MySQL 中的某些数据。这个过程会⽐较慢,因为是从硬盘上读取的。将该⽤
⼾访问的数据缓存在 Redis 中,这样下⼀次再访问这些数据的时候就可以直接从缓存中获取了,操
作 Redis 缓存就是直接操作内存,所以速度相当快 。

如果 MySQL 中的对应数据改变的之后,同步改变 Redis 缓存中相应的数据即可,不过这 ⾥会有
Redis 和 MySQL 双写⼀致性的问题。
2、 Redis 具备⾼并发
单台设备的 Redis 的 QPS (Query Per Second ,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,
Redis 单机的 QPS 能轻松破 10w ,⽽ MySQL 单机的 QPS 很难破 1w 。
所以,直接访问 Redis 能够承受的请求是远远 ⼤于直接访问 MySQL 的,所以我们可以考虑把数据
库中的部分数据转移到缓存中去,这样⽤⼾的⼀部分请求会直接到缓存这⾥⽽不⽤经过数据库。
与redis 对标的技术是什么 ?
还有Memcached ,它也是内存数据库,和 Redis ⼀样,它的数据存储在内存中,通过减少磁盘
I/O 操作来实现快速的数据读写。例如,当⼀个应⽤程序向 Memcached 请求⼀个数据时,它会根
据键来查 找对应的内存位置,如果找到就直接返回值。
Memcached 相⽐ Redis :
数据结构简单性:Memcached 只⽀持简单的键值对数据结构,不像 Redis 有丰富的数据结构
(如列表、集合、有序集合等)。这使得 Memcached 在⼀些只需要简单缓存功能的场景下更加
轻量级,但在复杂的数据操作场景下(如实现排⾏榜、消息队列等功能)不如 Redis 灵活。
持久化功能缺失:Memcached 没有内置的数据持 久化功能。这意味着⼀旦服务器重启或者出现
故障,存储在 Memcached 中的数据将会丢失。⽽ Redis 提供了多种持久化⽅式(如 RDB 和
AOF ),可以在⼀定程度上保证数据的安全性。
没有原⽣的集群模式:Redis 原⽣⽀持集群模式,Memcached 没有原⽣的集群模式,需要依靠
客⼾端来实现往集群中分⽚写⼊数据;
项⽬
看你项⽬上使⽤vue3 ,其中遇到过什么 问题吗?
做某某 项⽬的过程中遇到过什么 ⽐较⼤的问题,是怎么解决的?
⼿撕
快排
快速排序是⼀种基于分治思想的⾼效排序算法,它的基本思路如下:
第⼀步** 选择基准值:** 从待排序的数组中选择⼀个元素作为基准值。这个基准值的选择⽅式有
多种,常⻅的是选择数组的第⼀个元素、最后⼀个元素或者中间元素等。例如,我们可以简单
地选择数组的第⼀个元素作为基准值。
第⼆步划分 操作:通过⼀趟排序将待排序的数组分割 成两部分,使得左边部分的所有元素都⼩
于等于基准值,右边部分的所有元素都⼤于等于基准值。具体做法是设置两个 指针,⼀个从 数
组的左边开始向右 移动(通常称为 left 指针),⼀个从 数组的右边开始向左移动(通常称为
right 指针)。然后,从左到右找到第⼀个⼤于基准值的元素,从右到左找到第⼀个⼩于基准
值的元素,交换这两个 元素的位置。不断重复这个过程,直到 left 指针和 right 指针相遇,
此时将基准值与相遇位置的元素交换,这样就完成了⼀次划分 操作,确定了基准值在排序后数
组中的正确位置。
第三步递归排序⼦数组:对划分 后的左右两个 ⼦数组分别 重复上述的选择基准值和划分 操作,
也就是递归地调⽤快速排序算法,直到⼦数组的⻓度为 1 或者 0(表⽰已经有序),此时整个数
组就完成了排序。
以下是使⽤ Java 语⾔实现快速排序算法的代码:
public class QuickSort {
public static void quickSort (int [] arr , int low , int high ) {
if (low < high ) {
// 划分操作,获取基准值的最终位置索引
int pivotIndex = partition (arr , low , high );
// 对基准值左边的子数组进行递归排序
quickSort (arr , low , pivotIndex - 1);
// 对基准值右边的子数组进行递归排序
quickSort (arr , pivotIndex + 1, high );
}
}
private static int partition (int [] arr , int low , int high ) {
int pivot = arr [low ]; // 选择第一个元素作为基准值
int left = low + 1;
int right = high ;
while (true ) {
// 从左到右找到第一个大于基准值的元素
while (left <= right && arr [left ] <= pivot ) {
left ++ ;
}
// 从右到左找到第一个小于基准值的元素
while (left <= right && arr [right ] > pivot ) {
right --;
}
if (left > right ) {
// 交换两个元素的位置
swap(arr, left, right);
}
// 将基准值与相遇位置的元素交换
swap(arr, low, right);
return right;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {5, 3, 8, 6, 7, 2};
quickSort(arr, 0, arr.length - 1);
for (int num : arr) {
System.out.print(num + " ");
}
}
}在上述代码中:
quickSort ⽅法是快速排序的主⽅法,它接收⼀个整数数 组以及要排序的起始索引和结束索
引。⾸先判断起始索引是否⼩于结束索引,如果是,则进⾏划分 操作得到基准值的索引,然后
分别 对基准值左右两边的⼦数组进⾏递归调⽤ quickSort ⽅法。
partition ⽅法实现了划分 操作,按照前⾯描述的思路,通过双指针移动和元素交换来确定基
准值的正确位置,并返回这个位置的索引。
swap ⽅法⽤于交 换数组中两个 指定位置的元素,辅助 partition ⽅法完成元素的交换操作。
时间复杂度分析:
快速排序在平均情况下的时间复杂度是 O(nlogn ),但在最坏的情况下(例如数组本⾝已经有序
或者逆序,每次 选择的基准值恰好是最⼤或者最⼩的元素),时间复杂度会退化为 O(n^2 )。
快速排序的空间复杂度取决于递归调⽤的栈深度,在平均情况下,递归栈深度为 O(logn ),所
以空间复杂度为 O(logn )。在最坏情况下,递归栈深度为 O(n ),空间复杂度则变为O(n )。