【JavaSE系列】IO流

目录

前言

一、IO流概述

二、IO流体系结构

三、File相关的流

1. FileInputStream

2. FileOutputStream

3. FileReader

4. FileWriter

四、缓冲流

五、转换流

1. InputStreamReader

2. OutputStreamWriter

六、数据流

七、对象流

八、打印流

九、标准输入输出流

十、压缩和解压缩流

十一、字节数组流

1. 基本使用

2. 对象克隆

总结


前言

  在Java编程中,输入输出(I/O)操作是处理数据流的核心部分。无论是从文件读取数据、将数据写入到磁盘,还是通过网络进行通信,都需要使用I/O流来实现。Java提供了丰富的I/O流类库,它们不仅能够高效地处理各种类型的数据,还提供了灵活的方式来装饰和扩展这些基本功能。本篇博客旨在介绍Java I/O流的基础知识,包括其分类、体系结构以及如何使用不同类型的流来完成日常开发中的常见任务。

一、IO流概述

  IO流指的是程序中数据的流动。数据可以从内存流动到硬盘,也可以从硬盘流动到内存。 Java中IO流最基本的作用是完成文件的读和写。

  根据数据流向分为输入流和输出流,输入和输出是相对于内存而言的。 输入流指的是从硬盘到内存(输入又叫做读:read) ;输出流指的是从内存到硬盘(输出又叫做写:write)。根据读写数据形式分为字节流和字符流。字节流指的是一次读取一个字节,适合读取非文本数据,例如图片、声音、视频等文件;字符流指的是一次读取一个字符,只适合读取普通文本,不适合读取二进制文件。

注意:Java的所有IO流中凡是以Stream结尾的都是字节流。凡是以Reader和Writer结尾的都是字符流。

  根据流在IO操作中的作用和实现方式来分类可以分为节点流和处理流。 节点流负责数据源和数据目的地的连接,是IO中最基本的组成部分;处理流对节点流进行装饰/包装,提供更多高级处理操作,方便用户进行数据处理。 

二、IO流体系结构

三、File相关的流

1. FileInputStream

  文件字节输入流,可以读取任何文件。使用FileInputStream读取的文件中有中文时,有可能读取到中文某个汉字的一半,在将byte[]数组转换为String时可能出现乱码问题,因此FileInputStream不太适合读取纯文本。FileInputStream常用方法如下:

方法描述
FileInputStream(String name);构造方法,创建一个文件字节输入流对象,参数是文件的路径
int read();从文件读取一个字节(8个二进制位),返回值读取到的字节本身,如果读不到任何数据返回-1
int read(byte[] b);一次读取多个字节,如果文件内容足够多,则一次最多读取b.length个字节。返回值是读取到字节总数。如果没有读取到任何数据,则返回 -1
int read(byte[] b, int off, int len);读到数据后向byte数组中存放时,从off开始存放,最多读取len个字节。读取不到任何数据则返回 -1
long skip(long n);跳过n个字节
int available();返回流中剩余的估计字节数量
void close()关闭流

  下面我们在某一路径下创建一个1.txt的文件,在其中写入一串英文字符串,如下图所示:

  我们可以使用FileInputStream一个字节一个字节地读取我们写好的txt文件,代码如下:

public void testFileInputStream() throws IOException {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream("D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\1.txt");
        int readByte;
        while ((readByte = fis.read()) != -1) {
            System.out.print((char) readByte);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    } finally {
        if (fis != null) {
            fis.close();
        }
    }
}

  我们也可以每次读取多个字节,代码如下:

public void testFileInputStream2() throws IOException {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream("D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\1.txt");
        byte[] bytes = new byte[fis.available()];
        int readBytes;
        while ((readBytes = fis.read(bytes)) != -1) {
            String str = new String(bytes, 0, readBytes);
            System.out.print(str);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    } finally {
        if (fis != null) {
            fis.close();
        }
    }
}

  两者的运行结果均如下:

2. FileOutputStream

  FileOutputStream 是文件字节输出流。常用方法如下:

