IO 技术(input、output)知识理解与整理

IO 技术(input、output)

Java 中从程序一端到数据源(文件、数据库、网络等)之间的管道我们称之为 IO 流。IO 流是一连串连续动态的数据集合。一切以程序为中心,进入程序,也就是从外部系统获取数据叫输入流,即读取数据。从程序中出去,也就是从将数据输出到外部系统叫输出流,即写出数据。数据的输入与输出是非常重要的功能,所以我们要通过某种方式进行抽象、屏蔽外部的差异,从而实现更加便捷的编程,也就是我们即将要了解的 IO 技术。

一、数据源

数据源即我们要输入或者输出的数据来源。常见的数据源有:数据库、文件、其他程序、内存、网络连接、IO 设备。

数据源分为:源设备、目标设备。

  1. 源设备:为程序提供数据,一般对应输入流。

  2. 目标设备:程序数据的目的地,一般对应输出流。

b8ec5c3829dc43c79c0a150262a8bb76_1554973800442.png
当程序需要读取数据源的数据时,就会通过 IO 流对象开启一个通向数据源的流,通过这个 IO 流对象的相关方法可以顺序读取数据源中的数据。

二、流的概念

流是一个抽象、动态的概念,是一连串连续动态的数据集合。 我的理解是数据就像是大海与湖泊之间的流动的水,流就是水流经的路径,从海里流入湖泊的路径叫输入流,从湖泊流入大海的路径叫输出流。而当我们创建一个流对象时,这里的流对象就是让数据进行传输的路径,或者说媒介。

三、第一个简单的 IO 流程序及深入理解

