MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java I/O异常处理与最佳实践

2022-11-092.3k 阅读

Java I/O 异常类型概述

在 Java 的 I/O 操作中,会遇到各种各样的异常情况。理解这些异常类型是正确处理 I/O 异常的基础。

1. IOException 及其子类

IOException 是所有 I/O 操作相关异常的基类。当发生与 I/O 操作相关的错误时,通常会抛出这个异常或它的子类。

FileNotFoundException:当试图打开一个不存在的文件进行读取,或者创建一个已存在的文件进行写入(如果不允许覆盖)时,会抛出此异常。例如:

import java.io.FileInputStream;
import java.io.IOException;

public class FileNotFoundExample {
    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("nonexistentfile.txt");
        } catch (FileNotFoundException e) {
            System.out.println("文件未找到: " + e.getMessage());
        }
    }
}

在上述代码中,尝试打开一个不存在的 nonexistentfile.txt 文件,会捕获到 FileNotFoundException 异常。

EOFException:当在输入操作中意外到达文件末尾时抛出。例如,使用 DataInputStream 读取数据,预期有更多数据但却到达了文件末尾:

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class EOFExceptionExample {
    public static void main(String[] args) {
        try (DataInputStream dis = new DataInputStream(new FileInputStream("test.txt"))) {
            while (true) {
                int num = dis.readInt();
                System.out.println(num);
            }
        } catch (EOFException e) {
            System.out.println("到达文件末尾: " + e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这里假设 test.txt 文件中的数据不足以满足 while (true) 循环中 readInt() 操作的期望,就会抛出 EOFException

InterruptedIOException:当 I/O 操作被中断时抛出。例如,在一个线程执行 I/O 操作时,另一个线程调用了该线程的 interrupt() 方法:

import java.io.FileInputStream;
import java.io.IOException;

public class InterruptedIOExceptionExample {
    public static void main(String[] args) {
        Thread ioThread = new Thread(() -> {
            try (FileInputStream fis = new FileInputStream("test.txt")) {
                byte[] buffer = new byte[1024];
                while (fis.read(buffer) != -1) {
                    // 模拟 I/O 操作
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            } catch (InterruptedIOException e) {
                System.out.println("I/O 操作被中断: " + e.getMessage());
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        ioThread.start();
        try {
            Thread.sleep(500);
            ioThread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,主线程启动一个执行 I/O 操作的子线程,一段时间后主线程中断子线程,子线程的 I/O 操作可能会抛出 InterruptedIOException

SocketException:在基于套接字的 I/O 操作中,当发生与套接字相关的错误时抛出,例如套接字关闭、连接超时等。比如在一个简单的客户端 - 服务器套接字通信中:

import java.io.IOException;
import java.net.Socket;

public class SocketExceptionExample {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 12345)) {
            // 正常的套接字操作
        } catch (SocketException e) {
            System.out.println("套接字异常: " + e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

如果服务器未在指定端口 12345 监听,或者网络连接出现问题,就可能抛出 SocketException

异常处理基础:try - catch 块

在 Java 中,使用 try - catch 块来捕获和处理 I/O 异常。

1. 基本的 try - catch 结构

import java.io.FileInputStream;
import java.io.IOException;

public class BasicTryCatchExample {
    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("test.txt");
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
            fis.close();
        } catch (IOException e) {
            System.out.println("发生 I/O 异常: " + e.getMessage());
        }
    }
}

在这个例子中,try 块包含可能抛出 IOException 的代码。如果在 try 块执行过程中抛出了 IOException,程序流程会立即跳转到对应的 catch 块,在 catch 块中可以对异常进行处理,这里简单地打印了异常信息。

2. 多重 catch 块

try 块中的代码可能抛出多种不同类型的异常时,可以使用多重 catch 块来分别处理不同类型的异常。例如:

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.EOFException;
import java.io.IOException;

public class MultipleCatchExample {
    public static void main(String[] args) {
        try (DataInputStream dis = new DataInputStream(new FileInputStream("test.txt"))) {
            while (true) {
                int num = dis.readInt();
                System.out.println(num);
            }
        } catch (EOFException e) {
            System.out.println("到达文件末尾: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("发生其他 I/O 异常: " + e.getMessage());
        }
    }
}

在这个代码中,try 块中的 readInt() 操作可能抛出 EOFException 或者其他类型的 IOException。通过两个不同的 catch 块,分别处理这两种异常情况,对 EOFException 给出特定的提示,对其他 IOException 给出通用的提示。

3. catch 块的顺序

在使用多重 catch 块时,需要注意异常类型的顺序。子类异常的 catch 块应该放在父类异常的 catch 块之前。例如:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class CatchOrderExample {
    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("nonexistentfile.txt");
        } catch (FileNotFoundException e) {
            System.out.println("文件未找到异常: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("其他 I/O 异常: " + e.getMessage());
        }
    }
}

如果将 catch (IOException e) 放在 catch (FileNotFoundException e) 之前,由于 FileNotFoundExceptionIOException 的子类,FileNotFoundException 异常也会被 catch (IOException e) 捕获,这样就无法对 FileNotFoundException 进行特定的处理了。

使用 finally 块确保资源关闭

在 I/O 操作中,打开的资源(如文件、套接字等)需要及时关闭,以避免资源泄漏。finally 块提供了一种机制,无论 try 块中是否抛出异常,都会执行其中的代码。

1. finally 块的基本用法

import java.io.FileInputStream;
import java.io.IOException;

public class FinallyExample {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("test.txt");
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            System.out.println("发生 I/O 异常: " + e.getMessage());
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    System.out.println("关闭文件时发生异常: " + e.getMessage());
                }
            }
        }
    }
}

在这个例子中,try 块执行文件读取操作,catch 块处理可能发生的 IOException。无论 try 块是否抛出异常,finally 块都会执行。在 finally 块中,首先检查 fis 是否为 null(因为如果在 new FileInputStream("test.txt") 时抛出异常,fis 不会被初始化),然后尝试关闭文件。如果关闭文件时也发生异常,同样在 catch 块中进行处理。

2. try - with - resources 语句

从 Java 7 开始,引入了 try - with - resources 语句,它是一种更简洁的处理资源关闭的方式。例如:

import java.io.FileInputStream;
import java.io.IOException;

public class TryWithResourcesExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("test.txt")) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            System.out.println("发生 I/O 异常: " + e.getMessage());
        }
    }
}

try - with - resources 语句中,声明的资源(这里是 FileInputStream)必须实现 AutoCloseable 接口。当 try 块执行完毕,无论是否抛出异常,资源会自动关闭,无需在 finally 块中显式地调用 close() 方法。这种方式不仅代码更简洁,而且能更好地保证资源的及时关闭,减少资源泄漏的风险。

自定义异常处理策略

除了基本的异常捕获和处理,还可以根据应用程序的需求制定自定义的异常处理策略。

1. 异常日志记录

在实际应用中,记录异常信息对于调试和故障排查非常重要。可以使用日志框架(如 Log4j、SLF4J 等)来记录异常。例如,使用 SLF4J 和 Logback:

<!-- logback.xml 配置文件 -->
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy - MM - dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="info">
        <appender - ref ref="STDOUT" />
    </root>
</configuration>
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileInputStream;
import java.io.IOException;

public class ExceptionLoggingExample {
    private static final Logger logger = LoggerFactory.getLogger(ExceptionLoggingExample.class);

    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("test.txt")) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            logger.error("发生 I/O 异常", e);
        }
    }
}

