JAVA基础深化提高(七) | IO流技术

一、IO流技术介绍

1. 什么是IO?

输入(Input)指的是:可以让程序从外部系统获得数据(核心含义是“读”,读取外部数据)。

输出(Output)指的是:程序输出数据给外部系统从而可以操作外部系统(核心含义是“写”,将数据写出到外部系统)。

java.io包为我们提供了相关的API,实现了对所有外部系统的输入输出操作,这就是我们这章所要学习的技术。

2. 什么是数据源?

数据源data source,提供数据的原始媒介。常见的数据源有:数据库、文件、其他程序、内存、网络连接、IO设备。如图所示。
在这里插入图片描述
数据源分为:源设备、目标设备

  1. 源设备:为程序提供数据,一般对应输入流。
  2. 目标设备:程序数据的目的地,一般对应输出流。

3. 流的概念

流是一个抽象、动态的概念,是一连串连续动态的数据集合。

对于输入流而言,数据源就像水箱,流(stream)就像水管中流动着的水流,程序就是我们最终的用户。我们通过流(A Stream)将数据源(Source)中的数据(information)输送到程序(Program)中。

对于输出流而言,目标数据源就是目的地(dest),我们通过流(A Stream)将程序(Program)中的数据(information)输送到目的数据源(dest)中。

流与源数据源和目标数据源之间的关系:
在这里插入图片描述

