最新Java面试指南:2025年最常见面试知识点与技巧总结
异常处理
Exception 和 Error 的区别
在 Java 中,所有异常均继承自 java.lang
包下的 Throwable
类。Throwable
有两个主要子类:
Exception
:可以被程序本身处理的异常,通常通过catch
捕获。这一类又分为 Checked Exception(受检查异常,必须处理)和 Unchecked Exception(不受检查异常,可以选择性处理)。Error
:指无法通过程序处理的错误,通常不应被捕获,如 Java 虚拟机运行错误(VirtualMachineError
)、内存溢出错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等。发生这些错误时,JVM 通常会终止线程。
Checked Exception 和 Unchecked Exception 的区别
Checked Exception(受检查异常)在编译过程中必须处理,否则编译会失败。
例如:
除了 RuntimeException
及其子类外,其他异常均属于受检查异常。常见的受检查异常包括与 IO 相关的异常、ClassNotFoundException
、SQLException
等。
Unchecked Exception(不受检查异常)可以在编译时不处理,仍然能通过编译。所有 RuntimeException
及其子类都是不受检查异常,常见示例如下:
NullPointerException
(空指针异常)IllegalArgumentException
(参数不合法异常)NumberFormatException
(字符串转数字格式异常,属于IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界异常)ClassCastException
(类型转换异常)ArithmeticException
(算术运算异常)SecurityException
(安全异常,权限不足)UnsupportedOperationException
(不支持的操作异常,例如重复创建同一用户)
Throwable 类的常用方法
String getMessage()
:返回异常的简要描述String toString()
:返回异常的详细信息String getLocalizedMessage()
:返回异常对象的本地化信息。子类可以覆盖此方法以生成本地化信息。void printStackTrace()
:在控制台上打印异常信息。
try-catch-finally 的使用方式
try
块:用于捕获异常,可接零个或多个catch
块。如无catch
块,需至少跟一个finally
块。catch
块:处理try
块捕获的异常。finally
块:无论是否发生异常,finally
块中的代码都会执行。当try
或catch
块中遇到return
语句时,finally
中的代码会在方法返回之前执行。
代码示例:
try {
System.out.println("Try to do something");
throw new RuntimeException("RuntimeException");
} catch (Exception e) {
System.out.println("Catch Exception -> " + e.getMessage());
} finally {
System.out.println("Finally");
}
输出:
Try to do something
Catch Exception -> RuntimeException
Finally
注意:在 finally 块中不要使用 return! 如果在 try
和 finally
中都有 return
,try
的返回值会被忽略。
根据 JVM 官方文档:
如果
try
块执行了 return,编译代码做如下处理:
- 将返回值(如有)保存在一个局部变量中。
- 执行 jsr 调用
finally
块。- 返回值会在
finally
块执行完后返回。
代码示例:
public static void main(String[] args) {
System.out.println(f(2));
}
public static int f(int value) {
try {
return value * value;
} finally {
if (value == 2) {
return 0;
}
}
}
输出:
0
finally 块中的代码是否一定会执行?
并不一定!在某些情况下,例如 JVM 被强制终止时,finally
块中的代码将不会执行。
try {
System.out.println("Try to do something");
throw new RuntimeException("RuntimeException");
} catch (Exception e) {
System.out.println("Catch Exception -> " + e.getMessage());
// 强制终止当前正在运行的 Java 虚拟机
System.exit(1);
} finally {
System.out.println("Finally");
}
输出:
Try to do something
Catch Exception -> RuntimeException
此外,以下两种特殊情况也可能导致 finally
块的代码不被执行:
- 所在线程死亡。
- CPU 停止。
try-with-resources 的使用
- 适用范围:任何实现了
java.lang.AutoCloseable
或java.io.Closeable
接口的对象。 - 资源关闭顺序:在
try-with-resources
语句中,任何catch
或finally
块在声明的资源关闭后运行。
根据《Effective Java》的建议:
面对必须关闭的资源,优先使用
try-with-resources
而非try-finally
。代码简洁明了,异常信息更易管理。
Java 中需要手动关闭的资源有 InputStream
、OutputStream
、Scanner
、PrintWriter
等。
例如,使用 try-catch-finally
的传统实现:
// 读取文本文件的内容
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
使用 Java 7 以后的 try-with-resources
重构:
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
对于多个资源的处理也非常简单,可以通过分号分隔在 try-with-resources
块中声明多个资源:
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
异常处理的注意事项
- 不要将异常定义为静态变量,以免造成异常栈信息混乱。每次手动抛出异常时,应新建一个异常对象。
- 抛出的异常信息必须具有实际意义。
- 建议抛出更具体的异常。例如,转换字符串为数字格式错误时应抛出
NumberFormatException
,而非其父类IllegalArgumentException
。 - 使用日志打印异常后,不要再抛出该异常(两者不应同时存在于同一段逻辑中)。
泛型
什么是泛型?它的作用是什么?
Java 泛型(Generics)是在 JDK 5 中引入的特性,使用泛型参数可以增强代码的可读性和稳定性。
编译器可以检测泛型参数,并为传入对象指定类型。例如,以下代码指定该 ArrayList
对象只能接受 Persion
类型的对象:
ArrayList<Persion> persons = new ArrayList<Persion>();
泛型的使用方式
泛型通常有三种使用方式:泛型类、泛型接口、泛型方法。
1. 泛型类:
// T 可以用任意标识符,常见的有 T、E、K、V 等
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
2. 泛型接口:
public interface Generator<T> {
public T method();
}
实现泛型接口(不指定类型):
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
实现泛型接口(指定类型):
class GeneratorImpl implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
3. 泛型方法:
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用示例:
// 创建不同类型数组:Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
项目中泛型的应用
- 自定义接口
CommonResult<T>
通过参数T
可动态指定结果数据类型。 - 定义
Excel
处理类ExcelUtil<T>
用于动态指定导出数据类型。 - 构建集合工具类(例如
Collections
中的sort
和binarySearch
等方法)。
反射
反射的定义
反射赋予我们在运行时分析类及其方法的能力,这使得我们能够获取类的所有属性和方法,并调用这些方法。
反射的优缺点
- 优点:让代码更加灵活,便于框架提供即开即用的功能。
- 缺点:增加了安全风险,例如可以忽视泛型参数的安全检查。此外,反射的性能较低,但对于框架来说这些影响并不显著。
反射的应用场景
尽管在日常业务代码中使用反射的机会较少,但许多框架(如 Spring/Spring Boot、MyBatis)都大量应用了反射机制。
这些框架中也大量使用动态代理,而动态代理的实现依赖于反射。
以下是通过 JDK 实现动态代理的示例代码,使用了 Method
类来调用指定方法:
public class DebugInvocationHandler implements InvocationHandler {
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}
此外,Java 中的注解特性也基于反射实现。
注解
注解(Annotation
)是 Java 5 引入的特性,用于修饰类、方法或变量。注解本质上是一个继承了 Annotation
的特殊接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
注解只有在解析后才会生效,常见的解析方式有:
- 编译期直接扫描:编译器在编译 Java 代码时扫描相应的注解并处理,例如在编译时检测方法是否重写了父类的方法。
- 运行时通过反射处理:许多框架中使用的注解(如 Spring 的
@Value
、@Component
)都是通过反射进行处理的。
JDK 还提供了许多内置注解(如 @Override
、@Deprecated
),同时我们也可以自定义注解。
I/O 操作
什么是序列化和反序列化?
在需要持久化 Java 对象时(如将对象存储在文件中或通过网络传输),我们需要进行序列化与反序列化。
简而言之:
- 序列化:将对象或数据结构转化为二进制字节流的过程。
- 反序列化:将序列化生成的二进制字节流转换为数据结构或对象的过程。
对于 Java 等面向对象编程语言,我们序列化的主要是对象(实例化后的类)。
维基百科对序列化的定义为:
序列化是一种数据处理技术,它将数据结构或对象状态转换为可用格式(例如存储为文件或通过网络发送),以便在相同或不同计算机环境中恢复原先状态。
因此,序列化的主要目的是通过网络传输对象或将对象存储到文件系统、数据库或内存中。
如何在 Java 序列化中排除某些字段?
对于不希望序列化的字段,可以使用 transient
关键字进行修饰。
transient
的用途在于:阻止被修饰的变量参与序列化。当对象被反序列化时,transient
修饰的变量值不会被持久化或恢复。
关于 transient
的注意事项:
- 仅能修饰变量,不能修饰类或方法。
- 被修饰的变量在反序列化时将被重置为类型的默认值,例如
int
类型会重置为0
。 static
变量不属于任何对象,因此无论是否用transient
修饰,均不会被序列化。
获取键盘输入的常用方法
方法 1:使用 Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
方法 2:使用 BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
Java 中 I/O 流的分类
- 按流向分为输入流和输出流;
- 按操作单元分为字节流和字符流;
- 按角色分为节点流和处理流。
Java I/O 流涉及的类超过 40 个,它们看似杂乱,但实际上具有明确的层级关系,且彼此之间紧密相连。这些类均来自以下四个抽象基类:
- InputStream/Reader:所有输入流的基类,其中 InputStream 是字节输入流,Reader 是字符输入流。
- OutputStream/Writer:所有输出流的基类,其中 OutputStream 是字节输出流,Writer 是字符输出流。
按操作方式的分类结构图:
按操作对象的分类结构图:
字节流和字符流的必要性
提问本质为:不论是文件读写还是网络传输,信息的最小存储单位都是字节,为什么还要区分字节流和字符流?
答案是:字符流由 JVM 将字节转换而来,这个过程相对耗时,且如果不清楚编码类型容易出现乱码。因此,I/O 流提供了直接操作字符的接口,以便更便捷地处理字符数据。对于音频、图片等媒体文件,使用字节流更为合适,而涉及字符的则使用字符流。