前言

最近收到了Java后端的面试,虽然编程上没有什么问题,但是八股文还是该记得记,这里找到了所谓常考察的Java基础知识,以作学习之用。

Java

基本类型、变量和类型转换

Java 有 8 种基本类型,整数比如byte、short、int、long,浮点数比如float、double,字符类型char以及布尔boolean。
除此之外,类、数组、接口等都是引用类型。例如:

1
2
String name = "Chen";
int[] scores = {90, 85, 92};

这里的String类和数组都是引用类型变量。引用意味着它指向这个对象,但修改它不会修改这个对象本身,而是修改它指向的对象。示例:

1
2
3
4
5
6
7
String a = "Chen";
String b = a;

// 修改对象
b = "Li";
System.out.println(a); // Chen
System.out.println(b); // Li

基本类型和包装类型

每个基本类型都有对应的包装类,例如:

1
2
3
4
5
int     → Integer
long → Long
double → Double
boolean → Boolean

包装类用于需要对象的场景,例如集合:

1
2
List<Integer> numbers = new ArrayList<>();
numbers.add(10);

由于Java 提供了自动装箱和拆箱,所以它们的用法本质上也是相同的。但注意Integer包装类的值可能会为null,拆箱给int时会报错。

Java 里的集合,可以先理解为“用来存放一组数据的容器”。它可以动态增加或删除元素;相比之下,数组创建后长度固定。

常见问题

1.Java 有哪八种基本类型?各占多少空间?
答:byte、short、int、long、float、double、char、boolean。前七种的存储宽度分别是 1、2、4、8、4、8、2 字节;boolean 的具体存储大小由 JVM 实现决定,不能简单说固定占 1 字节。
2.int 和 Integer 有什么区别?
答:int 是基本类型,不能为 null;Integer 是包装类型,可以为 null,也可用于泛型集合。两者之间可自动装箱、拆箱,但对 null 的 Integer 拆箱会抛 NullPointerException。
3.自动装箱和拆箱是什么?拆箱可能有什么问题?
答:自动装箱是基本类型转包装对象,拆箱是包装对象转基本类型。包装对象为 null 时拆箱会触发 NullPointerException;频繁装箱也会产生额外对象操作。
4.Java 是值传递还是引用传递?
答:Java 只有值传递。传对象时传递的是引用值的副本,因此方法可以通过它修改同一个对象,但重新给形参赋新对象不会改变调用方变量。
5.byte 转 int 与 double 转 int 有什么区别?
答:byte 转 int 通常是扩大范围的自动类型提升;double 转 int 是缩窄转换,需要显式强转,小数部分会被截去,也可能超出 int 范围。

String 为什么不可变

String 对象创建后,字符串内容不能原地修改。对字符串做拼接会得到新的字符串:

1
2
3
4
5
6
7
String text = new String("Java");
String other = text; // other 也引用原来的 "Java" 对象

text = new String("后端");

System.out.println(text); // 后端
System.out.println(other); // Java

这样设计便于安全共享、缓存哈希值,也让字符串作为键时更可靠。

集合

集合可以理解为 Java 提供的“装一组对象的容器”。ArrayList、HashMap、HashSet 常被放在一起问,因为它们分别解决不同问题:

1
2
3
4
5
│ ArrayList │ 一组元素 │ 有顺序、可重复、按下标访问快 │ 列表数据、查询结果       
│ LinkedList│ 一组元素 │ 有顺序、可重复;头尾插入和删除方便,按下标访问较慢 │ 双端队列、频繁头尾增删 │
│ Queue │ 一组元素 │ 通常按先进先出(FIFO)顺序处理;从队尾添加、从队头取出 │ 排队任务、消息处理 │
│ HashMap │ 键值对 │ 通过 key 查找,key 唯一 │ 用户 ID 到用户对象的映射
│ HashSet │ 一组元素 │ 元素不重复,通常不保证顺序 │ 去重、判断元素是否存在

ArrayList动态数组,底层使用数组保存元素。数组容量不够时,会创建更大的数组,再把旧元素复制过去。查询和末尾添加很快,但中间插入和删除需要移动后续元素。

不要在遍历过程中使用remove直接删除元素,可能引发并发修改异常,可以使用removeIf。

LinkedList双向链表,底层由一组节点组成,每个节点保存元素,并记录前一个和后一个节点。它实现了 List 和 Deque 接口,可以按列表使用,也可以当双端队列使用。头尾插入和删除通常较方便;但按下标查找需要从头或尾逐个经过节点,访问速度通常不如 ArrayList。

LinkedList在已定位到节点后,插入或删除节点不需要移动后续元素;但如果要先按下标找到位置,查找本身仍需要遍历。不要笼统认为它的插入、删除在所有场景下都比ArrayList快。

HashMap键值映射,根据 key 计算哈希值,所以 key 不能重复,根据哈希值找到数组中的一个位置;如果该位置有多个候选项,再继续比较 key;找到相等的 key 后返回对应 value。

对于hashmap来说,需要同时自定义equals() 和 hashCode(),因为元素的存放位置依靠key值的 hashCode()决定,否则可能出现明明内容相等,集合却查不到对应元素。

HashSet不重复元素集合,内部主要借助 HashMap 保存元素:集合元素作为 key,内部用一个固定标记对象作为 value。因此,HashSet 是否认为两个元素重复,也依赖 equals() 和 hashCode()。

