Java基础 (五:异常处理,IO流、文件操作、序列化)

30 阅读9分钟

一、Java异常处理

Java异常处理是Java语言中用于处理程序运行时错误的机制,它允许程序在遇到错误时优雅地处理而不是直接崩溃。

1. 异常分类

Java异常主要分为三大类:

  • 检查异常(Checked Exception):编译时必须处理的异常,如 IOExceptionSQLException
  • 运行时异常(Runtime Exception):程序运行过程中可能出现的异常,如 NullPointerExceptionArrayIndexOutOfBoundsException
  • 错误(Error):系统级别的严重问题,如 OutOfMemoryErrorStackOverflowError

2. 异常处理语法

try-catch-finally结构

try {
    // 可能抛出异常的代码
    int result = 10 / 0;
} catch (ArithmeticException e) {
    // 处理特定异常
    System.out.println("除零异常: " + e.getMessage());
} catch (Exception e) {
    // 处理其他异常
    System.out.println("其他异常: " + e.getMessage());
} finally {
    // 无论是否有异常都会执行
    System.out.println("清理资源");
}

try-with-resources语句

这段代码演示了 Java 7 引入的 try-with-resources 语法特性,它是一种自动资源管理机制。

// 自动关闭资源
try (FileInputStream fis = new FileInputStream("test.txt");
     BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
    
    String line = reader.readLine();
    System.out.println(line);
    
} catch (IOException e) {
    System.out.println("文件操作异常: " + e.getMessage());
}
// 资源会自动关闭,无需手动调用close()
  • 在 try 后面的括号内声明并初始化资源
  • FileInputStream fis = new FileInputStream("test.txt"):创建文件输入流
  • BufferedReader reader = new BufferedReader(new InputStreamReader(fis)):创建带缓冲的读取器
  • 当 try 代码块执行完毕后(无论是正常结束还是发生异常)
  • JVM 会自动调用所有在 try 括号中声明的资源的 close() 方法
  • 无需手动编写 fis.close() 或 reader.close()
  • 只有实现了 AutoCloseable 或 Closeable 接口的类才能在 try-with-resources 中使用 FileInputStream 和 BufferedReader 都实现了 Closeable 接口

3. 抛出异常

throw关键字

public void validateAge(int age) throws IllegalArgumentException {
    if (age < 0) {
        throw new IllegalArgumentException("年龄不能为负数");
    }
    if (age > 150) {
        throw new IllegalArgumentException("年龄不能超过150");
    }
}

throws声明

// 声明可能抛出的检查异常
public void readFile(String filename) throws IOException {
    FileReader file = new FileReader(filename);
    // 文件操作代码...
}

4. 自定义异常

// 自定义检查异常
public class InsufficientFundsException extends Exception {
    private double amount;
    
    public InsufficientFundsException(double amount) {
        super("余额不足,缺少金额: " + amount);
        this.amount = amount;
    }
    
    public double getAmount() {
        return amount;
    }
}

// 自定义运行时异常
public class InvalidAccountException extends RuntimeException {
    public InvalidAccountException(String message) {
        super(message);
    }
}

5. 异常处理最佳实践

多重异常捕获

try {
    // 可能抛出多种异常的代码
    processData();
} catch (IOException | SQLException e) {
    // 同时处理多种相似异常
    logger.error("数据处理失败", e);
    throw new ServiceException("服务暂时不可用");
}

异常链

public void processFile(String filename) throws FileProcessException {
    try {
        // 文件处理逻辑
        readFile(filename);
    } catch (IOException e) {
        // 保留原始异常信息
        throw new FileProcessException("文件处理失败: " + filename, e);
    }
}

资源管理示例

public class ResourceManager {
    public String readFileContent(String filePath) {
        try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath))) {
            return reader.lines()
                        .collect(Collectors.joining("\n"));
        } catch (NoSuchFileException e) {
            System.err.println("文件不存在: " + filePath);
            return "";
        } catch (IOException e) {
            System.err.println("读取文件失败: " + e.getMessage());
            return "";
        }
    }
}