import java.io.*;
public class TestIO1 {
    public static void main(String[] args) {
        try {
            //创建输入流
            FileInputStream fis = new FileInputStream("d:/a.txt"); // 文件内容是:abc
            //一个字节一个字节的读取数据
            int s1 = fis.read(); // 打印输入字符a对应的ascii码值97
            int s2 = fis.read(); // 打印输入字符b对应的ascii码值98
            int s3 = fis.read(); // 打印输入字符c 对应的ascii码值99
            int s4 = fis.read(); // 由于文件内容已经读取完毕,返回-1
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s3);
            System.out.println(s4);
            // 流对象使用完,必须关闭!不然,总占用系统资源,最终会造成系统崩溃!
            fis.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

字节(Byte)是一种计量单位,表示数据量多少,它是计算机信息技术用于计量存储容量的一种计量单位。

字符是指计算中使用的文字和符号,比如 1、2、3、A、B、C、~!·#¥%……—*()——+、等等。

在 ASCII 编码中一个数字或字母占一个字节,一个汉字占两个字节。

注意:

  • 我们读取的文件内容是已知的,因此可以使用固定次数的“int s= fis.read();”语句读取内容,但是在实际开发中通常我们根本不知道文件的内容,因此我们在读取的时候需要配合 while 循环使用。
  •  为了保证出现异常后流的正常关闭,通常要将流的关闭语句要放到 finally 语句块中,并且要判断流是不是 null。

下面是经典代码

package IO;
import java.io.*;
public class TestIO2 {
    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) {//将每一个读取到的字节的ascll码赋给temp让后判断是否等于-1
                sb.append((char) temp);//将int型的temp强制转成char型的加在sb字符串的末尾
            }
            System.out.println(sb);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                //这种写法,保证了即使遇到异常情况,也会关闭流对象。
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

四、五个类和三个接口

说明
File 文件类
InputStream 字节输入流
OutputStream 字节输出流
Reader 字符输入流
Writer 字符输出流
Closeable 关闭流接口
Flushable 刷新流接口
Serializable 序列化接口

五、IO 流的分类

1.按流的方向分类:

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

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

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

3.按处理对象不同分类:

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

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

六、Java 中 IO 流类的体系

Java 为我们提供了多种多样的 IO 流,我们可以根据不同的功能及性能要求挑选合适的 IO 流 。

很多流都是成对出现的,比如: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进行包装,可以方便地输出字符,更加灵活。

七、四大 IO 抽象类

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

·InputStream

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

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

常用方法:

int read():读取一个字节的数据,并将字节的值作为 int 类型返回 (0-255 之间的一个值)。如果未读出字节则返回 -1(返回值为 -1 表示读取结束)。

void close():关闭输入流对象,释放相关系统资源。

· OutputStream

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

常用方法:

void write(int n):向目的地中写入一个字节。

void close():关闭输出流对象,释放相关系统资源。

· Reader

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

int read(): 读取一个字符的数据,并将字符的值作为 int 类型返回 (0-65535 之间的一个值,即 Unicode 值)。如果未读出字符则返回 -1(返回值为 -1 表示读取结束)。

void close() : 关闭流对象,释放相关系统资源。

· Writer

​ Writer 用于写入的字符流抽象类,数据单位为字符

void write(int n): 向输出流中写入一个字符。

void close() : 关闭输出流对象,释放相关系统资源。

八、文件字节流

  FileInputStream 通过字节的方式读取文件,适合读取所有类型的文件 (图像、视频、文本文件等)。Java 也提供了 FileReader 专门读取文本文件。

  FileOutputStream 通过字节的方式写数据到文件中,适合所有类型的文件。Java 也提供了 FileWriter 专门写入文本文件。

import java.io.FileOutputStream;
import java.io.IOException;
public class TestFileOutputStream {
    public static void main(String[] args) {
        FileOutputStream fos = null;
        String string = "北京尚学堂欢迎您!";
        try {
            // true表示内容会追加到文件末尾;false表示重写整个文件内容。
            fos = new FileOutputStream("d:/a.txt", true);
            //该方法是直接将一个字节数组写入文件中; 而write(int n)是写入一个字节
            fos.write(string.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

现在我们已经学习了使用文件字节流分别实现文件的读取与写入操作,接下来我们将两种功能综合使用就可以轻松实现文件的复制了。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestFileCopy {
    public static void main(String[] args) {
        //将a.txt内容拷贝到b.txt
        copyFile("d:/a.txt", "d:/b.txt"); 
    }
 
    /**
     * 将src文件的内容拷贝到dec文件
     * @param src 源文件
     * @param dec 目标文件
     */
    static void copyFile(String src, String dec) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        //为了提高效率,设置缓存数组!(读取的字节数据会暂存放到该字节数组中)
        byte[] buffer = new byte[1024];
        int temp = 0;
        try {
            fis = new FileInputStream(src);
            fos = new FileOutputStream(dec);
            //边读边写
            //temp指的是本次读取的真实长度,temp等于-1时表示读取结束
            while ((temp = fis.read(buffer)) != -1) {
                /*将缓存数组中的数据写入文件中,注意:写入的是读取的真实长度;
                 *如果使用fos.write(buffer)方法,那么写入的长度将会是1024,即缓存
                 *数组的长度*/
                fos.write(buffer, 0, temp);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //两个流需要分别关闭
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

注意

​ 在使用文件字节流时,我们需要注意以下两点:

​ 1. 为了减少对硬盘的读写次数,提高效率,通常设置缓存数组。相应地,读取时使用的方法为:read(byte[] b); 写入时的方法为:write(byte[] b, int off, int length)。

  1. 程序中如果遇到多个流,每个流都要单独关闭,防止其中一个流出现异常后导致其他流无法关闭的情况。

九、文件字符流

 前面介绍的文件字节流可以处理所有的文件,但是字节流不能很好的处理 Unicode 字符,经常会出现“乱码”现象。所以,我们处理文本文件,一般可以使用文件字符流,它以字符为单位进行操作。

使用 FileReader 与 FileWriter 实现文本文件的复制

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class TestFileCopy2 {
    public static void main(String[] args) {
        // 写法和使用Stream基本一样。只不过,读取时是读取的字符。
        FileReader fr = null;
        FileWriter fw = null;
        int len = 0;
        try {
            fr = new FileReader("d:/a.txt");
            fw = new FileWriter("d:/d.txt");
            //为了提高效率,创建缓冲用的字符数组
            char[] buffer = new char[1024];
            //边读边写
            while ((len = fr.read(buffer)) != -1) {
                fw.write(buffer, 0, len);
            }
 
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fr != null) {
                    fr.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

十、缓冲字节流

缓冲的意思就是说将读取的或者说要写出的数据缓冲在一个区域,直到所有数据都缓冲完了才一次性输入或输出。而不是一个个数据频繁的进行输入输出,这样大大提高效率。

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

当对文件或者其他数据源进行频繁的读写操作时,效率比较低,这时如果使用缓冲流就能够更高效的读写信息。因为缓冲流是先将数据缓存起来,然后当缓存区存满后或者手动刷新时再一次性的读取到程序或写入目的地。

因此,缓冲流还是很重要的,我们在 IO 操作时记得加上缓冲流来提升性能。

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

下面我们通过两种方式 (普通文件字节流与缓冲文件字节流) 实现一个视频文件的复制,来体会一下缓冲流的好处。

  注意:缓冲流属于包装流,只能对已有的流进行封装,不能直接关联文件进行操作。

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

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class TestBufferedFileCopy1 {
 
    public static void main(String[] args) {
        // 使用缓冲字节流实现复制
        long time1 = System.currentTimeMillis();
        copyFile1("D:/电影.mp4", "D:/电影/华语/大陆/尚学堂越
                 "+"来越传奇.mp4");
        long time2 = System.currentTimeMillis();
        System.out.println("缓冲字节流花费的时间为:" + (time2 - time1));
 
        // 使用普通字节流实现复制
        long time3 = System.currentTimeMillis();
        copyFile2("D:/电影/华语/大陆/传奇.mp4", "D:/电影/华语/大陆"+"传奇2.mp4");
        long time4 = System.currentTimeMillis();
        System.out.println("普通字节流花费的时间为:" + (time4 - time3));
    }
    /**缓冲字节流实现的文件复制的方法*/
    static void copyFile1(String src, String dec) {
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        FileOutputStream fos = null;
        BufferedOutputStream bos = null;
        int temp = 0;
        try {
            fis = new FileInputStream(src);
            fos = new FileOutputStream(dec);
            //使用缓冲字节流包装文件字节流,增加缓冲功能,提高效率
            //缓存区的大小(缓存数组的长度)默认是8192,也可以自己指定大小
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);
            while ((temp = bis.read()) != -1) {
                bos.write(temp);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //注意:增加处理流后,注意流的关闭顺序!“后开的先关闭!”
            try {
                if (bos != null) {
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (bis != null) {
                    bis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    /**普通节流实现的文件复制的方法*/
    static void copyFile2(String src, String dec) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        int temp = 0;
        try {
            fis = new FileInputStream(src);
            fos = new FileOutputStream(dec);
            while ((temp = fis.read()) != -1) {
                fos.write(temp);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 在关闭流时,应该先关闭最外层的包装流,即“后开的先关闭”。

  2. 缓存区的大小默认是 8192 字节,也可以使用其它的构造方法自己指定大小。

十一、缓冲字符流

BufferedReader/BufferedWriter 增加了缓存机制,大大提高了读写文本文件的效率,同时,提供了更方便的按行读取的方法:readLine(); 处理文本时,我们一般可以使用缓冲字符流。

使用 BufferedReader 与 BufferedWriter 实现文本文件的复制

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
 
public class TestBufferedFileCopy2 {
    public static void main(String[] args) {
        // 注:处理文本文件时,实际开发中可以用如下写法,简单高效!!
        FileReader fr = null;
        FileWriter fw = null;
        BufferedReader br = null;
        BufferedWriter bw = null;
        String tempString = "";
        try {
            fr = new FileReader("d:/a.txt");
            fw = new FileWriter("d:/d.txt");
            //使用缓冲字符流进行包装
            br = new BufferedReader(fr);
            bw = new BufferedWriter(fw);
            //BufferedReader提供了更方便的readLine()方法,直接按行读取文本
            //br.readLine()方法的返回值是一个字符串对象,即文本中的一行内容
            while ((tempString = br.readLine()) != null) {
                //将读取的一行字符串写入文件中
                bw.write(tempString);
                //下次写入之前先换行,否则会在上一行后边继续追加,而不是另起一行
                bw.newLine();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bw != null) {
                    bw.close();
                }
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            try {
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fr != null) {
                    fr.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. readLine() 方法是 BufferedReader 特有的方法,可以对文本文件进行更加方便的读取操作。
  2. 写入一行后要记得使用 newLine() 方法换行。

十二、字节数组流

ByteArrayInputStream 和 ByteArrayOutputStream 经常用在需要流和数组之间转化的情况!

说白了,FileInputStream 是把文件当做数据源。ByteArrayInputStream 则是把内存中的”某个字节数组对象”当做数据源。

简单测试 ByteArrayInputStream 的使用

import java.io.ByteArrayInputStream;
import java.io.IOException;
 
public class TestByteArray {
    public static void main(String[] args) {
        //将字符串转变成字节数组
        byte[] b = "abcdefg".getBytes();
        test(b);
    }
    public static void test(byte[] b) {
        ByteArrayInputStream bais = null;
        StringBuilder sb = new StringBuilder();
        int temp = 0;
        //用于保存读取的字节数
        int num = 0; 
        try {
            //该构造方法的参数是一个字节数组,这个字节数组就是数据源
            bais = new ByteArrayInputStream(b);
            while ((temp = bais.read()) != -1) {
                sb.append((char) temp);
                num++;
            }
            System.out.println(sb);
            System.out.println("读取的字节数:" + num);
        } finally {
            try {
                if (bais != null) {
                    bais.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果:

abcdefg
读取的字节数:7

十三、数据流

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

DataInputStream 和DataOutputStream 提供了可以存取与机器无关的所有 Java 基础类型数据

package IO;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class TestDataStream {
    public static void main(String[] args) {
        DataOutputStream dos = null;
        DataInputStream dis = null;
        FileOutputStream fos = null;
        FileInputStream  fis = null;
        try {
            fos = new FileOutputStream("D:/data.txt");
            fis = new FileInputStream("D:/data.txt");
            //使用数据流对缓冲流进行包装,新增缓冲功能
            //将已有的fos文件字节流进行包装,然后以数据流对其进行包装
            dos = new DataOutputStream(new BufferedOutputStream(fos));
            dis = new DataInputStream(new BufferedInputStream(fis));
            //将如下数据写入到文件中
            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();
        } finally {
            try {
                if(dos!=null){
                    dos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(dis!=null){
                    dis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(fos!=null){
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(fis!=null){
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

十四、对象流

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

ObjectInputStream/ObjectOutputStream 是以“对象”为数据源,但是必须将传输的对象进行序列化与反序列化操作。

ObjectInputStream/ObjectOutputStream 的使用

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.Date;
 
public class TestObjectStream {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        write();
        read();
    }
    /**使用对象输出流将数据写入文件*/
    public static void write(){
        // 创建Object输出流,并包装缓冲流,增加缓冲功能
        OutputStream os = null;
        BufferedOutputStream bos = null;
        ObjectOutputStream oos = null;
        try {
            os = new FileOutputStream(new File("d:/bjsxt.txt"));
            bos = new BufferedOutputStream(os);
            oos = new ObjectOutputStream(bos);
            // 使用Object输出流
            //对象流也可以对基本数据类型进行读写操作
            oos.writeInt(12);
            oos.writeDouble(3.14);
            oos.writeChar('A');
            oos.writeBoolean(true);
            oos.writeUTF("对象流");
            //对象流能够对对象数据类型进行读写操作
            //Date是系统提供的类,已经实现了序列化接口
            //如果是自定义类,则需要自己实现序列化接口
            oos.writeObject(new Date());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭输出流
            if(oos != null){
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(bos != null){
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(os != null){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /**使用对象输入流将数据读入程序*/
    public static void read() {
        // 创建Object输入流
        InputStream is = null;
        BufferedInputStream bis = null;
        ObjectInputStream ois = null;
        try {
            is = new FileInputStream(new File("d:/bjsxt.txt"));
            bis = new BufferedInputStream(is);
            ois = new ObjectInputStream(bis);
            // 使用Object输入流按照写入顺序读取
            System.out.println(ois.readInt());
            System.out.println(ois.readDouble());
            System.out.println(ois.readChar());
            System.out.println(ois.readBoolean());
            System.out.println(ois.readUTF());
            System.out.println(ois.readObject().toString());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭Object输入流
            if(ois != null){
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(bis != null){
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(is != null){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

注意

  1. 对象流不仅可以读写对象,还可以读写基本数据类型。

  2. 使用对象流读写对象时,该对象必须序列化与反序列化。

  3. 系统提供的类 (如 Date 等) 已经实现了序列化接口,自定义类必须手动实现序列化接口。

十五、转换流

InputStreamReader/OutputStreamWriter 用来实现将字节流转化成字符流。比如,如下场景:

​ System.in 是字节流对象,代表键盘的输入,如果我们想按行接收用户的输入时,就必须用到缓冲字符流 BufferedReader 特有的方法 readLine(),但是经过观察会发现在创建 BufferedReader 的构造方法的参数必须是一个 Reader 对象,这时候我们的转换流 InputStreamReader 就派上用场了。

​ 而 System.out 也是字节流对象,代表输出到显示器,按行读取用户的输入后,并且要将读取的一行字符串直接显示到控制台,就需要用到字符流的 **write(String str)** 方法,所以我们要使用 OutputStreamWriter 将字节流转化为字符流。

使用 InputStreamReader 接收用户的输入,并输出到控制台

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
 
public class TestConvertStream {
    public static void main(String[] args) {
        // 创建字符输入和输出流:使用转换流将字节流转换成字符流
        BufferedReader br = null;
        BufferedWriter bw = null;
        try {
            br = new BufferedReader(new InputStreamReader(System.in));
            bw = new BufferedWriter(new OutputStreamWriter(System.out));
            // 使用字符输入和输出流
            String str = br.readLine();
            // 一直读取,直到用户输入了exit为止
            while (!"exit".equals(str)) {
                // 写到控制台
                bw.write(str);
                bw.newLine();// 写一行后换行
                bw.flush();// 手动刷新
                // 再读一行
                str = br.readLine();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭字符输入和输出流
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bw != null) {
                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

十六、序列化和反序列化

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

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

​ 对象序列化的作用有如下两种:

​ 1. 持久化: 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中,比如:休眠的实现。以后服务器 session 管理,hibernate 将对象持久化实现。

  1. 网络通信:在网络上传送对象的字节序列。比如:服务器之间的数据通信、对象传递。

序列化涉及的类和接口

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

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

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

序列化 / 反序列化的步骤和实例

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
 
//Person类实现Serializable接口后,Person对象才能被序列化
class Person implements Serializable {
    // 添加序列化ID,它决定着是否能够成功反序列化!
    private static final long serialVersionUID = 1L;
    int age;
    boolean isMan;
    String name;
 
    public Person(int age, boolean isMan, String name) {
        super();
        this.age = age;
        this.isMan = isMan;
        this.name = name;
    }
 
    @Override
    public String toString() {
        return "Person [age=" + age + ", isMan=" + isMan + ", name=" + name + "]";
    }
}
 
public class TestSerializable {
    public static void main(String[] args) {
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        FileInputStream fis = null;
        try {
            // 通过ObjectOutputStream将Person对象的数据写入到文件中,即序列化。
            Person person = new Person(18, true, "高淇");
            // 序列化
            fos = new FileOutputStream("d:/c.txt");
            oos = new ObjectOutputStream(fos);
            oos.writeObject(person);
            oos.flush();
            // 反序列化
            fis = new FileInputStream("d:/c.txt");
            // 通过ObjectInputStream将文件中二进制数据反序列化成Person对象:
            ois = new ObjectInputStream(fis);
            Person p = (Person) ois.readObject();
            System.out.println(p);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

注意

  1. static 属性不参与序列化。

  2. 对象中的某些属性如果不想被序列化,不能使用 static,而是使用 transient 修饰。

  3. 为了防止读和写的序列化 ID 不一致,一般指定一个固定的序列化 ID。

489461bc2cc046d8bf2e2731bab1e31b_QQ20190412165653.png