适用于只关心元素是否存在、需要去重的用途。

常见问题

1.ArrayList 和 LinkedList 有什么区别?各适合什么场景?
答:ArrayList 基于动态数组,按下标访问快,尾部添加通常较快;LinkedList 基于双向链表,已定位节点后的增删方便,按下标查找较慢。常规列表通常优先考虑 ArrayList。
2.ArrayList 如何扩容?为什么中间插入、删除成本较高?
答:容量不足时会创建更大的底层数组并复制原元素;中间插入或删除后,后续元素通常需要移动,因此成本与元素数量相关,通常为 O(n)。
3.HashMap 如何处理哈希冲突?
答:不同 key 可能映射到同一个桶,HashMap 会在桶内继续比较 key;Java 8 中桶内主要使用链表,满足条件时可能转为红黑树。
4.自定义对象作为 key,为什么要正确实现 equals() 和 hashCode()?
答:hashCode 用于定位桶,equals 用于判断 key 是否相等。若 equals 判等的两个对象没有相同 hashCode,查找和去重可能不符合预期。
5.HashMap 中相同 key 再次 put 会发生什么?
答:原有键对应的 value 会被新 value 替换,通常不会新增一组键值对。
6.HashSet 如何判断元素重复?它是否保证顺序?
答:它主要依据 hashCode 定位,再用 equals 判断是否相等;HashSet 通常不保证迭代顺序。
7.遍历时删除元素有哪些安全做法?
答:可用 removeIf,或通过 Iterator 的 remove 方法删除;不要在增强 for 遍历时直接修改集合结构。

类型转换

范围较小的数值类型转为范围较大的类型,通常可以自动转换:

1
2
int a = 10;
long b = a;

反过来可能丢失数据,需要显式转换:

1
2
3
double price = 9.8;
int whole = (int) price; // 变成 9,小数部分丢失
// 也就是告诉Java我能够接受数据丢失,不显式转换可能报错
常见问题

1.什么是自动类型提升?什么情况下需要强制类型转换?
答:较小范围的数值类型参与运算时可能自动提升为较大类型;可能缩窄或丢失精度的转换通常需要显式强转。
2.double 转 int 会怎样处理小数?
答:小数部分直接截去,向 0 方向取整,例如 9.8 转为 9,-9.8 转为 -9。
3.类型强转会发生溢出吗?举一个例子。
答:会。目标类型表示范围不足时结果可能溢出,例如把超出 int 范围的 long 强转为 int,结果可能不再等于原值。
4.5 / 2 和 5.0 / 2 的结果分别是什么?
答:5 / 2 是整数除法,结果为 2;5.0 / 2 有浮点数参与,结果为 2.5。

运算符、条件和循环

这块比较常规,注意几个细节即可。

1
2
3
4
5
6
7
int age = 20;

if (age >= 18) {
System.out.println("成年");
} else {
System.out.println("未成年");
}

常见循环有 for、增强 for、while:

1
2
3
4
5
6
7
8
for (int i = 0; i < 3; i++) {
System.out.println(i);
}

int[] values = {1, 2, 3};
for (int value : values) {
System.out.println(value);
}

在循环中,break是结束当前循环;continue是跳过本轮循环,进入下一轮。
逻辑运算中,&& 和 || 有短路特性:左边已经能决定结果时,右边就不会执行。示例:

1
2
3
if (user != null && user.isActive()) {
// user 为 null 时,不会继续调用 isActive()
}
常见问题

1.break 和 continue 有什么区别?
答:break 结束当前循环;continue 跳过本轮剩余语句,开始下一轮循环。
2.&& 和 & 有什么区别?
答:布尔表达式中,&& 会短路,左侧已能决定结果时不计算右侧;& 会计算两侧。& 也可用于整数的按位与。
3.for、增强 for、while 各适合什么场景?
答:for 适合次数或索引明确的循环;增强 for 适合顺序遍历数组或集合;while 适合循环次数不确定、由条件控制的场景。
4.为什么 user != null && user.isActive() 可以避免空指针异常?
答:&& 有短路特性;user 为 null 时左侧为 false,右侧不会执行,因此不会调用 null 上的 isActive()。

方法与参数传递

方法由返回类型、方法名、参数列表和方法体组成:

1
2
3
public int add(int a, int b) {
return a + b;
}

void 表示不返回结果。参数的类型和数量共同决定方法的签名,Java 可以根据不同参数列表重载方法:

1
2
3
4
5
6
7
8
9
10
public int add(int a, int b) {
return a + b;
}

public double add(double a, double b) {
return a + b;
}
// 调用时,Java 会根据你传入的参数类型选择对应的方法:
add(1, 2); // 调用 int 版本
add(1.5, 2.5); // 调用 double 版本

Java 是值传递

传基本类型时,方法拿到的是数值副本:

1
2
3
4
5
6
7
static void change(int n) {
n = 100;
}

int x = 10;
change(x);
// x 仍然是 10

传对象时,方法拿到的是“引用值的副本”。它可以通过这个副本修改同一个对象的内容,但重新给这个副本赋值,不会改变调用者的引用:

1
2
3
4
5
6
7
static void rename(User user) {
user.setName("新名字"); // 修改对象内容,调用方能看到
}

static void replace(User user) {
user = new User(); // 只改变局部变量,调用方引用不变
}
常见问题

