前言 最近收到了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); System.out.println(b);
基本类型和包装类型 每个基本类型都有对应的包装类,例如:
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; text = new String ("后端" ); System.out.println(text); System.out.println(other);
这样设计便于安全共享、缓存哈希值,也让字符串作为键时更可靠。
集合 集合可以理解为 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 3 double price = 9.8 ;int whole = (int ) price;
常见问题 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()) { }
常见问题 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; } add(1 , 2 ); add(1.5 , 2.5 );
Java 是值传递 传基本类型时,方法拿到的是数值副本:
1 2 3 4 5 6 7 static void change (int n) { n = 100 ; } int x = 10 ;change(x);
传对象时,方法拿到的是“引用值的副本”。它可以通过这个副本修改同一个对象的内容,但重新给这个副本赋值,不会改变调用者的引用:
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(); dog.bark();
Java 的类是单继承:一个类只能直接继承一个父类,但可以实现多个接口。
多态 多态常见形式是:父类或接口类型的变量,指向子类对象;调用被重写的方法时,运行时执行子类实现。对于上面那个例子:
1 2 3 Animal animal = new Dog (); animal.eat();
父类类型屏蔽子类的具体差异,方便统一调用;假如子类重写父类方法后,运行时执行子类版本。子类独有功能则需要通过子类类型访问。 理解多态时重点记住:代码可以面向父类或接口编写,运行时由实际对象决定具体行为。 这有助于扩展实现,也常见于 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); System.out.println(a.equals(b));
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 创建的对象,通常由垃圾回收器管理。例如:
可以粗略理解为: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++;
因此,我们可以发现,栈隔离了各线程的方法执行信息,但不会自动保护堆中共享对象。 多线程安全问题通常来自共享数据的并发读写。
同步机制 怎么保护共享数据?对于简单计数,可以用同步机制:
1 2 3 4 5 6 7 synchronized void increment () { count++; } 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。面试初期不必先钻到底层网络细节。
面试中常见的追问 建议优先掌握这些:
字节流和字符流有什么区别?图片应该用哪一种? 答:字节流按字节处理,适合图片等二进制数据;字符流按字符处理,适合文本。
为什么文本读写要指定 UTF-8 等字符编码? 答:文件字节需要按字符集解码成字符;读写双方编码不一致可能出现乱码。
BufferedReader 有什么作用?为什么能提高效率? 答:它为字符输入提供缓冲并支持按行读取,可减少频繁的小块底层读取调用。
read() 返回 -1 代表什么?为什么要检查实际读取长度? 答:返回 -1 表示到达输入末尾;批量读取可能少于缓冲区长度,应只处理实际读到的字节数。
try-with-resources 是什么?什么对象可以放在里面? 答:它会在代码块结束时自动关闭资源;资源需实现 AutoCloseable(或其子接口 Closeable)。
File 和 Path 有什么区别? 答:File 是较早的文件路径与操作抽象;Path 是较新的路径抽象,常与 Files 配合完成文件操作。
throws IOException 和 try-catch 的区别是什么? 答:throws 将异常交给调用方处理;try-catch 在当前代码处捕获并处理异常,也可以在处理后继续抛出。
小文件和大文件读取时,方式上有什么不同? 答:小文件可按需求整体读取;大文件通常分块或逐行处理,避免一次性占用过多内存。
JVM:Java 程序运行的环境 JVM 负责加载并运行 Java 字节码。Java 源码经编译后得到 .class 字节码,JVM 再执行它。
运行时内存的基本区域
堆:主要存放对象,通常由多个线程共享;
虚拟机栈:每个线程各自拥有,方法调用时创建栈帧,保存局部变量等;
程序计数器:记录当前线程正在执行的位置;
方法区相关区域:保存类信息等;在现代 HotSpot JVM 中,类元数据主要放在本地内存的 Metaspace。 一个简单的理解方式:
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);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_nameFROM user uINNER JOIN organization_member m ON u.userid = m.user_idINNER JOIN organization o ON m.org_id = o.id;
LEFT JOIN:左表记录都保留 如果希望看到所有用户,包括还没有加入组织的用户:
1 2 3 4 SELECT u.userid, u.username, o.org_nameFROM user uLEFT JOIN organization_member m ON u.userid = m.user_idLEFT 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。
常见问题
RESTful 接口设计的核心思想是什么? 答:用 URL 标识资源,用 HTTP 方法表达对资源的操作,并通过状态码和响应体说明处理结果。
GET 和 POST 有什么区别? 答:GET 通常用于查询,参数常放在 URL 查询串中;POST 通常用于提交数据或创建资源,数据常放在请求体中。实际语义还应结合接口设计,不能只按“参数放在哪里”判断。
PUT 和 PATCH 有什么区别? 答:PUT 通常表示整体替换资源,PATCH 通常表示只修改资源的部分字段。
为什么接口不建议设计成 /getUser、/deleteUser? 答:这种写法把操作动词放在路径里,资源和动作不够统一。REST 风格通常使用 /users/{id} 表示资源,再用 GET、DELETE 等 HTTP 方法表达操作。
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。
常见问题
为什么要分 Controller、Service、Mapper? 答:分层能把 HTTP 处理、业务规则和数据库访问分开,降低耦合,让代码更容易维护和测试。
Controller 中能不能写业务逻辑? 答:简单的参数接收和流程转发可以放在 Controller;复杂规则、跨多个数据操作的流程应放在 Service,避免 Controller 变得臃肿。
Service 层一般负责什么? 答:承载业务规则,组合一个或多个 Mapper 操作,控制事务边界,并返回适合上层使用的结果。
MyBatis 的 Mapper 接口没有实现类,为什么还能调用? 答:MyBatis 会为 Mapper 接口创建代理对象,调用方法时根据映射配置找到 SQL 并执行。
一次请求如何从前端走到数据库再返回? 答:前端发 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:实际返回数据。
常见问题
为什么要设计统一响应格式? 答:统一字段和结构后,前端可以一致地处理成功数据、业务错误和提示信息,减少每个接口各自判断格式的情况。
统一响应通常包含哪些字段? 答:通常包含业务 code、提示 message 和 data;具体命名及 code 约定应在项目内保持一致。
为什么不能只用 code 表示成功或失败? 答:响应体中的业务 code 不能替代 HTTP 状态码。HTTP 状态码表达协议层结果,业务 code 表达应用层业务结果。
错误响应应避免包含什么? 答:避免返回堆栈、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()); } }
业务代码在发现业务条件不满足时,可以抛出明确的业务异常;全局异常处理器统一转换为响应。对未预期异常,应记录服务端日志并返回通用错误信息,避免把内部实现细节发给客户端。
常见问题
@RestControllerAdvice 有什么作用? 答:它可以对多个 Controller 生效,集中处理异常、绑定数据或做全局增强;作为 REST 接口异常处理时,返回值会写入响应体。
@ExceptionHandler 有什么作用? 答:它将指定异常类型映射到处理方法,发生对应异常时由该方法生成响应。
为什么需要全局异常处理? 答:可以统一异常到响应的转换,避免每个 Controller 重复写 try-catch,并让客户端收到一致的错误格式。
所有异常都应该返回同一个错误吗? 答:不应该。业务异常、参数校验异常和未预期异常可以使用不同的业务 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查询实现。