方法描述
FileOutputStream(String name);构造方法,创建输出流,先将文件清空,再不断写入
FileOutputStream(String name, boolean append);构造方法,创建输出流,在原文件最后面以追加形式不断写入
write(int b);写一个字节
void write(byte[] b);将字节数组中所有数据全部写出
void write(byte[] b, int off, int len);将字节数组的一部分写出
void close()关闭流
void flush()刷新

  下面是一个示例:

public void testFileOutputStream() {
    FileOutputStream fos = null;
    try {
        fos = new FileOutputStream("D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\2.txt");
        String str = "aBcDeFgH";
        fos.write(str.getBytes());
        fos.flush();
    } catch (IOException e) {
        throw new RuntimeException(e);
    } finally {
        if (fos != null) {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

  运行结果如下图所示:

  下面我们利用上面的FileInputStream和FileOutputStram来实现文件复制,这边我复制的是一个视频文件,具体代码如下所示:

public void testFileOutputStream2() throws FileNotFoundException {
    String uri = "D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\video.mp4"; // 定义资源路径
    FileInputStream fis = new FileInputStream(uri);
    FileOutputStream fos = new FileOutputStream(uri.replace("video", "video_copy"));
    byte[] bytes = new byte[1024]; // 每次读取1KB
    int len;
    try {
        while ((len = fis.read(bytes)) != -1) {;
            fos.write(bytes, 0, len);
        }
        fos.flush();
    } catch (IOException e) {
        throw new RuntimeException(e);
    } finally {
        try {
            fis.close();
            fos.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

  运行结果如下图所示,可以看到拷贝后的文件与拷贝前的文件大小相同,说明拷贝没有问题。

补充:我们在使用流的时候,需要手动关闭流,每次这样比较麻烦,代码结构看起来也比较混乱。Java7提供了一个try-with-resources新特性,可以自动关闭资源(凡是实现了AutoCloseable接口的流都可以使用try-with-resources,都会自动关闭)。try-with-resources语法格式如下:

try (ResourceType resource1 = new ResourceType(...);
     ResourceType2 resource2 = new ResourceType2(...)) {
    // 使用资源的代码
} catch (ExceptionType1 e1) {
    // 异常处理代码
} catch (ExceptionType2 e2) {
    // 另一个异常处理代码
} finally {
    // 可选的 finally 块
}

3. FileReader

  FileReader是文件字符输入流,默认采用UTF-8读取文件,一次读取至少一个字符,与FileInputStream类似,不同的是,FileReader读取的是char,FileInputStream读取的是byte。FileReader常用方法如下所示:

方法描述
FileReader(String fileName);构造方法
int read();读取一个字符
int read(char[] cbuf);读取一个字符数组
int read(char[] cbuf, int off, int len);读取某个区间的字符数组
long skip(long n);跳过n个字符
void close()关闭流

  下面是一个示例代码:

public void testReadFile() throws FileNotFoundException {
    try(FileReader fileReader = new FileReader("D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\_01_File相关的流\\汉字.txt")) {
        char[] chars = new char[2];
        int len;
        while ((len = fileReader.read(chars)) != -1) {
            System.out.print(new String(chars, 0, len));
        }
    } catch (IOException e) {
        System.out.println(e.getMessage());
    }
}

  运行结果如下图所示:

 

4. FileWriter

  FileWriter是文件字符输出流,默认采用UTF-8,用于对普通文本文件进行输出,常用的方法如下所示:

方法描述
FileWriter(String fileName);构造方法
FileWriter(String fileName, boolean append);构造方法
void write(char[] cbuf);写字符数组
void write(char[] cbuf, int off, int len);将字符数组的某个区间写出
void write(String str);写字符串
void write(String str, int off, int len);将字符串的某个区间写出
void flush();刷新
void close();关闭流
Writer append(CharSequence csq, int start, int end);追加文本

  下面是一个使用示例:

public void testFileWriter() {
    try(FileWriter fileWriter = new FileWriter("D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\_01_File相关的流\\文本输出.txt")) {
        fileWriter.write("Hello World!");
        fileWriter.write("I'm a file writer.", 0, 10);
        fileWriter.write("\n人生如戏,喝不喝Java".toCharArray());
        fileWriter.write("\n人生如戏,喝不喝Java".toCharArray(), 0, 5);
        fileWriter.flush();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

  运行结果如下图所示:

补充: 上述读取或者写出文件的时候,我们采用的是绝对路径,除了使用绝对路径,我们还可以使用相对路径和资源文件夹的路径。相对路径指的是从整个项目目录下开始的路径,资源文件夹路径在Maven结构下指的是resources目录下开始的路径,有关这两个路径的使用,如下图所示,可以帮助大家更好地理解:

四、缓冲流

  缓冲流读写速度快,能够提高读写的效率,与上面提到的四种流对应的缓冲流有BufferedInputStream、BufferedOutputStream(适合读写非普通文本文件)、 BufferedReader和BufferedWriter(适合读写普通文本文件)。缓冲流都是处理流/包装流,FileInputStream和FileOutputStream是节点流。

  缓冲流的读写速度快,原理是:在内存中准备了一个缓存。读的时候从缓存中读。写的时候将缓存中的数据一次写出。都是在减少和磁盘的交互次数。如何理解缓冲区?家里盖房子,有一堆砖头要搬在工地100米外,单字节的读取就好比你一个人每次搬一块砖头,从堆砖头的地方搬到工地,这样肯定效率低下。然而聪明的人类会用小推车,每次先搬砖头搬到小车上,再利用小推车运到工地上去,这样你再从小推车上取砖头是不是方便多了呀!这样效率就会大大提高,缓冲流就好比我们的小推车,给数据暂时提供一个可存放的空间。那么,缓冲流的输出效率是如何提高的?在缓冲区中先将字符数据存储起来,当缓冲区达到一定大小或者需要刷新缓冲区时,再将数据一次性输出到目标设备。输入效率是如何提高的? read()方法从缓冲区中读取数据。当缓冲区中的数据不足时,它会自动从底层输入流中读取一定大小的数据,并将数据存储到缓冲区中。大部分情况下,我们调用read()方法时,都是从缓冲区中读取,而不需要和硬盘交互。下面是一个创建缓冲流的示例代码:

public static void main(String[] args) throws IOException {
    FileInputStream fis = new FileInputStream("relative.txt");
    BufferedInputStream bis = new BufferedInputStream(fis);
    System.out.println(bis);
    bis.close();
}

  从上面可以看到,我们关闭流只需要关闭最外层的处理流即可。当关闭处理流时,底层节点流也会关闭。下面,我们将测试一下节点流和缓冲流的效率,进行一下对比。这里,我采用的是拷贝司马相如的《上林赋》,具体代码如下图所示:

public static void main(String[] args) {
    String inputUrl = "D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\_02_缓冲流\\效率测试文本-上林赋.txt";
    String outputUrl = inputUrl.replace("上林赋", "上林赋_copy");
    try(
        FileInputStream fileInputStream = new FileInputStream(inputUrl);
        FileOutputStream fileOutputStream = new FileOutputStream(outputUrl);
        BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream)
    ) {
        /* 使用节点流 */
        long begin = System.currentTimeMillis();
        int readByte;
        while ((readByte = fileInputStream.read()) != -1) {
            fileOutputStream.write(readByte);
        }
        fileOutputStream.flush();
        long end = System.currentTimeMillis();
        System.out.println("使用节点流耗时:" + (end - begin) + "ms");
        /* 使用缓冲流 */
        begin = System.currentTimeMillis();
        while ((readByte = bufferedInputStream.read()) != -1) {
            bufferedOutputStream.write(readByte);
        }
        bufferedOutputStream.flush();
        end = System.currentTimeMillis();
        System.out.println("使用缓冲流耗时:" + (end - begin) + "ms");
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

  运行结果如下图所示,可以看到缓冲流的效率比节点流要高很多。

补充:缓冲流的有两个特有方法(输入流),以下两个方法的作用是允许我们在读取数据流时回退到原来的位置(重复读取数据时用)

方法一:void mark(int readAheadLimit); 标记位置(在Java21版本中,参数无意义。低版本JDK中参数表示在标记处最多可以读取的字符数量,如果你读取的字符数超出的上限值,则调用reset()方法时出现IOException)

方法二:void reset(); 重新回到上一次标记的位置 这两个方法有先后顺序:先mark再reset,另外这两个方法不是在所有流中都能用。有些流中有这个方法,但是不能用。

五、转换流

  转换流主要用来解决编解码中出现的乱码问题。

1. InputStreamReader

  InputStreamReader为转换流,属于字符流,作用是将文件中的字节转换为程序中的字符。转换过程是一个解码的过程,主要用来解决读的乱码问题。那么,乱码问题是如何产生的呢?当我们指定的字符集和文件的字符集不一样时就有可能出现乱码。InputStreamReader常用的构造方法有两个:

  • InputStreamReader(InputStream in, String charsetName) // 指定字符集
  • InputStreamReader(InputStream in) // 采用平台默认字符集

  FileReader是InputStreamReader的子类,而InputStreamReader是包装流,所以FileReader也是包装流。 FileReader的出现简化了代码的编写,以下代码本质上是一样的:

Reader reader = new InputStreamReader(new FileInputStream(“file.txt”)); //采用平台默认字符集
Reader reader = new FileReader(“file.txt”); //采用平台默认字符集

  下面是指定字符集的情况: 

Reader reader = new InputStreamReader(new FileInputStream(“file.txt”), “GBK”);
Reader reader = new FileReader("e:/file1.txt", Charset.forName("GBK"));

2. OutputStreamWriter

  OutputStreamWriter是转换流,属于字符流,作用是将程序中的字符转换为文件中的字节。这个过程是一个编码的过程。OutputStreamWriter常用构造方法有两个:

  • OutputStreamWriter(OutputStream out, String charsetName) // 使用指定的字符集
  • OutputStreamWriter(OutputStream out) //采用平台默认字符集

  与InputStreamReader类似,FileWriter是InputStreamReader的子类,以下代码本质是一样的:

Writer writer = new OutputStreamWriter(new FileOutputStream(“file1.txt”), “GBK”);
Writer writer = new FileWriter(“file1.txt”, Charset.forName(“GBK”));

六、数据流

  有两个类与数据流有关,为DataOutputStream和DataInputStream,这两个流都是包装流,读写数据专用的流。DataOutputStream直接将程序中的数据写入文件,不需要转码,效率高。程序中是什么样子,原封不动的写出去。写完后,文件是打不开的。即使打开也是乱码,文件中直接存储的是二进制。 使用DataOutputStream写的文件,只能使用DataInputStream去读取。并且读取的顺序需要和写入的顺序一致,这样才能保证数据恢复原样。两者的构造方法如下:

  • DataInputStream(InputStream in)
  • DataOutputStream(OutputStream out)

  DataOutputStream写的方法如下有writeByte()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble()、writeBoolean()、writeChar()、writeUTF(String) ;DataOutputStream读的方法有readByte()、readShort()、readInt()、readLong()、readFloat()、readDouble()、readBoolean()、readChar()、readUTF()。下面是一个示例代码:

public static void main(String[] args) throws Exception {

    String path = "D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\_04_数据流\\1.txt";

    DataOutputStream dos = new DataOutputStream(new FileOutputStream(path));
    int a = 10;
    double b = 3.144;
    dos.writeInt(a);
    dos.writeDouble(b);
    dos.flush();
    dos.close();

    DataInputStream dis = new DataInputStream(new FileInputStream(path));
    System.out.println(dis.readInt());
    System.out.println(dis.readDouble());
    dis.close();

}

  运行结果如下图所示: 

 

七、对象流

  ObjectOutputStream和ObjectInputStream这两个流,可以完成对象的序列化和反序列化,两者是包装流,其中,ObjectOutputStream用来完成对象的序列化,ObjectInputStream用来完成对象的反序列化。 序列化(Serial)指的是将Java对象转换为字节序列。反序列化(DeSerial)指的是将字节序列转换为Java对象。参与序列化和反序列化的java对象必须实现实现Serializable接口,编译器会自动给该类添加序列化版本号的属性serialVersionUID。如果某对象没有实现该接口就进行序列化,编译器会报如下错误:

  在java中,是通过“类名 + 序列化版本号”来进行类的区分的。 serialVersionUID实际上是一种安全机制。在反序列化的时候,JVM会去检查存储Java对象的文件中的class的序列化版本号是否和当前Java程序中的class的序列化版本号是否一致。如果一致则可以反序列化。如果不一致则报错。 如果一个类实现了Serializable接口,还是建议将序列化版本号固定死,建议显示的定义出来,原因是:类有可能在开发中升级(改动),升级后会重新编译,如果没有固定死,编译器会重新分配一个新的序列化版本号,导致之前序列化的对象无法反序列化。

显示定义序列化版本号的语法:private static final long serialVersionUID = XXL;

  为了保证显示定义的序列化版本号不会写错,建议使用 @java.io.Serial 注解进行标注。并且使用它还可以帮助我们随机生成序列化版本号。 如果我们希望某个属性不参与序列化,需要使用瞬时关键字transient修饰。下面是一个代码示例:

public void test() throws IOException, ClassNotFoundException {
    String path = "D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\_05_对象流\\object";
    // 写对象
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
    Student stu = new Student("张三", 23);
    oos.writeObject(stu);
    oos.flush();
    oos.close();
    // 读对象
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
    Student object = (Student)ois.readObject();
    System.out.println(object.getAge() + object.getName() + object.getGender());
}

  其中提到的Student类如下图所示:

/**
 * 学生类
 */
public class Student implements Serializable {

    @Serial
    private static final long serialVersionUID = 7826613280278208564L;

    private String name;

    private transient int age;

    private String gender;

    public Student() {}

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

}

  运行结果如下图所示:

更多序列化与反序列化的知识可以参考这篇博文:序列化与反序列化详解_java序列化和反序列化-CSDN博客

八、打印流

  打印流有PrintStream和PrintWriter,其中PrintStream以字节形式打印,PrintWriter以字符形式打印。下面是一个示例代码:

public void test() throws FileNotFoundException {
    String path = "D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\_06_打印流\\info.log";
    PrintStream printStream = new PrintStream(path); // PrintStream是包装流 
    printStream.append("你好");
    printStream.close();
    PrintWriter printWriter = new PrintWriter(path.replace("log", "log1")); // PrintWriter 是包装流 
    printWriter.append("你好abc");
    printWriter.close();
}

  运行结果如下图所示:

九、标准输入输出流

  System.in获取到的InputStream就是一个标准输入流,标准输入流是用来接收用户在控制台上的输入的。标准输入流不需要关闭,它是一个系统级的全局的流,JVM负责最后的关闭。我们可以使用BufferedReader对标准输入流进行包装。这样可以方便的接收用户在控制台上的输入(这种方式太麻烦了,因此JDK中提供了更好用的Scanner)。我们可以修改输入流的方向(System.setIn()),让其指向文件。

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = br.readLine();

  System.out获取到的PrintStream就是一个标准输出流,标准输出流是用来向控制台上输出的。标准输出流不需要关闭,它是一个系统级的全局的流,JVM负责最后的关闭。 我们可以修改输出流的方向(System.setOut())。让其指向文件。

十、压缩和解压缩流

    在IO体系中,有不同的压缩和解压缩流,这里以GZIPOutputStream(压缩)和GZIPInputStream(解压缩)为例,下面是压缩流的示例代码:

public static void main(String[] args) throws IOException {
    String path = "D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\_07_压缩和解压缩流\\待压缩文本.txt";
    FileInputStream fileInputStream = new FileInputStream(path);
    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(new FileOutputStream(path.replace("待压缩文本.txt", "待压缩文本.txt.gz")));
    byte[] buffer = new byte[1024];
    int read;
    while ((read = fileInputStream.read(buffer)) != -1) {
        gzipOutputStream.write(buffer, 0, read);
    }
    gzipOutputStream.finish(); // 在压缩完所有数据之后调用finish()方法,以确保所有未压缩的数据都被刷新到输出流中,并生成必要的 Gzip 结束标记,标志着压缩数据的结束。
    fileInputStream.close();
    gzipOutputStream.close();
}

  运行结果如下,可以看到在指定的目录下生成了一个压缩文件。

  下面是对应的解压缩的代码:

public static void main(String[] args) throws IOException {
    String path ="D:\\Code\\study\\JavaCode\\JavaSEDemo\\base\\src\\main\\java\\cn\\javase\\base\\io\\_07_压缩和解压缩流\\待压缩文本.txt.gz";
    GZIPInputStream gzipInputStream = new GZIPInputStream(new FileInputStream(path));
    FileOutputStream fileOutputStream = new FileOutputStream(path.replace("待压缩文本.txt.gz", "解压缩文本.txt"));
    byte[] buffer = new byte[1024];
    int len;
    while ((len = gzipInputStream.read(buffer)) != -1) {
        fileOutputStream.write(buffer, 0, len);
    }
    gzipInputStream.close();
    fileOutputStream.flush();
    fileOutputStream.close();
}

  运行结果如下,我们压缩后的文件重新解压缩回来了。 

补充:实际上所有的输出流中,只有带有缓冲区的流才需要手动刷新,节点流是不需要手动刷新的,节点流在关闭的时候会自动刷新。

十一、字节数组流

1. 基本使用

  ByteArrayInputStream和ByteArrayOutputStream都是内存操作流,不需要打开和关闭文件等操作。这些流是非常常用的,可以将它们看作开发中的常用工具,能够方便地读写字节数组、图像数据等内存中的数据。 ByteArrayInputStream和ByteArrayOutputStream都是节点流。 ByteArrayOutputStream,将数据写入到内存中的字节数组当中;ByteArrayInputStream,读取内存中某个字节数组中的数据。下面是一个示例代码:

public void test2() throws IOException {
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 是节点流,默认byte数组大小为32
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
    objectOutputStream.writeLong(3000L);
    objectOutputStream.writeBoolean(true);
    objectOutputStream.writeBoolean(false);
    objectOutputStream.writeUTF("人生如戏");
    objectOutputStream.flush();
    objectOutputStream.close();
    byte[] byteArray = byteArrayOutputStream.toByteArray(); // 转为byte数组
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
    ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
    System.out.println(objectInputStream.readLong());
    System.out.println(objectInputStream.readBoolean());
    System.out.println(objectInputStream.readBoolean());
    System.out.println(objectInputStream.readUTF());
    objectInputStream.close();
}

  运行结果如下图所示:

 

2. 对象克隆

对象克隆参考博文:Java中对象的克隆_java 对象克隆-CSDN博客

  我们除了重写clone()方法来完成对象的深克隆,也使用字节数组流也可以完成对象的深克隆。 原理是将要克隆的Java对象写到内存中的字节数组中,再从内存中的字节数组中读取对象,读取到的对象就是一个深克隆。 下面是一个示例代码:

public static void main(String[] args) throws IOException, ClassNotFoundException {
    User user = new User("张三", 23);
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(user);
    oos.flush();
    oos.close();
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);
    User user1 = (User) ois.readObject();
    user1.setName("李四");
    user1.setAge(11);
    System.out.println(user.getAge() + user.getName());
    System.out.println(user1.getAge() + user1.getName());
    ois.close();
}

  运行结果如下图所示,修改克隆后对象中的值,原来的对象中的值没有改变,说明这是一个深克隆。

 

小结一下对象克隆的方法:

  • 调用Object的clone方法,默认是浅克隆,需要深克隆的话,就需要重写clone方法。
  • 可以通过序列化和反序列化完成对象的克隆。
  • 可以通过ByteArrayInputStream和ByteArrayOutputStream完成深克隆。

总结

  通过本文的详细介绍,我们对Java I/O流有了更深入的理解。从最基本的节点流到复杂的处理流,Java为我们提供了一套强大而灵活的工具集,使得我们可以轻松地处理各种数据流。无论是字节流还是字符流,缓冲流或是转换流,每种流都有其独特的用途和优势。掌握这些知识点不仅可以帮助我们在日常开发中更加得心应手,还能让我们在面对复杂问题时游刃有余。希望这篇博客能成为你学习Java I/O流的一个良好起点,并激发你进一步探索这一领域的兴趣。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

边城仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值