1.什么是方法重载?只改变返回类型能否构成重载?
答:同一类中方法名相同但参数列表不同称为重载;只改变返回类型不能构成重载,因为调用时无法仅凭返回类型区分方法。
2.Java 是值传递还是引用传递?对象参数为什么容易让人误以为是引用传递?
答:Java 是值传递。对象形参拿到的是引用值的副本,通过它修改对象状态会影响同一对象,但给形参重新赋值不会改变调用方引用。
3.rename(user) 和 replace(user) 分别会对调用者产生什么影响?
答:rename 通过形参修改原对象属性,调用者能看到变化;replace 只让局部形参指向新对象,调用者持有的引用不变。
4.可变参数是什么?它和数组参数有什么关系?
答:可变参数允许调用时传入零个或多个同类型实参,方法内部按数组处理;每个方法最多一个可变参数,且必须放在参数列表最后。

面向对象

面向对象的几个核心概念是类与对象、封装、继承、多态。

类和对象

类是对一类事物的描述,对象是根据类创建出来的具体实例:

1
2
3
4
5
6
7
8
9
10
11
class User {
String name;

void sayHello() {
System.out.println("你好,我是 " + name);
}
}

User user = new User();
user.name = "小陈";
user.sayHello();

封装

封装是把数据和操作数据的方法放在一起,并控制外部如何访问:

1
2
3
4
5
6
7
class User {
private String password;

public void setPassword(String password) {
this.password = password;
}
}

封装有助于保护数据、集中约束和降低耦合。

继承

继承表示子类可以复用父类中可继承的成员,并扩展自己的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
void eat() {
System.out.println("吃东西");
}
}

class Dog extends Animal {
void bark() {
System.out.println("汪");
}
}

Dog dog = new Dog();
dog.eat(); // 来自父类 Animal
dog.bark(); // Dog 自己定义的

Java 的类是单继承:一个类只能直接继承一个父类,但可以实现多个接口。

多态

多态常见形式是:父类或接口类型的变量,指向子类对象;调用被重写的方法时,运行时执行子类实现。对于上面那个例子:

1
2
3
Animal animal = new Dog(); // 这里实际创建的是 Dog 对象
animal.eat(); // 父类定义的功能,可以调用
// animal.bark(); // Animal 没有 bark(),编译不通过

父类类型屏蔽子类的具体差异,方便统一调用;假如子类重写父类方法后,运行时执行子类版本。子类独有功能则需要通过子类类型访问。
理解多态时重点记住:代码可以面向父类或接口编写,运行时由实际对象决定具体行为。 这有助于扩展实现,也常见于 Spring 的依赖注入。

重载与重写

重载(Overload):同一个类里方法名相同、参数列表不同;编译时根据参数选择方法。前面已经在方法与参数传递举例.
重写(Override):子类重新实现父类可重写的方法;运行时根据实际对象决定调用哪个实现。示例:

1
2
3
4
5
6
class Dog extends Animal {
@Override
void eat() {
System.out.println("狗在吃东西");
}
}

常见问题

1.封装、继承、多态分别解决什么问题?
答:封装控制数据访问并集中约束;继承复用并扩展父类能力;多态让调用方依赖父类或接口,以统一方式调用不同实现。
2.抽象类和接口有什么区别?
答:抽象类可保存实例状态、构造器和部分实现,类只能继承一个抽象类;接口主要定义行为契约,一个类可以实现多个接口。现代 Java 接口也可有默认方法和静态方法。
3.Java 为什么是单继承?一个类能实现多个接口吗?
答:Java 类只能直接继承一个父类,以避免多重实现继承带来的复杂性;一个类可以实现多个接口。
4.父类引用指向子类对象时,哪些成员能直接访问?
答:编译期按引用的父类类型检查可访问成员,因此只能直接访问父类声明的成员;被重写的实例方法运行时会执行子类实现。
5.重载和重写有什么区别?
答:重载是在同一作用域中方法名相同、参数列表不同,通常编译期确定调用;重写是子类重新实现父类可重写方法,运行时按对象实际类型分派。
static 方法可以被重写吗?
答:不能按实例方法的方式重写。子类声明同签名 static 方法属于隐藏,调用选择主要由引用的声明类型决定。

==、equals() 和 hashCode()

这是常见组合题:

  • 对基本类型使用 ==,比较数值是否相等;
  • 对引用类型使用 ==,比较是不是同一个对象;
  • equals() 用于比较对象内容是否相等,但具体含义取决于类有没有重写它。
    示例:
1
2
3
4
5
String a = new String("hello");
String b = new String("hello");

System.out.println(a == b); // false:两个不同对象
System.out.println(a.equals(b)); // true:字符串内容相同

Object 默认的 equals() 行为接近引用比较;String 重写了它,改成比较字符内容。
如果一个类重写了 equals(),通常也应该重写 hashCode()。因为 HashMap、HashSet 等哈希集合会先根据哈希值定位,再判断对象是否相等;两者规则不一致可能导致集合行为异常。

常见问题