4. IO流的经典写法

     import java.io.*;
        public class Test2 {
            public static void main(String[] args) {
                FileInputStream fis = null;
                try {
                    fis = new FileInputStream("d:/a.txt"); // 内容是:abc
                    StringBuilder sb = new StringBuilder();
                    int temp = 0;
                    //当temp等于-1时,表示已经到了文件结尾,停止读取
                    while ((temp = fis.read()) != -1) {
                        sb.append((char) temp);
                    }
                    System.out.println(sb);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        //这种写法,保证了即使遇到异常情况,也会关闭流对象。
                        if (fis != null) {
                            fis.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

5. IO流新语法经典写法

在JDK7以及以后的版本中可以使用try-with-resource语法更优雅的关闭资源。

java.lang.AutoCloseable接口:

在java.lang.AutoCloseable接口中包含了一个close方法,该方法用于关闭资源。

只要是实现了java.lang.AutoCloseable接口的对象,都可以使用try-with-resource关闭资源。

 	 public class Test3 {
            public static void main(String[] args) {
                //使用try-with-resource方式关闭资源。
                //在try中打开资源,不需要在代码中添加finally块关闭资源。
                try(FileInputStream fis = new FileInputStream("d:/a.txt");){
                    StringBuilder sb = new StringBuilder();
                    int temp=0;
                    while((temp = fis.read()) != -1)
                    {
                        sb.append((char) temp);
                    }
                    System.out.println(sb);
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        }

6. Java中流的概念细分

在这里插入图片描述

6.1 按流的方向分类

  • 输入流:数据流向是数据源到程序(以InputStream、Reader结尾的流)。
  • 输出流:数据流向是程序到目的地(以OutPutStream、Writer结尾的流)。

6.2 按处理的数据单元分类

  • 字节流:以字节为单位获取数据,命名上以Stream结尾的流一般是字节流,如FileInputStream、FileOutputStream。
  • 字符流:以字符为单位获取数据,命名上以Reader/Writer结尾的流一般是字符流,如FileReader、FileWriter。

6.3 按处理对象不同分类

  • 节点流:可以直接从数据源或目的地读写数据,如FileInputStream、FileReader、DataInputStream等。
  • 处理流:不直接连接到数据源或目的地,是”处理流的流”。通过对其他流的处理提高程序的性能,如BufferedInputStream、BufferedReader等。

节点流处于IO操作的第一线,所有操作必须通过它们进行;处理流可以对节点流进行包装,提高性能或提高程序的灵活性。
在这里插入图片描述

7. Java中IO流类的体系

在这里插入图片描述

注: 这里只列出常用的类,详情可以参考JDK API文档。

从上图发现,很多流都是成对出现的,比如:FileInputStream/FileOutputStream,显然是对文件做输入和输出操作的。我们下面简单做个总结:

  1. InputStream/OutputStream
    字节流的抽象类。
  2. Reader/Writer
    字符流的抽象类。
  3. FileInputStream/FileOutputStream
    节点流:以字节为单位直接操作“文件”。
  4. ByteArrayInputStream/ByteArrayOutputStream
    节点流:以字节为单位直接操作“字节数组对象”。
  5. ObjectInputStream/ObjectOutputStream
    处理流:以字节为单位直接操作“对象”。
  6. DataInputStream/DataOutputStream
    处理流:以字节为单位直接操作“基本数据类型与字符串类型”。
  7. FileReader/FileWriter
    节点流:以字符为单位直接操作“文本文件”(注意:只能读写文本文件)。
  8. BufferedReader/BufferedWriter
    处理流:将Reader/Writer对象进行包装,增加缓存功能,提高读写效率。
  9. BufferedInputStream/BufferedOutputStream
    处理流:将InputStream/OutputStream对象进行包装,增加缓存功能,提高读写效率
  10. InputStreamReader/OutputStreamWriter
    处理流:将字节流对象转化成字符流对象。
  11. PrintStream
    处理流:将OutputStream进行包装,可以方便地输出字符,更加灵活。

8. Java中IO的四大抽象类

InputStream/OutputStream和Reader/writer类是所有IO流类的抽象父类,我们有必要简单了解一下这个四个抽象类的作用。然后,通过它们具体的子类熟悉相关的用法。

8.1 InputStream

此抽象类是表示字节输入流的所有类的父类。InputSteam是一个抽象类,它不可以实例化。 数据的读取需要由它的子类来实现。根据节点的不同,它派生了不同的节点流子类 。

继承自InputSteam的流都是用于向程序中输入数据,且数据的单位为字节(8 bit)。

常用方法:

方法名使用说明
int read()读取一个字节的数据,并将字节的值作为int类型返回(0-255之间的一个值)。如果未读出字节则返回-1(返回值为-1表示读取结束)
void close()关闭输入流对象,释放相关系统资源

8.2 OutputStream

此抽象类是表示字节输出流的所有类的父类。输出流接收输出字节
并将这些字节发送到某个目的地。

常用方法:

方法名使用说明
void write(int n)向目的地中写入一个字节
void close()关闭输出流对象,释放相关系统资源

8.3 Reader

Reader用于读取的字符流抽象类,数据单位为字符。

常用方法:

方法名使用说明
int read()读取一个字符的数据,并将字符的值作为int类型返回(0-65535之间的一个值,即Unicode值)。如果未读出字符则返回-1(返回值为-1表示读取结束)
void close()关闭流对象,释放相关系统资源

8.4 Writer

Writer用于输出的字符流抽象类,数据单位为字符。

常用方法:

方法名使用说明
void write(int n)向输出流中写入一个字符
void close()关闭输出流对象,释放相关系统资源

二、常用流详解

1. 文件字节流

FileInputStream通过字节的方式读取文件,适合读取所有类型的文件(图像、视频、文本文件等)。

FileOutputStream 通过字节的方式写数据到文件中,适合所有类型的文件(图像、视频、文本文件等)。

1.1 FileInputStream文件输入字节流

public class TestFileInputStream {
    public static void main(String[] args) {
        //使用try-with-resource方式关闭资源。
        //在try中打开资源,不需要在代码中添加finally块关闭资源。
        try (FileInputStream fis = new FileInputStream("d:/a.txt");) {
            StringBuilder sb = new StringBuilder();
            int temp = 0;
            while ((temp = fis.read()) != -1) {
                sb.append((char) temp);
            }
            System.out.println(sb);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1.2 FileOutputStream文件输出字节流

public class TestFileOutputStream {
    public static void main(String[] args) {
        String str = "Old Lu";
        // true表示内容会追加到文件末尾;false表示重写整个文件内容。
        try (FileOutputStream fos = new FileOutputStream("d:/a.txt", true)) {
            //将整个字节数组写入到文件中。
            fos.write(str.getBytes());
            //将数据从内存中写入到磁盘中。
            fos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1.3 通过字节缓冲区提高读写效率

public class TestFileByteBuffer {
    public static void main(String[] args) {
        long time1 = System.currentTimeMillis();
        copyFile("d:/1.jpg", "d:/2.jpg");
        long time2 = System.currentTimeMillis();
        System.out.println(time2 - time1);
    }

    /**
     * @param src  源文件
     * @param desc 目标文件
     */
    public static void copyFile(String src, String desc) {
        //“后开的先关闭!”按照他们被创建顺序的逆序来关闭
        try (FileInputStream fis = new FileInputStream(src);
             FileOutputStream fos = new FileOutputStream(desc)) {
            //创建一个缓冲区,提高读写效率
            byte[] buffer = new byte[1024];
            int temp = 0;

            while ((temp = fis.read(buffer)) != -1) {
                //将缓存数组中的数据写入文件中,注意:写入的是读取的真实长度;
                fos.write(buffer, 0, temp);
            }
            //将数据从内存中写入到磁盘中。
            fos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注意 在使用字节缓冲区时,我们需要注意:

  • 为了减少对硬盘的读写次数,提高效率,通常设置缓存数组。相应地,读取时使用的方法
    为:read(byte[] b);写入时的方法为:write(byte[ ] b, int off, int length)
  • 程序中如果遇到多个流,每个流都要单独关闭,防止其中一个流出现异常后导致其他流无法
    关闭的情况。

2. 缓冲字节流

Java缓冲流本身并不具有IO流的读取与写入功能,只是在别的流(节点流或其他处理流)上加上缓冲功能提高效率,就像是把别的流包装起来一样,因此缓冲流是一种处理流(包装流)。

BufferedInputStream和BufferedOutputStream这两个流是缓冲字节流,通过内部缓存数组来提高操作流的效率。

2.1 使用缓冲流实现文件的高效率复制

public class TestFileBufferStream {
    public static void main(String[] args) {
        long time1 = System.currentTimeMillis();
        copyFile("d:/1.jpg", "d:/2.jpg");
        long time2 = System.currentTimeMillis();
        System.out.println(time2 - time1);
    }

    public static void copyFile(String source, String destination) {
        //实例化节点流
        try (FileInputStream fis = new FileInputStream(source);
             FileOutputStream fos = new FileOutputStream(destination);
             //实例化处理流
             BufferedInputStream bis = new BufferedInputStream(fis);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            int temp = 0;
            while ((temp = bis.read()) != -1) {
                bos.write(temp);
            }
            bos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注意

  • 在关闭流时,应该先关闭最外层的包装流,即“后开的先关闭”。
  • 缓存区的大小默认是8192字节,也可以使用其它的构造方法自己指定大小。

3. 文件字符流

文件字节流可以处理所有的文件,如果我们处理的是文本文件,也可以使用文件字符流,它以字符为单位进行操作。

3.1 文件字符输入流

public class TestFileReader {
    public static void main(String[] args) {
        //创建文件字符输入流对象
        try (FileReader fr = new FileReader("d:/a.txt")) {
            StringBuilder sb = new StringBuilder();
            //读取文件
            int temp = 0;
            while ((temp = fr.read()) != -1) {
                sb.append((char) temp);
            }
            System.out.println(sb);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.2 文件字符输出流

public class TestFileWriter {
    public static void main(String[] args) {
        //创建文件字符输出流对象
        try (FileWriter fw = new FileWriter("d:/aa.txt")) {
            fw.write("您好 JAVA\r\n");
            fw.write("您好 张三 \r\n");
            fw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4. 缓冲字符流

BufferedReader/BufferedWriter增加了缓存机制,大大提高了读写文本文件的效率。

4.1 字符输入缓冲流

BufferedReader是针对字符输入流的缓冲流对象,提供了更方便的按行读取的方法:readLine(); 在使用字符流读取文本文件时,我们可以使用该方法以行为单位进行读取。

public class TestBufferedReader {
    public static void main(String[] args) {
        //创建文件字符输入流对象
        try (FileReader fr = new FileReader("d:/aa.txt");
             //创建字符缓冲处理流。缓冲区默认大小为8192个字符。
             BufferedReader br = new BufferedReader(fr)) {
            //操作流
            String temp = "";
            //readLine():读取一行文本。
            while ((temp = br.readLine()) != null) {
                System.out.println(temp);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.2 字符输出缓冲流

public class TestBufferedWriter {
    public static void main(String[] args) {
        //创建文件字符输出流对象
        try (FileWriter fw = new FileWriter("d:/sxt.txt");
             //创建字符输出缓冲流对象
             BufferedWriter bw = new BufferedWriter(fw)) {
            //操作缓冲流
            bw.write("您好JAVA");
            bw.write("您好 张三");
            //换行
            bw.newLine();
            bw.write("窈窕淑女");
            bw.newLine();
            bw.write("君子好逑");
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注意

  • readLine()方法是BufferedReader的方法,可以对文本文件进行更加方便的读取操作。
  • newLine()方法BufferedWriter的方法,可以使用newLine()方法换行。

4.3 为文件中的内容添加行号

public class TestLineNumber {
    public static void main(String[] args) {
        //创建字符输入缓冲流与文件字符输入流
        try (BufferedReader br = new BufferedReader(new FileReader("d:/sxt.txt"));
             //创建字符输出缓冲流与文件字符输出流
             BufferedWriter bw = new BufferedWriter(new FileWriter("d:/sxt2.txt"))) {
            String temp = "";
            //定义序号变量
            int i = 1;
            while ((temp = br.readLine()) != null) {
                //将读取到的内容添加序号,并输出到指定文件中。
                bw.write(i + "," + temp);
                //换行处理
                bw.newLine();
                //序号变量累加
                i++;
            }
            //刷新
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5. 转换流

InputStreamReader/OutputStreamWriter用来实现将字节流转化成字符流。

5.1 通过转换流解决乱码

ANSI(American National Standards Institute)美国国家标准协会

public class TestInputStreamReader {
    public static void main(String[] args) {
        //创建文件字节输入流对象
        try (FileInputStream fis = new FileInputStream("d:/sxt.txt");
             //创建转换流(字节到字符的转换)流对象,并在该对象中指定编码。
             InputStreamReader isr = new InputStreamReader(fis, "gbk")) {
            StringBuilder sb = new StringBuilder();
            //操作流对象
            int temp = 0;
            while ((temp = isr.read()) != -1) {
                sb.append((char) temp);
            }
            System.out.println(sb);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.2 通过转换流实现键盘输入屏幕输出

public class TestKeyboardInput {
    public static void main(String[] args) {
        //创建键盘输入相关流对象
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
             //创建向屏幕输出相关流对象
             BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out))) {
            while (true) {
                bw.write("请输入:");
                bw.flush();
                //获取键盘输入的字符串
                String input = br.readLine();
                //判断输入的内容是否含有退出关键字。
                if ("exit".equals(input) || "quit".equals(input)) {
                    bw.write("Bye Bye !");
                    bw.flush();
                    break;
                }
                //将读取到键盘输入的字符串,输出到屏幕。
                bw.write("您输入的 是:" + input);
                bw.newLine();
                bw.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.3 通过字节流读取文本文件并添加行号

public class TestLineNumber2 {
    public static void main(String[] args) {
        //创建字符输入缓冲流、输入字节到字符转换流、文件字节输入流对象
        try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("d:/sxt.txt")));
             //创建字符输出缓冲流、输出字符到字节转换流、文件字节输出流对象
             BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("d:/sxt4.txt")))) {
            //操作流
            String temp = "";
            //序号变量
            int i = 1;
            //按照行读取
            while ((temp = br.readLine()) != null) {
                bw.write(i + "," + temp);
                //换行
                bw.newLine();
                //序号累加
                i++;
            }
            //刷新
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6. 字符输出流

在Java的IO流中专门提供了用于字符输出的流对象PrintWriter。该对象具有自动行刷新缓冲字符输出流,特点是可以按行写出字符串,并且可通过println();方法实现自动换行。

public class TestPrintWriter {
    public static void main(String[] args) {
        //创建字符输出流对象
        try (PrintWriter pw = new PrintWriter("d:/sxt5.txt")) {
            //调用不带换行方法完成内容的输出
            pw.print("abc");
            pw.print("def");
            //调用带有自动换行方法完成内容的输出
            pw.println("Oldlu");
            pw.println("sxt");
            pw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6.1 通过字符输出流添加行号

public class TestLineNumber3 {
    public static void main(String[] args) {
        //创建字符输入缓冲流对象与文件字符输入流对象
        try (BufferedReader br = new BufferedReader(new FileReader("d:/sxt.txt"));
             //创建字符输出流对象
             PrintWriter pw = new PrintWriter("d:/sxt6.txt")) {
            //操作流
            String temp = "";
            //定义序号变量
            int i = 1;
            while ((temp = br.readLine()) != null) {
                pw.println(i + "," + temp);
                //序号累加
                i++;
            }
            //刷新
            pw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

7. 数据流

数据流将“基本数据类型与字符串类型”作为数据源,从而允许程序以与机器无关的方式从底层输入输出流中操作Java基本数据类型与字符串类型。

DataInputStream和DataOutputStream提供了可以存取与机器无关的所有Java基础类型数据(如:int、double、String等)的方法。


public class TestDataStream {
    public static void main(String[] args) {
        //创建数据输出流对象与文件字节输出流对象
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/data"));
             //创建数据输入流对象与文件字节输入流对象
             DataInputStream dis = new DataInputStream(new FileInputStream("d:/data"))) {
            //将如下数据写入到文件中
            dos.writeChar('a');
            dos.writeInt(10);
            dos.writeDouble(Math.random());
            dos.writeBoolean(true);
            dos.writeUTF("惊鸿化雨");
            //手动刷新缓冲区:将流中数据写入到文件中
            dos.flush();
            //直接读取数据:读取的顺序要与写入的顺序一致,否则不能正确读取数据。
            System.out.println("char: " + dis.readChar());
            System.out.println("int: " + dis.readInt());
            System.out.println("double: " + dis.readDouble());
            System.out.println("boolean: " + dis.readBoolean());
            System.out.println("String: " + dis.readUTF());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注意: 使用数据流时,读取的顺序一定要与写入的顺序一致,否则不
能正确读取数据。

8. 对象流

数据流只能实现对基本数据类型和字符串类型的读写,并不能读取对象(字符串除外),如果要对某个对象进行读写操作,我们需要学习一对新的处理流:

ObjectInputStream/ObjectOutputStream。

● ObjectInputStream和OjbectOutputSteam: 用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
● 序列化: 用ObjectOutputStream类保存基本类型数据或对象的机制
● 反序列化: 用ObjectInputStream类读取基本类型数据或对象的机制
● ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量

  1. 对象的序列化
    ● 对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。 //当其它程序获取了这种二进制流,就可以恢复成原来的Java对象
    ● 序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原
    ● 序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是JavaEE 平台的基础
    ● 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。否则,会抛出NotSerializableException异常
    ○ Serializable
    ○ Externalizable
    ● 凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:
    ○ private static final long serialVersionUID;
    ○ serialVersionUID用来表明类的不同版本间的兼容性。 简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。
    ○ 如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。 若类的实例变量做了修改, serialVersionUID 可能发生变化。 故建议,显式声明。
    ● 简单来说, Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时, JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。 (InvalidCastException)
    ● 强调: 如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的Field 的类也不能序列化
public class User implements Serializable {
    private static final long serialVersionUID = 5125934336917258915L;
    private String name;
    private String age;

    public User() {
    }

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

    public String getName() {
        return name;
    }

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

    public String getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }
}

/**
 * 序列化:写对象
 * 反序列化:读对象
 * transient, static不会序列化(写)
 * serialVersionUID:版本不一致报错InvalidClassException
 */
public class TestObjectStream {
    public static void main(String[] args) throws Exception {
        ObjectOutputStream ob = new ObjectOutputStream(new FileOutputStream("a"));
        User user = new User("张三 ","18");
        ob.writeObject(user);
        ob.close();
    }

    @Test
    public void  test() throws Exception {
        ObjectInputStream os =new ObjectInputStream(new FileInputStream("a"));
        System.out.println(os.readObject());
        os.close();
    }
}

8.1 处理基本数据类型数据

ObjectInputStream/ObjectOutputStream处理基本数据类型。

public class TestObjectStreamBasicType {
    public static void main(String[] args) {
        //创建对象输出流对象与文件字节输出流对象
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/data2"));
             //创建对象输入流对象与文件字节输入流对象
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/data2"))) {
            //将如下数据写入到文件中
            oos.writeInt(10);
            oos.writeDouble(Math.random());
            oos.writeChar('a');
            oos.writeBoolean(true);
            oos.writeUTF("惊鸿化雨");
            oos.flush();
            //必须要按照写入的顺序读取数据
            System.out.println("int: "+ois.readInt());
            System.out.println("double: "+ois.readDouble());
            System.out.println("char: "+ois.readChar());
            System.out.println("boolean: "+ois.readBoolean());
            System.out.println("String: "+ois.readUTF());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注意

  • 对象流不仅可以读写对象,还可以读写基本数据类型。
  • 读写基本数据类型时,读取的顺序一定要与写入的顺序一致,否则不能正确读取数据。

9. Java对象的序列化和反序列化

在这里插入图片描述

9.1 序列化和反序列化是什么

当两个进程远程通信时,彼此可以发送各种类型的数据。 无论是何种类型的数据,都会以二进制序列的形式在网络上传送。比如,我们可以通过http协议发送字符串信息;我们也可以在网络上直接发送Java对象。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象才能正常读取。

把Java对象转换为字节序列的过程称为对象的序列化。把字节序列恢复为Java对象的过程称为对象的反序列化。

9.2 序列化涉及的类和接口

ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

只有实现了Serializable接口的类的对象才能被序列化。Serializable接口是一个空接口,只起到标记作用。

9.3 将对象序列化到文件

在这里插入图片描述
ObjectOutputStream可以将一个内存中的Java对象通过序列化的方式写入到磁盘的文件中。被序列化的对象必须要实现Serializable序列化接口,否则会抛出异常。

public class SerializationExample {

    public static void main(String[] args) {
        // 创建一个对象
        Person person = new Person("John", 30);
        try(
            // 创建 ObjectOutputStream 对象来进行对象序列化
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"))
        ) {
            // 将对象写入文件
            out.writeObject(person);
            //刷新
            out.flush();
            System.out.println("对象已成功序列化到文件。");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// Serializable 接口用于标记可序列化的类
class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

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

    // Getter 和 Setter 方法

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

9.4 将对象反序列化到内存中

public class DeserializationExample {
    public static void main(String[] args) {
        Person person = null;
        try(
             // 创建 ObjectInputStream 对象来进行对象反序列化
             ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"))
        ) {
            // 从文件中读取并反序列化对象
            person = (Person) in.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 使用反序列化后的对象
        if (person != null) {
            System.out.println("姓名:" + person.getName());
            System.out.println("年龄:" + person.getAge());
        }
    }
}

10. File类在IO中的作用

当以文件作为数据源或目标时,除了可以使用字符串作为文件以及位置的指定以外,我们也可以使用File类指定。

public class TestFile {
    public static void main(String[] args) {
        //创建字符缓冲流与文件字符输入流对象
        try (BufferedReader br = new BufferedReader(new FileReader(new File("d:/sxt.txt")));
             //创建字符输出流对象
             PrintWriter pw = new PrintWriter(new File("d:/sxt8.txt"))) {
            //操作流
            String temp = "";
            int i = 1;
            while ((temp = br.readLine()) !=
                    null) {
                pw.println(i + "," + temp);
                i++;
            }
            pw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

11. 基于数组操作的流

11.1 ByteArrayInputStream:可以基于字节数组创建输入流

    @Test
    public void test1() throws Exception {
        //
        byte[] arr = {97, 98, 99, 100, 101, 102};
        ByteArrayInputStream in = new ByteArrayInputStream(arr);

        int length = -1;
        byte[] buffer = new byte[3];

        while ((length = in.read(buffer)) != -1) {
            System.out.println(Arrays.toString(buffer));
        }

    }

11.2 ByteArrayOutputStream:可以把数据输出字节数组中

    @Test
    public void test2() throws Exception {
        BufferedInputStream in = new BufferedInputStream(new FileInputStream("hello.txt"));
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        int length = -1;
        byte[] buffer = new byte[5];

        while ((length = in.read(buffer)) != -1) {
            out.write(buffer, 0, length);
        }

        byte[] data =  out.toByteArray(); //得到输出流的字节
        in.close();
        out.close();

        System.out.println(Arrays.toString(data));
    }

11.3 CharArrayReader:基于字符数组产生输入流

    @Test
    public void test3() throws Exception {
        //
        char[] arr = {97, 'b', 99, 100, 101, 102};
        CharArrayReader in = new CharArrayReader(arr);

        int length = -1;
        char[] buffer = new char[3];

        while ((length = in.read(buffer)) != -1) {
            System.out.println(Arrays.toString(buffer));
        }
        in.close();

    }

11.4 CharArrayWriter:输出到字节数组中的输出流

    @Test
    public void test4() throws Exception {
        BufferedReader in = new BufferedReader(new FileReader("hello.txt"));
        CharArrayWriter out = new CharArrayWriter();

        int length = -1;
        char[] buffer = new char[5];

        while ((length = in.read(buffer)) != -1) {
            out.write(buffer, 0, length);
        }

        char[] data = out.toCharArray();
        in.close();
        out.close();

        System.out.println(Arrays.toString(data));
    }

三、装饰器模式构建IO流体系

1. 装饰器模式简介

装饰器模式是GOF23种设计模式中较为常用的一种模式。它可以实现对原有类的包装和装饰,使新的类具有更强的功能。

2. IO流体系中的装饰器模式

IO流体系中大量使用了装饰器模式,让流具有更强的功能、更强的灵活性。比如:

FileInputStream  fis = new FileInputStream(src);
BufferedInputStream  bis = new BufferedInputStream(fis);

显然BufferedInputStream装饰了原有的FileInputStream,让普通的FileInputStream也具备了缓存功能,提高了效率。

四、Apache commons-io工具包的使用

1. Apache基金会介绍

Apache软件基金会(也就是Apache Software Foundation,简称为ASF),是专门为支持开源软件项目而办的一个非盈利性组织。在它所支持的Apache项目与子项目中,所发行的软件产品都遵循Apache许可证(Apache License)。

官方网址为:www.apache.org

很多著名的Java开源项目都来源于这个组织。比如:commons、kafka、lucene、maven、shiro、struts等技术,以及大数据技术中的:hadoop(大数据第一技术)、hbase、spark、storm、mahout等。

2. commons-io工具包

Apache的commons-io工具包中提供了IOUtils/FileUtils,为我们提供了更加简单、功能更加强大的文件操作和IO流操作功能。非常值得大家学习和使用。

2.1 FileUtils类中常用方法的介绍

打开FileUtils的api文档,我们抽出一些工作中比较常用的方法,进行总结和讲解。总结如下:

方法名使用说明
cleanDirectory清空目录,但不删除目录
contentEquals比较两个文件的内容是否相同
copyDirectory将一个目录内容拷贝到另一个目录。可以通过FileFilter过滤需要拷贝的文件
copyFile将一个文件拷贝到一个新的地址
copyFileToDirectory将一个文件拷贝到某个目录下
copyInputStreamToFile将一个输入流中的内容拷贝到某个文件
deleteDirectory删除目录
deleteQuietly删除文件
listFiles列出指定目录下的所有文件
openInputSteam打开指定文件的输入流
readFileToString将文件内容作为字符串返回
readLines将文件内容按行返回到一个字符串数组中
size返回文件或目录的大小
write将字符串内容直接写到文件中
writeByteArrayToFile将字节数组内容写到文件中
writeLines将容器中的元素的toString方法返回的内容依次写入文件中
writeStringToFile将字符串内容写到文件中

读取文件内容,并输出到控制台上(只需一行代码!)

public class TestFileUtilsDemo1 {
    public static void main(String[] args) throws Exception {
        String content = FileUtils.readFileToString(new File("d:/a.txt"), "gbk");
        System.out.println(content);
    }
}

2.2 使用FileUtils工具类实现目录拷贝

我们可以使用FileUtils完成目录拷贝,在拷贝过程中可以通过文件过滤器(FileFilter)选择拷贝内容。

public class TestFileUtilsDemo2 {
    public static void main(String[] args) throws Exception {
        FileUtils.copyDirectory(new File("d:/aaa"), new File("d:/bbb"), new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                // 使用FileFilter过滤目录和以html结尾的文件
                if (pathname.isDirectory() || pathname.getName().endsWith("html")) {
                    return true;
                } else {
                    return false;
                }
            }
        });
    }
}

2.3 IOUtils的妙用

打开IOUtils的api文档,我们发现它的方法大部分都是重载的。所以,我们理解它的方法并不是难事。因此,对于方法的用法总结如下:

方法名使用说明
buffer将传入的流进行包装,变成缓冲流。并可以通过参数指定缓冲大小
closeQueitly关闭流
contentEquals比较两个流中的内容是否一致
copy将输入流中的内容拷贝到输出流中,并可以指定字符编码
copyLarge将输入流中的内容拷贝到输出流中,适合大于2G内容的拷贝
lineIterator返回可以迭代每一行内容的迭代器
read将输入流中的部分内容读入到字节数组中
readFully将输入流中的所有内容读入到字节数组中
readLine读入输入流内容中的一行
toBufferedInputStream,toBufferedReader将输入转为带缓存的输入流
toByteArray,toCharArray将输入流的内容转为字节数组、字符数组
toString将输入流或数组中的内容转化为字符串
write向流里面写入内容
writeLine向流里面写入一行内容

2.4 IOUtils的使用

public class TestIOUtilsDemo {
    public static void main(String[] args) throws Exception {
        String content = IOUtils.toString(new FileInputStream("d:/sxt.txt"), "utf-8");
        System.out.println(content);
    }
}

五、本章总结

1. 按流的方向分类

  • 输入流:数据源到程序(InputStream、Reader读进来)。
  • 输出流:程序到目的地(OutputStream、Writer写出去)。

2. 按流的处理数据单元分类:

  • 字节流:按照字节读取数据(InputStream、OutputStream)。
  • 字符流:按照字符读取数据(Reader、Writer)。

3. 按流的功能分类

  • 字符流:按照字符读取数据(Reader、Writer)。
  • 处理流:不直接连接到数据源或目的地,是处理流的流。通过对其他流的处理提高程序的性能。

4. IO的四个基本抽象类

  • InputStream
  • OutputStream
  • Reader
  • Writer

5. InputStream的实现类

  • FileInputStream
  • BufferedInputStream
  • DataInputStream
  • ObjectInputStream

6. OutputStream的实现类

  • FileOutputStream
  • BufferedOutputStream
  • DataOutputStream
  • ObjectOutputStream

7. Reader的实现类

  • FileReader
  • BufferedReader
  • InputStreamReader

8. Writer的实现类

  • FileWriter
  • BufferedWriter
  • OutputStreamWriter
  • PrintWriter

9. 把Java对象转换为字节序列的过程称为对象的序列化

10. 把字节序列恢复为Java对象的过程称为对象的反序列化

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值