Java 学习笔记:第十章 IO技术

10.1 基本概念和IO入门

对于任何程序设计语言而言,输入输出(input/output)系统都是非常核心的功能。程序运行需要数据,数据的获取往往需要跟外部系统进行通信,外部系统可能是文件、数据库、其他程序、网络、IO设备等等。外部系统比较复杂多变,那么我们有必要通过某种手段进行抽象、屏蔽外部的差异,从而实现更加便捷的编程。

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

  • 读取硬盘上的文件内容到程序。例如:播放器打开一个视频文件、word 打开一个doc 文件。
  • 读取网络上某个位置内容到程序。例如:浏览器中输入网址后,打开该网址对应的网页内容;下载网络上某个网址的文件。
  • 读取数据库系统的数据到程序。
  • 读取某些硬件系统数据到程序。例如:车载电脑读取雷达扫描信息到程序;温控系统等。

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

  • 将数据写到硬盘中。例如:我们编辑完一个word 文档后,将内容写到硬盘上进行保存。
  • 将数据写到数据库系统中。例如:我们注册一个网站会员,实际就是后台程序向数据库中写入一条记录。
  • 将数据写到某些硬件系统中,例如:导弹系统导航程序将新的路径输出到飞控子系统,飞控子系统根据数据修正飞行路径。

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

10.1.1 数据源

数据源 data source,提供数据的原始媒介。常见的数据源有:数据库、文件、其他程序、内存、网络连接、IO设备。

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

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

在这里插入图片描述

10.1.2 流的概念

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

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

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

在这里插入图片描述

菜鸟雷区

输入流 或 输出流的划分是相对程序而言的,并不是相对数据源。

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

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

【示例10-1】使用流读取文件内容(不规范写法,仅用于测试)

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();
        }
    }
}

执行结果
在这里插入图片描述

通过示例 10-1 我们注意一下几点:

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

【示例10-2】 使用流读取文件内容(经典代码,一定要掌握)

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) {
                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();
            }
        }
    }
}

执行结果

在这里插入图片描述

老鸟建议

如上代码时一段非常典型的IO流代码,其他流对象的使用也基本是同样的模式!

10.1.4 Java 中的流的概念细分

按流的方向分类:

  1. 输入流:数据流向是数据源到程序(以InputStream、Reader结尾的流)
  2. 输出流:数据流向是程序到目的地(以OutputStream、Write结尾的流)

在这里插入图片描述

按处理的数据单元分类:

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

按处理对象不同分类:

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

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

在这里插入图片描述

10.1.5 Java 中IO流类的体系

Java 为我们提供了多种多样的IO 流,我们可以根据不同的功能及性能要求挑选合适的IO流,如图所示,为 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 进行包装,可以方便地输出字符,更加灵活。

老鸟建议

上面的解释,一句话就点中了流的核心作用,大家在后面学习的时候,用心体会

10.1.6 四大IO抽象类

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

InputStream

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

继承自 InputStream 的流都是用于向程序总输入数据,且数据的单位为字节(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():关闭流对象,释放相关系统资源。

10.2.1 文件字节流

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

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

【示例10-3】 将文件内用读取到程序中

参考【示例10-2】即可。

【示例10-4】 将字符串/字节数组的内用写入到文件中

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();
            }
        }
    }
}

在示例10-4 中,用到一个write 方法:void write(byte[] b),该方法不在一个字节一个字节地写入,而是直接写入一个字节数组;另外其还有一个重载的方法:void write(byte[] b,int off,int length),这个方法也是雪茹一个字节数组,但是我们程序员可以指定从字节数组的哪个文职开始写入,写入的长度是多少。

执行结果:

在这里插入图片描述

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

【示例10-5】利用文件流实现文件的复制

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)
  2. 程序中如果遇到多个刘,每个流都要单独关闭,防止其中一个流出现异常后导致其他流无法关闭的情况。

10.2.2 文件字符流

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

【示例10-6】使用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();
            }
        }
    }
}

执行结果:

在这里插入图片描述
在这里插入图片描述

10.2.3 缓冲字节流

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

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

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

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

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

【示例10-7】使用缓冲流实现文件的高效率复制

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字节,也可以使用其他的构造方法自己指定大小。

10.2.4 缓冲字符流

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

【示例10-8】使用 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()方法换行。

10.2.5 字节数组流

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

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

【示例10-9】简单测试 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();
            }
        }
    }
}

执行结果:

在这里插入图片描述

10.2.6 数据流

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

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