1.== 和 equals() 有什么区别?
答:基本类型的 == 比较值;引用类型的 == 比较是否为同一对象。equals() 的含义由类定义,常用于比较逻辑内容。
2.Object.equals() 默认做什么?
答:默认比较两个引用是否指向同一个对象;子类可以重写它来定义内容相等。
3.为什么重写 equals() 时通常也要重写 hashCode()?
答:哈希集合先用 hashCode 定位,再用 equals 判断。按 equals 相等的对象必须有相同 hashCode,否则集合查找和去重可能异常。
4.两个对象 hashCode() 相同,能否说明它们相等?
答:不能。哈希冲突是允许的;相同 hashCode 还需要通过 equals 判断是否相等。
5.String a = “x” 和 new String(“x”) 的比较结果是什么?
答:若分别赋给变量,a.equals(b) 为 true;a == b 通常为 false,因为 new String 创建了新的字符串对象。

异常

异常处理

首先需要分开看,错误条件/错误事件,异常类,异常对象。
程序运行时可能会出错,比如加载文件时文件不存在,这时会读取失败。这些是客观情况,不依赖你写不写 Java 代码。文件确实可能不存在。
为了区分不同的错误类型,Java 定义了异常类型,可以先分成两类:

  • 受检异常:编译器要求处理或声明,例如 IOException;
  • 非受检异常:通常继承自 RuntimeException,编译器不要求显式声明,例如NullPointerException。
    遇到了不同的异常情况,我们创建一个新的异常对象用于描述这个错误事件。

Throwable、Error 和 Exception

Java 中可以被 throw 抛出的对象,必须属于 Throwable 体系。结构大致是:

1
2
3
4
Throwable
├── Error
└── Exception
└── RuntimeException

其中

  • Error:通常表示严重问题
  • Exception:程序运行中可能处理的问题,包括上述两种类型。

throw 和 throws 怎么配合

需要说明的是:

  • throws 是声明层面:写在方法签名上,告诉编译器和调用者:“这个方法可能会抛出这些异常。”
  • throw 是执行层面:写在方法体里,是真正的动作:“现在真的抛出一个异常对象。”
1
2
3
public void readFile() throws IOException {
throw new IOException("读取失败");
}

异常处理

有了捕捉和抛出异常后我们可以尝试访问并在遇到错误时释放资源。
1.finally 本身不会自动关闭任意资源。旧式写法可以在 finally 里显式关闭。
2.现在常用try-with-resources来关闭资源。

常见问题

1.受检异常和非受检异常有什么区别?
答:受检异常需在编译期捕获或声明,例如 IOException;非受检异常通常是 RuntimeException 及其子类,编译器不要求显式处理。
2.throw 和 throws 有什么区别?
答:throw 在方法体中实际抛出一个异常对象;throws 写在方法声明中,说明方法可能向调用方抛出的异常类型。
3.Exception 和 Error 有什么区别?
答:二者都继承 Throwable。Exception 表示应用可能处理的异常情况;Error 通常表示较严重的 JVM 或运行环境问题,应用一般不应尝试恢复。
4.finally 一定会执行吗?资源关闭推荐怎么做?
答:通常会执行,但进程退出、虚拟机崩溃等极端情况不能保证。文件等资源推荐用 try-with-resources 自动关闭。
5.什么是自定义异常?何时值得定义?
答:自定义异常是业务代码定义的异常类型;当需要清楚表达特定业务错误、让调用方按错误类别处理时值得使用。

多线程与并发

进程是操作系统分配资源和运行程序的单位;线程是进程中的执行路径。每个线程有自己的栈,同一个进程里的线程可以共享堆中的对象,因此多个线程同时修改同一份数据时会出现并发问题。

1
2
3
4
5
一个 Java 进程
│
├─ 线程 A:自己的栈 ──┐
├─ 线程 B:自己的栈 ──┼── 共享的堆
└─ 线程 C:自己的栈 ──┘

堆(Heap)主要存放通过 new 创建的对象,通常由垃圾回收器管理。例如:

1
User user = new User();

可以粗略理解为:new User() 创建的 User 对象在堆上;局部变量 user 保存着引用,位于当前方法的栈帧中。这里的“引用在栈上”是帮助理解的简化说法,JVM 的实际实现可能经过优化。
栈(Stack)属于线程。线程调用方法时,会为方法创建一个栈帧,里面保存局部变量、参数和方法执行所需的信息。方法结束后,对应栈帧就退出。
从流程上来讲,一个进程会分几个线程运行,而每个线程都会记录自己运行到哪一步了,当前的方法和局部变量是什么,这就是栈帧,每个方法有自己的栈,所以其中的局部变量不与其他线程贡献,但对象通常在堆上,所以同一个对象可以被多个线程引用,故而多个线程都使用时,两个线程可能交错执行,导致更新丢失,例如:

1
2
3
4
5
6
7
8
9
class Counter {
int count = 0;
}
Counter counter = new Counter(); // 创建一个对象

counter.count++;
// 假设两个线程都拿到 counter,并执行:
// 这个操作实际大致包含“读取 count、加一、写回 count”。
// 并发时两个线程都先读到 0,然后各自写回 1,最终结果是 1,而不是预期的 2。

因此,我们可以发现,栈隔离了各线程的方法执行信息,但不会自动保护堆中共享对象。 多线程安全问题通常来自共享数据的并发读写。

同步机制

怎么保护共享数据?对于简单计数,可以用同步机制:

1
2
3
4
5
6
7
// synchronized:给一段代码加锁
synchronized void increment() {
count++;
}
// 也可以使用 AtomicInteger,提供原子操作
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