6. 异常处理原则

  • 具体异常优先:先捕获具体的异常类型,再捕获通用异常
  • 避免空catch块:至少记录异常信息
  • 合理使用finally:用于释放资源和清理工作
  • 异常信息要明确:提供有用的错误信息帮助调试
  • 不要忽略检查异常:必须显式处理或声明抛出

二、Java IO流

Java IO流是Java中用于处理输入输出操作的核心机制,它提供了一种统一的方式来读取和写入数据。IO流将数据的传输抽象为流的形式,使得程序可以像处理水流一样处理数据。

1. 流的分类

按数据流向分类
  • 输入流InputStreamReader
  • 输出流OutputStreamWriter
按处理数据单位分类
  • 字节流:处理二进制数据

    • 基类:InputStreamOutputStream
    • 常用实现类:FileInputStreamFileOutputStreamByteArrayInputStreamByteArrayOutputStream
  • 字符流:处理字符数据

    • 基类:ReaderWriter
    • 常用实现类:FileReaderFileWriterStringReaderStringWriter

2. 常用IO流类详解

字节流示例
// FileInputStream示例
try (FileInputStream fis = new FileInputStream("input.txt")) {
    int data;
    // 逐字节读取输入流中的数据
    while ((data = fis.read()) != -1) {
        System.out.print((char) data);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// FileOutputStream示例
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
    String content = "Hello World";
    fos.write(content.getBytes());
} catch (IOException e) {
    e.printStackTrace();
}
字符流示例
// FileReader示例
try (FileReader fr = new FileReader("input.txt")) {
    int ch;
    // 逐字符读取输入流中的数据
    while ((ch = fr.read()) != -1) {
        System.out.print((char) ch);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// FileWriter示例
try (FileWriter fw = new FileWriter("output.txt")) {
    fw.write("Hello World");
} catch (IOException e) {
    e.printStackTrace();
}
缓冲流示例

缓冲流(Buffered Stream)是Java IO中的一种处理流,它通过在内存中创建缓冲区来提高IO操作的效率。

// BufferedInputStream示例
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) {
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = bis.read(buffer)) != -1) {
        System.out.write(buffer, 0, bytesRead);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// BufferedWriter示例
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    bw.write("Hello World");
    bw.newLine();
    bw.write("This is a new line");
} catch (IOException e) {
    e.printStackTrace();
}

三、文件操作

1. File类操作

文件和目录基本操作
// 创建File对象
File file = new File("example.txt");
File dir = new File("exampleDir");

// 文件操作
if (!file.exists()) {
    try {
        file.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// 目录操作
if (!dir.exists()) {
    dir.mkdir(); // 创建单级目录
    // dir.mkdirs(); // 创建多级目录
}

// 文件信息获取
System.out.println("文件名: " + file.getName());
System.out.println("文件路径: " + file.getAbsolutePath());
System.out.println("文件大小: " + file.length() + " bytes");
System.out.println("是否为文件: " + file.isFile());
System.out.println("是否为目录: " + file.isDirectory());
文件列表操作
// 列出目录下的文件
File directory = new File(".");
File[] files = directory.listFiles();
if (files != null) {
    for (File f : files) {
        System.out.println(f.getName() + (f.isDirectory() ? " [目录]" : " [文件]"));
    }
}

// 使用文件过滤器
File[] txtFiles = directory.listFiles((dir, name) -> name.endsWith(".txt"));

2. NIO.2文件操作(Path和Files)

NIO.2(New I/O 2)是Java 7引入的文件I/O API,提供了更现代化和功能丰富的文件操作方式。使用 Path 接口替代传统的 File 类,通过Files工具类提供更灵活的路径处理能力,Files 方法内部封装了流的创建和管理, 可以异步进行文件读写。一般开发都是用这个。

// 传统方式:手动管理流
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line = reader.readLine();
}

// Files方式:简化操作
List<String> lines = Files.readAllLines(path);

Path和Files类操作
// Path操作
Path path = Paths.get("example.txt");
Path absolutePath = path.toAbsolutePath();
System.out.println("绝对路径: " + absolutePath);

// Files类操作
try {
    // 读取文件所有行
    List<String> lines = Files.readAllLines(path);
    lines.forEach(System.out::println);
    
    // 写入文件
    List<String> content = Arrays.asList("第一行", "第二行", "第三行");
    Files.write(Paths.get("output.txt"), content, StandardCharsets.UTF_8);
    
    // 复制文件
    Files.copy(Paths.get("source.txt"), Paths.get("destination.txt"), 
               StandardCopyOption.REPLACE_EXISTING);
               
    // 删除文件
    Files.deleteIfExists(Paths.get("temp.txt"));
    
} catch (IOException e) {
    e.printStackTrace();
}

Path类速查

2. 创建 Path 对象
  • Paths.get(String first, String... more): 通过字符串创建路径
  • Paths.get(URI uri): 通过 URI 创建路径
  • FileSystem.getPath(String first, String... more): 通过文件系统创建
路径操作
  • Path resolve(Path other): 解析相对路径
  • Path resolve(String other): 解析字符串路径
  • Path relativize(Path other): 计算相对路径
  • Path normalize(): 规范化路径
  • Path getParent(): 获取父路径
  • Path getFileName(): 获取文件名
  • Path getName(int index): 获取指定索引的名称
路径查询
  • boolean isAbsolute(): 判断是否为绝对路径
  • Path toAbsolutePath(): 转换为绝对路径
  • int getNameCount(): 获取路径元素数量
  • Iterator<Path> iterator(): 获取路径迭代器
文件系统操作
  • FileSystem getFileSystem(): 获取关联的文件系统
  • URI toUri(): 转换为 URI
  • File toFile(): 转换为传统 File 对象(如果适用)
与其他组件协作
  • Files 类配合进行文件操作
  • 可作为 InputStream/OutputStream 等流的源或目标
  • 支持与传统 File 类相互转换

Files类速查

1. 文件创建和删除
  • createFile(Path path, FileAttribute<?>... attrs): 创建新文件
  • createDirectory(Path dir, FileAttribute<?>... attrs): 创建目录
  • createDirectories(Path dir, FileAttribute<?>... attrs): 创建目录及其所有父目录
  • delete(Path path): 删除文件或目录
  • deleteIfExists(Path path): 如果文件存在则删除
2. 文件复制和移动
  • copy(Path source, Path target, CopyOption... options): 复制文件
  • move(Path source, Path target, CopyOption... options): 移动或重命名文件
3. 文件读写
  • readString(Path path): 读取文件内容为字符串
  • writeString(Path path, CharSequence csq, OpenOption... options): 将字符串写入文件
  • readAllLines(Path path): 读取文件所有行
  • readAllBytes(Path path): 读取文件所有字节
  • write(Path path, byte[] bytes, OpenOption... options): 写入字节数组到文件
4. 文件属性操作
  • exists(Path path, LinkOption... options): 检查文件是否存在
  • isDirectory(Path path, LinkOption... options): 检查是否为目录
  • isRegularFile(Path path, LinkOption... options): 检查是否为普通文件
  • size(Path path): 获取文件大小
  • getLastModifiedTime(Path path): 获取最后修改时间
  • setLastModifiedTime(Path path, FileTime time): 设置最后修改时间
5. 目录遍历
  • list(Path dir): 列出目录内容
  • walk(Path start, FileVisitOption... options): 遍历文件树
  • find(Path start, int maxDepth, BiPredicate<Path, BasicFileAttributes> matcher, FileVisitOption... options): 查找匹配的文件
6. 文件权限和属性
  • getPosixFilePermissions(Path path, LinkOption... options): 获取POSIX文件权限
  • setPosixFilePermissions(Path path, Set<PosixFilePermission> perms): 设置POSIX文件权限
  • getAttribute(Path path, String attribute, LinkOption... options): 获取文件属性
  • setAttribute(Path path, String attribute, Object value, LinkOption... options): 设置文件属性

四、 序列化

序列化(Serialization)是将对象转换为字节流的过程,以便于存储或传输。反序列化(Deserialization)则是将字节流还原为对象的过程。

1. 基本序列化

实现Serializable接口
import java.io.*;

// 可序列化的类
public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String name;
    private int age;
    private transient String password; // 不会被序列化
    
    public Person(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }
    
    // getter和setter方法...
    
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", password='" + password + "'}";
    }
}
序列化和反序列化示例
// 序列化对象到文件
public static void serializeObject(Object obj, String filename) {
    try (ObjectOutputStream oos = new ObjectOutputStream(
            new FileOutputStream(filename))) {
        oos.writeObject(obj);
        System.out.println("对象已序列化到文件: " + filename);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// 从文件反序列化对象
public static Object deserializeObject(String filename) {
    try (ObjectInputStream ois = new ObjectInputStream(
            new FileInputStream(filename))) {
        Object obj = ois.readObject();
        System.out.println("对象已从文件反序列化: " + filename);
        return obj;
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
        return null;
    }
}

// 使用示例
Person person = new Person("张三", 25, "secret123");
serializeObject(person, "person.ser");

Person deserializedPerson = (Person) deserializeObject("person.ser");
System.out.println(deserializedPerson);

2. 自定义序列化

实现自定义序列化方法
public class CustomSerializableClass implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String name;
    private int age;
    private String sensitiveData;
    
    public CustomSerializableClass(String name, int age, String sensitiveData) {
        this.name = name;
        this.age = age;
        this.sensitiveData = sensitiveData;
    }
    
    // 自定义序列化方法
    private void writeObject(ObjectOutputStream oos) throws IOException {
        // 执行默认序列化
        oos.defaultWriteObject();
        
        // 自定义序列化逻辑 - 加密敏感数据
        String encryptedData = encrypt(sensitiveData);
        oos.writeObject(encryptedData);
    }
    
    // 自定义反序列化方法
    private void readObject(ObjectInputStream ois) 
            throws IOException, ClassNotFoundException {
        // 执行默认反序列化
        ois.defaultReadObject();
        
        // 自定义反序列化逻辑 - 解密敏感数据
        String encryptedData = (String) ois.readObject();
        this.sensitiveData = decrypt(encryptedData);
    }
    
    private String encrypt(String data) {
        // 简单加密实现
        return new StringBuilder(data).reverse().toString();
    }
    
    private String decrypt(String data) {
        // 简单解密实现
        return new StringBuilder(data).reverse().toString();
    }
}

3. Externalizable接口

实现Externalizable接口
public class ExternalizableExample implements Externalizable {
    private String name;
    private int age;
    
    // 必须有无参构造函数
    public ExternalizableExample() {}
    
    public ExternalizableExample(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }
    
    @Override
    public void readExternal(ObjectInput in) 
            throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = in.readInt();
    }
    
    @Override
    public String toString() {
        return "ExternalizableExample{name='" + name + "', age=" + age + "}";
    }
}

最佳实践

1. IO流使用建议

  • 使用try-with-resources确保资源正确关闭
  • 对于大量数据操作,使用缓冲流提高性能
  • 根据数据类型选择合适的流(字节流vs字符流)

2. 文件操作建议

  • 使用NIO.2 API进行现代文件操作
  • 注意处理文件路径分隔符兼容性
  • 检查文件操作权限和异常处理

3. 序列化建议

  • 显式声明serialVersionUID
  • 对敏感数据使用transient关键字
  • 考虑使用JSON等格式替代Java原生序列化
  • 注意序列化版本兼容性问题