在上述代码中,使用 SLF4J 的 Logger 记录异常信息,error 方法的第一个参数是描述信息,第二个参数是异常对象,这样可以将详细的异常堆栈信息记录到日志中。

2. 异常转换

有时候,为了更好地适配上层应用的处理逻辑,可以将一种类型的异常转换为另一种类型的异常。例如:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionTranslationExample {
    public static void main(String[] args) {
        try {
            readFile("nonexistentfile.txt");
        } catch (CustomBusinessException e) {
            System.out.println("业务异常: " + e.getMessage());
        }
    }

    public static void readFile(String filePath) throws CustomBusinessException {
        try (FileInputStream fis = new FileInputStream(filePath)) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (FileNotFoundException e) {
            throw new CustomBusinessException("文件未找到,业务无法继续", e);
        } catch (IOException e) {
            throw new CustomBusinessException("读取文件时发生错误", e);
        }
    }

    static class CustomBusinessException extends RuntimeException {
        public CustomBusinessException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}

readFile 方法中,将 FileNotFoundExceptionIOException 转换为自定义的 CustomBusinessException,上层调用者可以统一处理这种业务相关的异常,而不需要关心底层具体的 I/O 异常类型。

3. 异常传播

在某些情况下,方法内部可能不适合处理异常,而是将异常传播给调用者,让调用者来决定如何处理。例如:

import java.io.FileInputStream;
import java.io.IOException;

public class ExceptionPropagationExample {
    public static void main(String[] args) {
        try {
            readFile("test.txt");
        } catch (IOException e) {
            System.out.println("在 main 方法中处理 I/O 异常: " + e.getMessage());
        }
    }

    public static void readFile(String filePath) throws IOException {
        try (FileInputStream fis = new FileInputStream(filePath)) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
        }
    }
}