【示例10-10】DataInputStream 和 DataOutputStream 的使用

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");
            //使用数据流对缓冲流进行包装,新增缓冲功能
            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();
            }
        }
    }
}

执行结果:

在这里插入图片描述

菜鸟雷区

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

10.2.7 对象流

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

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

序列化与反序列化 的具体内容,请见 10.3

【示例10-11】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 类)已经实现了序列化接口,自定义类必须手动实现序列化接口。

10.2.8 转换流

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

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

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

【示例10-12】使用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();
                }
            }
        }
    }
}

执行结果:

在这里插入图片描述

10.3.1 序列化和反序列化

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

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

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

  1. 持久化:把对象的字节序列永久地保存在硬盘上,通常存放在一个文件中,比如:休眠的实现。以后服务器 session 管理,hibernate 将对象持久化实现。
  2. 网络通信:在网络上传送对象的字节序列。比如:服务器之间的数据通信、对象传递。

10.3.2 序列化涉及的类和接口

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

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

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

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

【示例10-14】 将Person类的实例进行序列化和反序列化

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。

10.4.1 装饰器模式简介

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

我这里有只能手机iphone,我们可以通过加装投影组件,实现原有手机功能的扩展。这就是一种 “装饰器模式”。我们在未来给普通人加装“外骨骼”装饰,让普通人具有很大的力量,也是一种“装饰器模式”

在这里插入图片描述

【示例10-15】装饰器模式演示

class Iphone {
    private String name;
    public Iphone(String name) {
        this.name = name;
    }
    public void show() {
        System.out.println("我是" + name + ",可以在屏幕上显示");
    }
}
 
class TouyingPhone {
    public Iphone phone;
    public TouyingPhone(Iphone p) {
        this.phone = p;
    }
    // 功能更强的方法
    public void show() {
        phone.show();
        System.out.println("还可以投影,在墙壁上显示");
    }
}
 
public class TestDecoration {
    public static void main(String[] args) {
        Iphone phone = new Iphone("iphone30");
        phone.show();
        System.out.println("===============装饰后");
        TouyingPhone typhone = new TouyingPhone(phone);
        typhone.show();
    }
}

10.4.2 IO流体系中的装饰器模式

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

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

显然 BufferedInputStream 装饰了原有的 FileInputStream ,让普通的 FileInputStream 也具备了缓存功能,提高了效率。大家举一反三,可以翻看本章代码,看看还有哪些地方使用了 装饰器模式。

10.5 Apache IOUtils 和 FileUtils 的使用

JDK 中提供的文件操作相关的类,但是功能都非常基础,进行复杂操作时需要大量编程工作。实际开发中,往往需要你自己动手编写相关的代码。尤其在 遍历目录文件时,经常用到递归,非常繁琐。Apache-commons 工具包中提供了IOUtils/FileUtils ,可以让我们非常方便的对文件和目录进行操作。本文就是让大家对 IOUtils/FileUtils类有一个全面的认识,便于大家以后开发与文件的目录相关的功能。

Apache IOUtils 和 FileUtils 类库为我们提供了更加简单、功能更加强大的文件操作和IO流操作功能。非常值得大家学习和使用。

10.5.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 等。

10.5.2 FileUtils 的妙用

jar包下载和介绍

首先,我们要下载FileUtils 相关的 Apache-commons-io jar包以及api 文档。FileUtils类库的下载页面在:

http://commons.apache.org/proper/commons-io/download_io.cgi

点击进入下载页面:

在这里插入图片描述

API文档的页面:

http://commons.apache.org/proper/commons-io/javadocs/api-2.5/index.html

我们本次示例,下载了最新的2.5版本,作为测试和示范。

eclipse 项目如何导入外部的jar包

  1. 在 eclipse 项目下新建 lib 文件夹
  2. 解压下载后的版本,找到 commons-io-2.5.jar包,并拷贝到lib文件夹下
  3. 设置jar 包进入项目的classpath 中。

项目名上右击,依次选择【Build Path】–>【Configure Build Path…】,在打开的窗口中,先选中【Libraries】页,再从右边的按钮中点击 【add JARs…】; 在打开的窗口中,我们依次展开本项目的项目和lib文件夹,然后选中我们刚才复制到项目中的jar包,然后点击【Apply】使刚才的操作生效,最后点击【OK】关闭窗口。

在这里插入图片描述

项目结构如下:
在这里插入图片描述

新手雷区

