异常处理

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 相关的异常、ClassNotFoundExceptionSQLException 等。

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 块中的代码都会执行。当 trycatch 块中遇到 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! 如果在 tryfinally 中都有 returntry 的返回值会被忽略。

根据 JVM 官方文档:

如果 try 块执行了 return,编译代码做如下处理:

  1. 将返回值(如有)保存在一个局部变量中。
  2. 执行 jsr 调用 finally 块。
  3. 返回值会在 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 块的代码不被执行:

  1. 所在线程死亡。
  2. CPU 停止。

try-with-resources 的使用

  1. 适用范围:任何实现了 java.lang.AutoCloseablejava.io.Closeable 接口的对象。
  2. 资源关闭顺序:在 try-with-resources 语句中,任何 catchfinally 块在声明的资源关闭后运行。

根据《Effective Java》的建议:

面对必须关闭的资源,优先使用 try-with-resources 而非 try-finally。代码简洁明了,异常信息更易管理。

Java 中需要手动关闭的资源有 InputStreamOutputStreamScannerPrintWriter 等。

例如,使用 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 中的 sortbinarySearch 等方法)。

反射

反射的定义

反射赋予我们在运行时分析类及其方法的能力,这使得我们能够获取类的所有属性和方法,并调用这些方法。

反射的优缺点

  • 优点:让代码更加灵活,便于框架提供即开即用的功能。
  • 缺点:增加了安全风险,例如可以忽视泛型参数的安全检查。此外,反射的性能较低,但对于框架来说这些影响并不显著。

反射的应用场景

尽管在日常业务代码中使用反射的机会较少,但许多框架(如 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 流提供了直接操作字符的接口,以便更便捷地处理字符数据。对于音频、图片等媒体文件,使用字节流更为合适,而涉及字符的则使用字符流。