readFile 方法中,不使用 try - catch 块处理 IOException,而是通过方法声明 throws IOException 将异常传播给调用者 main 方法,main 方法再进行异常处理。

异常处理的最佳实践

1. 精确捕获异常

避免捕获过于宽泛的异常类型,如 Exception。应该尽量捕获具体的异常类型,这样可以更精确地处理不同的异常情况,并且有助于调试。例如:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class PreciseExceptionCatchingExample {
    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("nonexistentfile.txt");
        } catch (FileNotFoundException e) {
            System.out.println("文件未找到,可能需要创建文件: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("其他 I/O 错误: " + e.getMessage());
        }
    }
}

在这个例子中,分别捕获 FileNotFoundExceptionIOException,针对 FileNotFoundException 可以给出创建文件的提示,而不是统一当作一般的 IOException 处理。

2. 避免空 catch 块

空的 catch 块会忽略异常,使得问题难以发现和调试。例如:

import java.io.FileInputStream;
import java.io.IOException;

// 不推荐的写法
public class EmptyCatchBlockExample {
    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("nonexistentfile.txt");
        } catch (IOException e) {
            // 空 catch 块,忽略异常
        }
    }
}

应该至少记录异常信息,以便后续排查问题:

import java.io.FileInputStream;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggingCatchBlockExample {
    private static final Logger logger = LoggerFactory.getLogger(LoggingCatchBlockExample.class);

    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("nonexistentfile.txt");
        } catch (IOException e) {
            logger.error("发生 I/O 异常", e);
        }
    }
}

3. 合理使用 finally 块或 try - with - resources

始终确保打开的 I/O 资源被正确关闭。在 Java 7 及之后,优先使用 try - with - resources 语句,它能更简洁地处理资源关闭,并且能更好地保证资源一定会被关闭。例如:

import java.io.FileInputStream;
import java.io.IOException;

// 使用 try - with - resources
public class TryWithResourcesBestPracticeExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("test.txt")) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            System.out.println("发生 I/O 异常: " + e.getMessage());
        }
    }
}

4. 异常处理与性能

虽然异常处理是必要的,但频繁地抛出和捕获异常会影响性能。因此,应该尽量在代码逻辑中避免不必要的异常情况。例如,在打开文件之前先检查文件是否存在:

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class PerformanceAwareExample {
    public static void main(String[] args) {
        File file = new File("test.txt");
        if (file.exists() && file.isFile()) {
            try (FileInputStream fis = new FileInputStream(file)) {
                int data;
                while ((data = fis.read()) != -1) {
                    System.out.print((char) data);
                }
            } catch (IOException e) {
                System.out.println("发生 I/O 异常: " + e.getMessage());
            }
        } else {
            System.out.println("文件不存在或不是一个文件");
        }
    }
}

这样可以减少 FileNotFoundException 的抛出,提高程序性能。

5. 异常处理与业务逻辑分离

将异常处理代码与业务逻辑代码清晰地分离,使得代码更易于维护和理解。例如:

import java.io.FileInputStream;
import java.io.IOException;

public class SeparationOfConcernsExample {
    public static void main(String[] args) {
        try {
            processFile("test.txt");
        } catch (IOException e) {
            handleException(e);
        }
    }

    public static void processFile(String filePath) throws IOException {
        try (FileInputStream fis = new FileInputStream(filePath)) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
        }
    }

    public static void handleException(IOException e) {
        System.out.println("处理 I/O 异常: " + e.getMessage());
    }
}

在这个例子中,processFile 方法专注于文件处理的业务逻辑,而 handleException 方法专门处理可能发生的异常,使得代码结构更加清晰。

通过深入理解 Java I/O 异常类型、掌握各种异常处理方式以及遵循最佳实践,可以编写出健壮、可靠且易于维护的 Java I/O 代码。在实际开发中,根据具体的应用场景和需求,灵活运用这些知识,能有效地提高程序的质量和稳定性。