此外,volatile 可以让一个变量的更新对其他线程更及时可见,也能限制部分指令重排;不过它不能保护count++ 这样的复合操作,而更适合某些单纯读写状态标志的场景。
也就是并发常见的这三点:

  • 原子性:操作不能被线程切换拆开,例如 count++ 不是原子操作。
  • 可见性:一个线程修改变量后,另一个线程能否及时看到。
  • 有序性:代码执行顺序是否可能受编译器或处理器优化影响。

死锁

两个线程互相等待对方持有的锁,就可能一直卡住:

1
2
线程 A 持有锁 1,等待锁 2
线程 B 持有锁 2,等待锁 1

降低风险的常见做法是:固定获取锁的顺序、减少嵌套锁、缩小加锁范围,并尽量使用并发工具类。

并发编程

wait()、notify() 和 sleep()
常见问题:
sleep() 是 Thread 的方法,暂停当前线程一段时间,不会主动释放已经持有的锁;
wait() 是 Object 的方法,必须在持有该对象监视器的同步代码中调用;等待时会释放该对象的锁;
notify() 会唤醒一个等待该对象监视器的线程,但被唤醒线程还需要重新竞争锁才能继续执行。

常见问题

1.进程和线程有什么区别?
答:进程是资源分配和隔离的基本单位;线程是进程中的执行路径。同一进程的线程通常共享堆等资源,但各自有线程栈和执行状态。
2.什么是线程安全?count++ 为什么不是原子操作?
答:线程安全表示并发访问时结果仍符合预期。count++ 包含读取、计算、写回多个步骤,线程交错时可能丢失更新。
3.synchronized 锁住的是什么?实例同步方法和静态同步方法有什么区别?
答:实例同步方法锁当前对象 this;静态同步方法锁该类对应的 Class 对象。竞争同一把锁的线程才会互斥。
4.volatile 能保证什么,不能保证什么?
答:它主要保证变量更新的可见性并限制相关重排序;不能保证 count++ 这类复合操作的原子性。
5.AtomicInteger 如何保证计数操作的原子性?
答:它通过原子变量操作(通常基于 CAS 等机制)完成递增,避免普通读改写之间的竞争;复杂的多变量业务仍可能需要锁。
6.什么是死锁?常见的四个必要条件是什么?
答:多个线程永久等待对方持有的资源称为死锁。必要条件是互斥、占有并等待、不可抢占、循环等待。
7.什么是线程池?为什么不建议随意创建大量线程
答:线程池复用线程并管理任务队列和并发数量;线程过多会增加内存、调度和上下文切换开销,甚至耗尽系统资源。

IO 与 NIO

Java 的 IO(Input/Output,输入/输出)主要解决一件事:程序如何从外部读取数据,或把数据写到外部。我们需要重点学习文件读写、字节流与字符流、缓冲、编码、资源关闭,以及传统 IO 和 NIO 的区别。

字节流与字符流

对于流,可以理解为一条连续的数据通道,读数据时,数据从文件流向程序;写文件时,数据从程序流向文件。常见的流分类:

  • 字节流:InputStream / OutputStream,适合图片、压缩包等原始字节;
  • 字符流:Reader / Writer,适合文本,并处理字符编码。
    读文本文件可以用:
1
2
3
4
5
6
7
try (BufferedReader reader =
Files.newBufferedReader(Path.of("notes.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}

try-with-resources 会在代码结束时关闭资源,减少文件句柄泄漏的风险。文本处理时要明确字符编码,例如 UTF-8,否则可能乱码。

缓冲流

BufferedReader、BufferedInputStream 等通过缓冲减少小块读写次数,通常比逐字节直接读写更合适。

NIO

Java NIO 提供了 Channel、Buffer、Selector 等能力:

  • Channel:数据通道;
  • Buffer:读写数据的缓冲区;
  • Selector:让一个线程监控多个可选择的通道,常用于非阻塞网络 IO。
    可以先记住区别:传统 IO 常以流的方式处理数据;NIO 提供缓冲区和通道,并支持非阻塞 IO。面试初期不必先钻到底层网络细节。

面试中常见的追问

建议优先掌握这些:

  1. 字节流和字符流有什么区别?图片应该用哪一种?
    答:字节流按字节处理,适合图片等二进制数据;字符流按字符处理,适合文本。
  2. 为什么文本读写要指定 UTF-8 等字符编码?
    答:文件字节需要按字符集解码成字符;读写双方编码不一致可能出现乱码。
  3. BufferedReader 有什么作用?为什么能提高效率?
    答:它为字符输入提供缓冲并支持按行读取,可减少频繁的小块底层读取调用。
  4. read() 返回 -1 代表什么?为什么要检查实际读取长度?
    答:返回 -1 表示到达输入末尾;批量读取可能少于缓冲区长度,应只处理实际读到的字节数。
  5. try-with-resources 是什么?什么对象可以放在里面?
    答:它会在代码块结束时自动关闭资源;资源需实现 AutoCloseable(或其子接口 Closeable)。
  6. File 和 Path 有什么区别?
    答:File 是较早的文件路径与操作抽象;Path 是较新的路径抽象,常与 Files 配合完成文件操作。
  7. throws IOException 和 try-catch 的区别是什么?
    答:throws 将异常交给调用方处理;try-catch 在当前代码处捕获并处理异常,也可以在处理后继续抛出。
  8. 小文件和大文件读取时,方式上有什么不同?
    答:小文件可按需求整体读取;大文件通常分块或逐行处理,避免一次性占用过多内存。

JVM:Java 程序运行的环境

JVM 负责加载并运行 Java 字节码。Java 源码经编译后得到 .class 字节码,JVM 再执行它。

运行时内存的基本区域

  • 堆:主要存放对象,通常由多个线程共享;
  • 虚拟机栈:每个线程各自拥有,方法调用时创建栈帧,保存局部变量等;
  • 程序计数器:记录当前线程正在执行的位置;
  • 方法区相关区域:保存类信息等;在现代 HotSpot JVM 中,类元数据主要放在本地内存的 Metaspace。
    一个简单的理解方式:
1
User user = new User();

user 这个局部变量通常在当前方法的栈帧中;new User() 创建的对象通常在堆中。具体实现会受 JVM 优化影响,面试基础回答掌握这个模型即可。

垃圾回收

垃圾回收器会清理不可达对象占用的堆内存。对象没有任何可到达路径时,通常具备被回收的条件;但不应该依赖某个对象立刻被回收,也不应通过 System.gc() 保证立即回收。

内存泄漏

Java 有垃圾回收,也可能发生内存泄漏:对象已经不再需要,但仍被某个长期存活的引用持有,垃圾回收器就无法回收它。例如把不再使用的数据长期放在静态集合中。

常见问题

1.JVM 运行时内存区域有哪些?哪些是线程私有的?
答:常见区域有程序计数器、虚拟机栈、本地方法栈、堆和方法区。前三者通常是线程私有的,堆和方法区通常由线程共享。
2.堆和虚拟机栈分别存什么?
答:堆主要存放对象实例;虚拟机栈按线程维护方法调用栈帧,包含局部变量等执行信息。
3.对象什么时候可以被垃圾回收?
答:对象从 GC Roots 出发不可达时通常具备回收条件,但实际回收时机由 JVM 决定,不保证立即执行。
4.Java 中会发生内存泄漏吗?举例说明。
答:会。对象已经无用但仍被长生命周期引用持有,例如静态集合持续保存不再需要的数据,GC 就无法回收它。
5.类加载大致经历哪些阶段?
答:常概括为加载、验证、准备、解析、初始化;不同阶段完成字节码载入、校验、类变量准备、引用解析和初始化代码执行等工作。
6.方法区和 Metaspace 是什么关系?
答:方法区是 JVM 规范中的逻辑区域;Metaspace 是 HotSpot JVM 对类元数据存储的一种实现,使用本地内存。
7.StackOverflowError 和 OutOfMemoryError 有什么区别?
答:StackOverflowError 常见于线程栈调用层次过深;OutOfMemoryError 表示某类内存资源无法继续分配。

MySQL

MySQL 基础

基本 SQL:SELECT、INSERT、UPDATE、DELETE,以及 WHERE、ORDER BY、LIMIT。
表结构:常见字段类型、主键、非空、唯一约束;理解一对一、一对多、多对多关系。
联表查询:掌握 INNER JOIN 和 LEFT JOIN,会根据表之间的关联字段查询。
索引:理解索引为何能加快查询、索引的代价、联合索引最左前缀规则。
事务:理解事务的作用和 ACID,知道 Spring 中常用 @Transactional。
分页和聚合:会用 LIMIT 分页、COUNT 统计,以及基础的 GROUP BY。
执行计划:知道 EXPLAIN 可以查看数据库如何执行一条查询。

索引

索引可以理解成数据库为了更快找到数据而建立的查询目录。没有合适索引时,数据库可能要逐行检查整张表;有合适索引时,可以先按索引找到相关记录,再读取数据。
例如用户表中经常按账号查询:

1
2
3
SELECT *
FROM user
WHERE userid = 10001;

如果 userid 上有索引,数据库通常可以更快定位对应行。InnoDB 常见索引结构是 B+ 树。
创建索引示例:

1
CREATE INDEX idx_user_userid ON user(userid);

如果 userid 本身是主键,通常已经有主键索引,不需要再重复创建普通索引。
联合索引
多个字段组成的索引叫联合索引:

1
2
3
4
CREATE INDEX idx_org_user ON organization_member(org_id, user_id);
-- 它通常不适合只按 user_id 查询。这就是常说的最左前缀原则:联合索引从最左边的字段开始匹配。
WHERE org_id = 10
WHERE org_id = 10 AND user_id = 10001

索引的代价和注意点

索引不是越多越好:

  • 索引占用存储空间;
  • 新增、修改、删除数据时,数据库也要维护索引;
  • 某个查询不一定会使用索引,数据库会根据执行计划选择;
  • 应该围绕常见查询条件设计索引,并通过 EXPLAIN 检查,而不是给每个字段都加索引。
    对你的项目,可以选一条真实的条件查询,看看筛选条件、排序字段和关联字段是什么,再讨论索引。具体是否需要索引,要以表结构和实际查询为依据。

常见问题

1.索引是什么,为什么能加快查询?
答:索引是数据库维护的查找结构,可以帮助定位数据、减少扫描范围;是否变快取决于索引是否匹配查询。
2.索引越多越好吗?
答:不是。索引占空间,也增加写入维护成本,应围绕常见查询建立必要索引。
3.什么是联合索引的最左前缀原则?
答:联合索引 (a, b, c) 通常从最左侧字段开始匹配,如按 a 或 a、b 查询;单独按 b 查询通常不能充分利用该索引。
4.如何查看一条查询是否使用索引?
答:使用 EXPLAIN 查看执行计划,关注选择的索引、访问方式和扫描行数等信息。

联表查询

现实业务的数据通常拆在多张表中。联表查询就是依据表之间的关联字段,把多张表的数据组合起来读取。
比如系统中可能有:

  • user:用户信息;
  • organization_member:用户和组织的关系;
  • organization:组织信息。
    一个用户可以加入多个组织,一个组织也可以有多个用户,因此用 organization_member 作为中间表表达多对多关系。

INNER JOIN:只返回匹配的记录

查询用户及其所属组织:

1
2
3
4
5
6
7
SELECT u.userid, u.username, o.org_name
FROM user u
-- 第一段 INNER JOIN:用户 ID 必须能在成员关系表的 user_id 中找到匹配记录。
INNER JOIN organization_member m ON u.userid = m.user_id
-- 第二段 INNER JOIN:成员关系中的组织 ID 又必须能在组织表的 id 中找到匹配记录。
INNER JOIN organization o ON m.org_id = o.id;
-- 两段都匹配成功,用户才会出现在结果中。

LEFT JOIN:左表记录都保留

如果希望看到所有用户,包括还没有加入组织的用户:

1
2
3
4
SELECT u.userid, u.username, o.org_name
FROM user u
LEFT JOIN organization_member m ON u.userid = m.user_id
LEFT JOIN organization o ON m.org_id = o.id;

LEFT JOIN 会保留左侧 user 表的所有记录。某个用户没有匹配的组织时,组织相关字段会是 NULL。

联表查询面试题

1.INNER JOIN 和 LEFT JOIN 有什么区别?
答:INNER JOIN 只返回两边匹配的行;LEFT JOIN 保留左表所有行,右表无匹配时右侧字段为 NULL。
2.多对多关系通常怎么建模?
答:通过中间表分别关联两侧实体,例如组织成员表保存组织 ID 和用户 ID。
3.为什么联表时要写 ON 条件?
答:ON 用来说明两张表如何关联;缺少或写错关联条件可能生成大量错误组合。

Springboot

RESTful 接口设计

RESTful 是一种 API 设计风格:URL 表示资源,HTTP 方法表示对资源的操作。接口路径尽量使用名词和资源层级,避免把操作动词都放进 URL。这样的好处是接口统一,更易理解和维护,包括复用机制方法等等。

1
2
3
4
5
6
GET    /users/1001       查询用户
POST /users 新增用户
PUT /users/1001 完整修改用户
PATCH /users/1001 部分修改用户
DELETE /users/1001 删除用户
GET /organizations/5/members 查询组织成员

常见状态码包括:200 表示请求成功,201 表示创建成功,400 表示请求参数有问题,401 表示未认证,403 表示无权限,404 表示资源不存在,500 表示服务端发生未预期错误。
接口是否“RESTful”要看 URL、HTTP 方法和响应语义是否一致。项目使用 Spring Boot 提供 HTTP 接口,可以说采用了 REST 风格的接口设计;如果某些操作使用 POST 查询或删除,应按实际实现描述,不必说所有接口都严格符合 RESTful。

常见问题

  1. RESTful 接口设计的核心思想是什么?
    答:用 URL 标识资源,用 HTTP 方法表达对资源的操作,并通过状态码和响应体说明处理结果。
  2. GET 和 POST 有什么区别?
    答:GET 通常用于查询,参数常放在 URL 查询串中;POST 通常用于提交数据或创建资源,数据常放在请求体中。实际语义还应结合接口设计,不能只按“参数放在哪里”判断。
  3. PUT 和 PATCH 有什么区别?
    答:PUT 通常表示整体替换资源,PATCH 通常表示只修改资源的部分字段。
  4. 为什么接口不建议设计成 /getUser、/deleteUser?
    答:这种写法把操作动词放在路径里,资源和动作不够统一。REST 风格通常使用 /users/{id} 表示资源,再用 GET、DELETE 等 HTTP 方法表达操作。
  5. HTTP 状态码和统一响应里的业务 code 有什么区别?
    答:HTTP 状态码描述 HTTP 请求层面的结果;业务 code 描述应用内部的业务结果。两者可以配合使用,不应只靠响应体里的业务 code 表示所有错误。

Controller、Service、Mapper 分层

常见后端调用链是:

1
2
HTTP 请求 → Controller → Service → Mapper → 数据库
HTTP 响应 ← Controller ← Service ← Mapper ← 数据库
  • Controller:接收 HTTP 请求、解析参数、调用 Service、组织响应。应保持轻量,避免直接写复杂业务逻辑和 SQL。
  • Service:实现业务规则、组织多个数据操作;需要保证一组数据库操作同时成功或失败时,通常在这一层定义事务边界。
  • Mapper:负责数据访问。项目中使用 MyBatis,Mapper 接口的方法与 XML 或注解中的 SQL 对应。
  • 数据库:存储业务数据并执行 SQL。

常见问题

  1. 为什么要分 Controller、Service、Mapper?
    答:分层能把 HTTP 处理、业务规则和数据库访问分开,降低耦合,让代码更容易维护和测试。
  2. Controller 中能不能写业务逻辑?
    答:简单的参数接收和流程转发可以放在 Controller;复杂规则、跨多个数据操作的流程应放在 Service,避免 Controller 变得臃肿。
  3. Service 层一般负责什么?
    答:承载业务规则,组合一个或多个 Mapper 操作,控制事务边界,并返回适合上层使用的结果。
  4. MyBatis 的 Mapper 接口没有实现类,为什么还能调用?
    答:MyBatis 会为 Mapper 接口创建代理对象,调用方法时根据映射配置找到 SQL 并执行。
  5. 一次请求如何从前端走到数据库再返回?
    答:前端发 HTTP 请求,Spring MVC 匹配 Controller 方法;Controller 调用 Service,Service 调用 Mapper,MyBatis 执行 SQL;结果映射成 Java 对象后逐层返回,再序列化为 JSON 响应。

统一响应格式

统一响应的目的,是让前端用一致的方式读取成功数据和错误信息。常见结构为:

1
2
3
4
5
6
7
{
"code": "0",
"message": "success",
"data": {
"id": 1001
}
}

字段通常包括:

  • code:业务结果标识;
  • message:给调用方看的提示;
  • data:实际返回数据。

常见问题

  1. 为什么要设计统一响应格式?
    答:统一字段和结构后,前端可以一致地处理成功数据、业务错误和提示信息,减少每个接口各自判断格式的情况。
  2. 统一响应通常包含哪些字段?
    答:通常包含业务 code、提示 message 和 data;具体命名及 code 约定应在项目内保持一致。
  3. 为什么不能只用 code 表示成功或失败?
    答:响应体中的业务 code 不能替代 HTTP 状态码。HTTP 状态码表达协议层结果,业务 code 表达应用层业务结果。
  4. 错误响应应避免包含什么?
    答:避免返回堆栈、SQL、服务器路径、密码、密钥和 Token 等敏感信息;详细错误应记录在受控的服务端日志中。

全局异常处理

Spring MVC 中可以用 @RestControllerAdvice 定义全局异常处理类,用 @ExceptionHandler 指定某类异常由哪个方法处理。

1
2
3
4
5
6
7
8
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
return Result.error(e.getMessage());
}
}

业务代码在发现业务条件不满足时,可以抛出明确的业务异常;全局异常处理器统一转换为响应。对未预期异常,应记录服务端日志并返回通用错误信息,避免把内部实现细节发给客户端。

常见问题

  1. @RestControllerAdvice 有什么作用?
    答:它可以对多个 Controller 生效,集中处理异常、绑定数据或做全局增强;作为 REST 接口异常处理时,返回值会写入响应体。
  2. @ExceptionHandler 有什么作用?
    答:它将指定异常类型映射到处理方法,发生对应异常时由该方法生成响应。
  3. 为什么需要全局异常处理?
    答:可以统一异常到响应的转换,避免每个 Controller 重复写 try-catch,并让客户端收到一致的错误格式。
  4. 所有异常都应该返回同一个错误吗?
    答:不应该。业务异常、参数校验异常和未预期异常可以使用不同的业务 code 和 HTTP 状态码;对客户端的错误信息应安全、明确。

JWT 登录鉴权

JWT 通常由三部分组成:Header、Payload、Signature。它常用于在客户端和服务端之间传递身份声明。
典型流程:
用户提交登录信息
→ 后端校验账号与密码
→ 后端生成并签名 JWT
→ 前端保存凭证
→ 后续请求携带 Authorization: Bearer
→ 后端验证签名、有效期等信息
→ 识别用户并检查接口权限
JWT 通常是签名而非加密:签名用于检查内容是否被篡改,但 Payload 一般可以被解码读取,因此不能把密码等敏感信息放进 JWT。后端必须校验签名和有效期,不能只解码 Token 就相信里面的用户信息。

数据库表设计

设计表时先识别业务实体、属性和实体间关系,再确定主键、字段类型、约束和索引。

项目简介

对于上一关项目来讲,主要是对于Vue 和 Springboot 的学习做一个总结,通过全栈式开发用到所学的知识,前端使用Vue + Element UI开发页面和组件,使用Axios包装请求发送到后端,后端使用Spring Boot,按 Controller、Service、Mapper 分层。其中 Controller 层负责接收 HTTP 请求并返回响应,Service 处理业务流程,Mapper 配合 MyBatis SQL 访问数据库,响应通常包装为统一的 Result,再转换成 JSON 返回前端。
从功能实现来讲,首先就是角色鉴权,不同的角色包含不同的用户实例,用户登陆后返回用户信息,包含在token中,由于token将信息加密算法填入,所以可以实现防止修改,以用户权限进入用户端和教师端会有不同的功能页面。涉及账户信息,新建群组,好友聊天,资源上传等功能。
这其中对于Java来说主要是包括各种实体类,如用户信息,聊天列表,数据加载和交互主要是使用arry list数组;方法调用,如controller层对于service业务层的方法调用,用于处理各项业务实际,比如登录,controller层获取前端传入的信息后调用service业务方法,将数据传入后由service传入mapper查询数据库。而业务处理过程中也包含各种异常处理设置,如用户登录失败,对应无用户名或者密码错误,文件读取失败会释放io资源等等。与数据库的交互主要依靠mybatis查询实现。