java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java IO流文件复制、文本读写、对象序列化

Java IO流实战之文件复制、文本读写、对象序列化详细解析

作者:梵得儿SHI

Java IO提供了对象序列化机制,可以将对象转换为字节流,并从字节流还原对象,这篇文章主要介绍了Java IO流实战之文件复制、文本读写、对象序列化的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

IO 流是 Java 操作数据传输的核心工具,从简单的文件复制到复杂的对象持久化,都离不开 IO 流的灵活运用。本文将通过三个实战场景 ——文件复制文本读写对象序列化存储,带你从原理到代码彻底掌握 IO 流的实战技巧,每个场景都配备直观图示和优化方案,让你在实际开发中少走弯路。

一、实战场景一:文件复制 —— 字节流的核心应用

文件复制是 IO 流最基础也最常用的场景,无论是图片、视频还是文档,本质上都是字节数据的传输。我们需要解决的核心问题是:如何高效地将源文件的字节数据传输到目标文件

1.1 基础实现:字节流直接读写

最原始的文件复制可以通过FileInputStream(字节输入流)和FileOutputStream(字节输出流)实现,原理是逐字节或按字节数组读取源文件,再写入目标文件。

public class FileCopyBasic {
    public static void main(String[] args) {
        // 源文件和目标文件路径
        String sourcePath = "source.jpg";
        String targetPath = "target_basic.jpg";
        
        // 声明流对象(try-with-resources语法自动关闭资源)
        try (FileInputStream fis = new FileInputStream(sourcePath);
             FileOutputStream fos = new FileOutputStream(targetPath)) {
            
            byte[] buffer = new byte[1024]; // 缓冲区(一次读1024字节)
            int len; // 实际读取的字节数
            
            // 循环读取:当len=-1时表示读取完毕
            while ((len = fis.read(buffer)) != -1) {
                // 写入缓冲区中的有效数据(避免写入空字节)
                fos.write(buffer, 0, len);
            }
            System.out.println("基础字节流复制完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

原理图示:字节流通过缓冲区批量传输数据,减少直接操作磁盘的次数(直接逐字节读写效率极低,必须用数组缓冲)。

1.2 优化方案:缓冲流提升效率

基础字节流虽然能完成复制,但read()write()仍会频繁触发系统调用。BufferedInputStreamBufferedOutputStream通过内置缓冲区(默认 8KB)进一步减少 IO 次数,效率更高。

public class FileCopyWithBuffer {
    public static void main(String[] args) {
        String sourcePath = "source.jpg";
        String targetPath = "target_buffer.jpg";
        
        // 缓冲流包装基础字节流
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourcePath));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(targetPath))) {
            
            byte[] buffer = new byte[1024];
            int len;
            while ((len = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
                // 缓冲流会自动刷新,大文件可手动调用bos.flush()避免数据滞留
            }
            System.out.println("缓冲流复制完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

为什么缓冲流更快?基础字节流的read()每次都从磁盘读数据,而缓冲流会一次性读取大量数据到内置缓冲区,后续read()直接从内存缓冲区获取,大幅减少磁盘 IO 次数(磁盘 IO 速度远低于内存)。

1.3 进阶方案:NIO 的零拷贝复制

Java NIO 的FileChannel提供了transferTo()方法,支持零拷贝(数据直接从内核缓冲区传输到目标文件,不经过用户态),是大文件复制的最优选择。

public class FileCopyWithChannel {
    public static void main(String[] args) {
        String sourcePath = "source.mp4";
        String targetPath = "target_channel.mp4";
        
        try (FileChannel inChannel = new FileInputStream(sourcePath).getChannel();
             FileChannel outChannel = new FileOutputStream(targetPath).getChannel()) {
            
            // 传输数据:从输入通道到输出通道,每次最多传输Integer.MAX_VALUE字节
            long position = 0;
            long size = inChannel.size();
            while (position < size) {
                position += inChannel.transferTo(position, Integer.MAX_VALUE, outChannel);
            }
            System.out.println("NIO通道复制完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

三种方案对比

实现方式核心类优点缺点适用场景
基础字节流FileInputStream简单直接效率低(频繁 IO)小文件、简单场景
缓冲字节流BufferedInputStream内置缓冲区,效率较高仍有用户态 / 内核态切换中大型文件
NIO 通道FileChannel零拷贝,效率最高代码稍复杂超大文件、性能敏感场景

二、实战场景二:文本读写 —— 字符流与编码处理

文本文件(如.txt、.java)由字符组成,直接用字节流读写可能因编码问题导致乱码。字符流(Reader/Writer)专门处理字符数据,能自动完成字节与字符的转换。

2.1 基础字符流:FileReader/FileWriter

   FileReaderFileWriter是字符流的基础实现,默认使用系统编码(可能导致跨平台乱码),适合简单场景。

public class TextReadWriteBasic {
    public static void main(String[] args) {
        String sourceTxt = "source.txt";
        String targetTxt = "target_basic.txt";
        
        // 字符流读写文本
        try (FileReader fr = new FileReader(sourceTxt);
             FileWriter fw = new FileWriter(targetTxt)) {
            
            char[] buffer = new char[1024]; // 字符缓冲区
            int len;
            while ((len = fr.read(buffer)) != -1) {
                fw.write(buffer, 0, len);
            }
            System.out.println("基础字符流读写完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.2 解决编码问题:InputStreamReader/OutputStreamWriter

当文本文件编码(如 UTF-8)与系统默认编码不一致时,必须用InputStreamReaderOutputStreamWriter指定编码,避免乱码。

public class TextReadWriteWithCharset {
    public static void main(String[] args) {
        String sourceTxt = "source_utf8.txt";
        String targetTxt = "target_utf8.txt";
        String charset = "UTF-8"; // 明确指定编码
        
        // 字节流→字符流(指定编码)
        try (InputStreamReader isr = new InputStreamReader(
                 new FileInputStream(sourceTxt), charset);
             OutputStreamWriter osw = new OutputStreamWriter(
                 new FileOutputStream(targetTxt), charset)) {
            
            char[] buffer = new char[1024];
            int len;
            while ((len = isr.read(buffer)) != -1) {
                osw.write(buffer, 0, len);
            }
            System.out.println("指定编码的字符流读写完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

编码转换原理:文本文件存储的是字节(如 UTF-8 编码的汉字占 3 字节),InputStreamReader按指定编码将字节转换为字符,OutputStreamWriter再将字符转换为目标编码的字节。

2.3 高效文本处理:缓冲字符流与特殊操作

BufferedReaderBufferedWriter是字符流的缓冲增强版,提供readLine()(逐行读取)和newLine()(跨平台换行)等实用方法,是文本处理的首选。

public class TextReadWriteWithBuffer {
    public static void main(String[] args) {
        String sourceTxt = "article.txt";
        String targetTxt = "article_copy.txt";
        
        try (BufferedReader br = new BufferedReader(
                 new InputStreamReader(new FileInputStream(sourceTxt), "UTF-8"));
             BufferedWriter bw = new BufferedWriter(
                 new OutputStreamWriter(new FileOutputStream(targetTxt), "UTF-8"))) {
            
            String line; // 存储每行内容
            // 逐行读取(readLine()不包含换行符)
            while ((line = br.readLine()) != null) {
                bw.write(line); // 写入一行内容
                bw.newLine(); // 换行(自动适配系统换行符:\n或\r\n)
            }
            System.out.println("缓冲字符流逐行读写完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

关键技巧

  • readLine()适合处理按行结构化的文本(如日志、CSV);
  • newLine()比硬编码\n更通用,避免跨平台问题;
  • 大文件读写时,缓冲字符流的效率远高于基础字符流。

三、实战场景三:对象序列化 —— 对象的持久化存储

序列化是将对象转换为字节序列以便存储或传输的过程,反序列化则是将字节序列恢复为对象。Java 通过Serializable接口和对象流(ObjectInputStream/ObjectOutputStream)实现这一功能。

3.1 序列化的基本实现

步骤 1:定义可序列化的类(实现Serializable接口)

import java.io.Serializable;
import java.util.Date;

// 实现Serializable接口(标记接口,无方法)
public class User implements Serializable {
    // 序列化版本号(强烈建议显式声明,避免类结构变化导致反序列化失败)
    private static final long serialVersionUID = 1L;
    
    private String username;
    private int age;
    private transient String password; // transient修饰的字段不参与序列化
    private Date registerTime;
    
    // 构造器、getter、setter、toString()
    public User(String username, int age, String password) {
        this.username = username;
        this.age = age;
        this.password = password;
        this.registerTime = new Date();
    }
    
    @Override
    public String toString() {
        return "User{username='" + username + "', age=" + age + 
               ", password='" + password + "', registerTime=" + registerTime + "}";
    }
}

步骤 2:使用对象流进行序列化和反序列化

import java.io.*;

public class ObjectSerialization {
    public static void main(String[] args) {
        String filePath = "user.ser";
        User user = new User("zhangsan", 25, "123456");
        
        // 1. 序列化:将对象写入文件
        try (ObjectOutputStream oos = new ObjectOutputStream(
                 new FileOutputStream(filePath))) {
            oos.writeObject(user);
            System.out.println("序列化完成:" + user);
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 2. 反序列化:从文件恢复对象
        try (ObjectInputStream ois = new ObjectInputStream(
                 new FileInputStream(filePath))) {
            User deserializedUser = (User) ois.readObject();
            System.out.println("反序列化结果:" + deserializedUser);
            // 注意:password为null(transient字段未被序列化)
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出结果

序列化完成:User{username='zhangsan', age=25, password='123456', registerTime=...}
反序列化结果:User{username='zhangsan', age=25, password='null', registerTime=...}

3.2 序列化的核心知识点

  1. Serializable 接口:是一个标记接口(无任何方法),仅用于告诉 JVM 该类可以被序列化。若类未实现此接口,序列化时会抛出NotSerializableException

  2. serialVersionUID 的作用:用于验证序列化和反序列化的类版本是否一致。若不显式声明,JVM 会根据类结构自动生成,类结构(如增减字段)变化会导致版本号改变,反序列化失败。建议所有可序列化类显式声明此常量

  3. transient 关键字:被transient修饰的字段不参与序列化,反序列化时会被赋予默认值(如 null、0)。适合存储敏感信息(如密码)或无需持久化的临时数据。

  4. 父类序列化规则:若父类未实现Serializable,则父类必须有默认无参构造器,否则反序列化时会报错(无法初始化父类字段)。

序列化流程图示

3.3 序列化的应用场景与限制

应用场景

  • 对象持久化(如存储到文件、数据库 BLOB 字段);
  • 网络传输(如 RPC 框架中对象的跨服务传输);
  • 深拷贝(通过序列化 + 反序列化实现对象的完全复制)。

限制

  • 不能序列化静态字段(静态字段属于类,不属于对象);
  • 循环引用的对象可序列化(JVM 会处理引用关系);
  • 序列化后的字节流与 JVM 相关,跨语言兼容性差(可考虑 JSON、Protobuf 等跨语言格式)。

四、IO 流实战总结与最佳实践

通过三个场景的实战,我们可以总结出 IO 流使用的核心原则:

  1. 选择合适的流类型

    • 字节流:处理非文本文件(图片、视频、二进制数据);
    • 字符流:处理文本文件(需注意编码);
    • 对象流:处理对象的序列化 / 反序列化。
  2. 优先使用缓冲流:缓冲流(BufferedXXX)通过内置缓冲区减少 IO 次数,效率远高于基础流,几乎所有场景都应优先使用。

  3. 资源管理必须严谨:始终使用try-with-resources 语法(自动关闭资源),避免流未关闭导致的资源泄漏(尤其是文件流和网络流)。

  4. 关注编码问题:文本处理时必须明确指定编码(如 UTF-8),避免依赖系统默认编码导致的乱码。

  5. 大文件优化:大文件复制优先用 NIO 的FileChannel.transferTo(),利用零拷贝提升性能;大文件读写避免一次性加载到内存,应分块处理。

IO 流是 Java 开发的基础技能,掌握这些实战技巧不仅能解决日常开发问题,更能帮助你理解 IO 操作的底层原理。希望本文的三个实战场景能让你对 IO 流的运用更加得心应手,欢迎在评论区分享你的实战经验!

总结 

到此这篇关于Java IO流实战之文件复制、文本读写、对象序列化详细解析的文章就介绍到这了,更多相关Java IO流文件复制、文本读写、对象序列化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文