很多初学者会忘记配置项目的classPath,从而项目找不到相关的jar 包。大家可以在此处多配置几次,直到足够熟练!

FileUtils 类中常用方法的介绍

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

cleanDirectory:清空目录,但不删除目录。

contentEquals:比较两个文件的内用是否相同。

copyDirectory:将一个目录内容拷贝到另一个目录。可以通过 FileFilter 过滤需要拷贝的文件。

copyFile:将一个文件拷贝到一个新的地址。

copyFileToDirectory:将一个文件拷贝到某个目录下。

copyInputStreamToFile:将一个输入流中的内容拷贝到某个文件。

deleteDirectory:删除目录。

deleteQuietly:删除文件。

listFiles:列出指定目录下的所有文件。

openInputStream:打开指定文件的输入流。

readFileToString:将文件内用作为字符串返回。

readLines:将文件内容按行返回到一个字符串数组中。

size:返回文件或目录的大小。

write:将字符串内容直接写到文件中。

writeByteArrayToFile:将字节数组内用写到文件中。

writeLines:将容器中的元素的toString方法返回的内用一次写入文件中。

writeStringToFile:将字符串内容写到文件中。

代码演示:

【示例10-16】读取文件内容,并输出到控制台上(只需要一句代码!)

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

执行结果:
在这里插入图片描述

【示例10-17】目录拷贝,并使用 FileFilter 过滤目录和以html 结尾的文件

import java.io.File;
import java.io.FileFilter;
import org.apache.commons.io.FileUtils;
 
public class TestUtils2 {
    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;
                }
            }
        });
    }
}

执行结果:

在这里插入图片描述

10.5.3 IOUtils 的妙用

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

  1. buffer 方法:将传入的流进行包装,变成缓冲流。并可以通过参数指定缓冲大小。

  2. closeQueitly 方法:关闭流。

  3. contentEquals 方法:比较两个流中的内容是否一致。

  4. copy 方法:将输入流中的内容拷贝到输出流中,并可以指定字符编码。

  5. copyLarge 方法:将输入流中的内容拷贝到输出流中,适合大于2G 内容的拷贝。

  6. IineIterator 方法:返回可以迭代每一行内容的迭代器。

  7. read 方法:将输入流中的所有内容读入到字节数组中。

  8. readFully 方法:将输入流中的所有内容读入到字节数组中。

  9. readLine 方法:读入输入流内容中的一行。

  10. toBufferedInputStream,toBufferedReader:将输入转为带缓存的输入流。

  11. toByteArray,toCharArray:将输入流的内容转为字节数组、字符数组。

  12. toString :将输入流或数组中的内容转化为字符串。

  13. wirte 方法:向流里面写入内容。

  14. writeLine 方法:向流里面写入一行内容。

我们没有不要对每个方法做测试,只是演示一下读入 d:/a.txt 文件内容到程序中,并转化为 String 对象打印出来。

【示例10-18】IOutils 的方法

import java.io.*;
import org.apache.commons.io.IOUtils;
public class TestUtils3 {
    public static void main(String[] args) throws Exception {
        String content = IOUtils.toString(new FileInputStream("d:/a.txt"),"gbk");
        System.out.println(content);
    }
}

执行结果:

在这里插入图片描述

总结

  1. 按流的方向分类:
    输入流:数据源到程序(InputStream、Reader 读进来)
    输出流:程序到目的地(OutputStream、Writer 写出去)

  2. 按流的处理数据单元分类:
    字节流:按照字节读取数据(InputStream、OutputStream)
    字符流:按照字符读取数据(Reader、Writer)

  3. 按流的功能分类:
    节点流:可以直接从数据源或目的地读写数据。
    处理流:不直接连接到数据源或目的地,是处理流的流。通过对其他流的处理提高程序的性能。

  4. IO 的四个基本抽象类:InputStream 、OutputStream、Reader、Writer

  5. InputStream 的实现类:
    FileInputStream
    ByteArrayInputStream
    BufferedInputStream
    DataInputStream
    ObjectInputStream

  6. OutputStream 的实现类:
    FilOutputStream
    ByteArrayOutputStream
    BufferedOutputStream
    DataOutputStream
    ObjectOutputStream
    PrintStream

  7. Reader 的实现类:
    FileReader
    BufferedReader
    InputStreamReader

  8. Writer的实现类
    FileWriter
    BufferedWriter
    OutputStreamWriter

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

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

猜你喜欢

转载自blog.csdn.net/weixin_44626569/article/details/89394574