全面解析IO流一:Java IO流详解

数据流的基本概念

几乎所有的程序都离不开信息的输入和输出,比如从键盘读取数据,从文件中获取或者向文件中存入数据,在显示器上显示数据。这些情况下都会涉及有关输入/输出的处理。

在Java中,把这些不同类型的输入、输出源抽象为流(Stream),其中输入或输出的数据称为数据流(Data Stream),用统一的接口来表示。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。

IO 流的分类

数据流是指一组有顺序的、有起点和终点的字节集合。

①按照流的流向分,可以分为输入流和输出流。

注意:这里的输入、输出是针对程序来说的。

输出:把程序(内存)中的内容输出到磁盘、光盘等存储设备中。

 

 输入:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。

②按处理数据单位不同分为字节流和字符流。

字节流:每次读取(写出)一个字节,当传输的资源文件有中文时,就会出现乱码。

字符流:每次读取(写出)两个字节,有中文时,使用该流就可以正确传输显示中文。

1字符 = 2字节; 1字节(byte) = 8位(bit); 一个汉字占两个字节长度,一个英文占1个字节长度。

③按照流的角色划分为节点流和处理流。

节点流:从或向一个特定的地方(节点)读写数据。如FileInputStream。

处理流(包装流):是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。

 扩展:字节与字符之间的关系

编码方式

英文字符

中文字符

GB 2312、GBK

1

2

UTF-8

1

3-4

UTF-16

2

3-4

UTF-32

4

4

字节流和字符流的区别

1.字节流读取的时候,读到一个字节就返回一个字节; 字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在UTF-8码表中是3个字节)时。先去查指定的编码表,将查到的字符返回。

2.字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件,而字符流只能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。

Java IO 流有4个抽象基类,其他流都是继承于这四大基类的

下图是Java IO 流的整体架构图:

知道了 IO 流有这么多分类,那我们在使用的时候应该怎么选择呢?

比如什么时候用输出流?什么时候用字节流?可以根据下面三步选择适合自己的流:

  • 首先自己要知道是选择输入流还是输出流。这就要根据自己的情况决定,如果想从程序写东西到别的地方,那么就选择输入流,反之就选输出流;
  • 然后考虑你传输数据时,是每次传一个字节还是两个字节,每次传输一个字节就选字节流,如果存在中文,那肯定就要选字符流了。
  • 通过前面两步就可以选出一个合适的节点流了,比如字节输入流 InputStream,如果要在此基础上增强功能,那么就在处理流中选择一个合适的即可。

 一、字节流

1、Inputstream

InputStream有read方法,一次读取一个字节,OutputStream的write方法一次写一个int。这两个类都是抽象类。意味着不能创建对象,那么需要找到具体的子类来使用。

InputStream是所有输入字节流的父类,是一个抽象类。ByteArrayInputStream、StringBufferInputStream、FileInputStream 是三种基本的介质流,它们分别从Byte 数组、StringBuffer、和本地文件中读取数据。PipedInputStream是从与其它线程共用的管道中读取数据。

ObjectInputStream 和所有FilterInputStream 的子类都是装饰流(装饰器模式的主角)。

操作流的步骤都是:

  • 第一步:1:打开流(即创建流)
  • 第二步:2:通过流读取内容
  • 第三步:3:用完后,关闭流资源

案例一:使用 read()方法,一次读取一个字节,读到文件末尾返回-1.

private static void showContent(String path) throws IOException {
        // 打开流
        FileInputStream fis = new FileInputStream(path);

        int len;
        while ((len = fis.read()) != -1) {
            System.out.print((char) len);
        }
        // 使用完关闭流
        fis.close();
}

案例二:使用read()方法的时候,可以将读到的数据装入到字节数组中,一次性的操作数组,可以提高效率。

private static void showContent2(String path) throws IOException {
        // 打开流
        FileInputStream fis = new FileInputStream(path);

        // 通过流读取内容
        byte[] byt = new byte[1024];
        int len = fis.read(byt);
        for (int i = 0; i <len; i++) {
            System.out.print(byt[i]);
      }
    
        // 使用完关闭流
        fis.close();
    }

 2、OutputStream

OutputStream 是所有的输出字节流的父类,它是一个抽象类。

ByteArrayOutputStream、FileOutputStream 是两种基本的介质流,它们分别向Byte 数组、和本地文件中写入数据。PipedOutputStream 是向与其它线程共用的管道中写入数据,

ObjectOutputStream 和所有FilterOutputStream 的子类都是装饰流。

OutputStram 的write方法,一次只能写一个字节。成功的向文件中写入了内容。但是并不高效,如何提高效率呢?可以使用缓冲,在OutputStram类中有write(byte[] b)方法,将 b.length个字节从指定的 byte 数组写入此输出流中。

private static void writeTxtFile(String path) throws IOException {
        // 1:打开文件输出流,流的目的地是指定的文件
        FileOutputStream fos = new FileOutputStream(path,true);

        // 2:通过流向文件写数据
        byte[] byt = "java".getBytes();
        fos.write(byt);

        // 3:用完流后关闭流
        fos.close();
    }

输入输出流综合使用——文件拷贝实现

扩展

(1)void flush():刷新此输出流并强制写出所有缓冲的输出字节。为了加快数据传输速度,提高数据输出效率,又是输出数据流会在提交数据之前把所要输出的数据先暂时保存在内存缓冲区中,然后成批进行输出,每次传输过程都以某特定数据长度为单位进行传输,在这种方式下,数据的末尾一般都会有一部分数据由于数量不够一个批次,而存留在缓冲区里,调用 flush() 方法可以将这部分数据强制提交。

(2)缓冲流

Java其实提供了专门的字节流缓冲来提高效率。BufferedInputStream 和 BufferedOutputStream。BufferedOutputStream和BufferedOutputStream类可以通过减少读写次数来提高输入和输出的速度。它们内部有一个缓冲区,用来提高处理效率。查看API文档,发现可以指定缓冲区的大小。其实内部也是封装了字节数组。没有指定缓冲区大小,默认的字节是8192。显然缓冲区输入流和缓冲区输出流要配合使用。首先缓冲区输入流会将读取到的数据读入缓冲区,当缓冲区满时,或者调用flush方法,缓冲输出流会将数据写出。

注意:当然使用缓冲流来进行提高效率时,对于小文件可能看不到性能的提升。但是文件稍微大一些的话,就可以看到实质的性能提升了。示例:

public class Test {
    public static void main(String[] args) throws IOException {
        String srcPath = "c:\\a.mp3";
        String destPath = "d:\\copy.mp3";
        copyFile(srcPath, destPath);
    }

    public static void copyFile(String srcPath, String destPath)
            throws IOException {
        // 打开输入流,输出流
        FileInputStream fis = new FileInputStream(srcPath);
        FileOutputStream fos = new FileOutputStream(destPath);

        // 使用缓冲流
        BufferedInputStream bis = new BufferedInputStream(fis);
        BufferedOutputStream bos = new BufferedOutputStream(fos);

        // 读取和写入信息
        int len = 0;

        while ((len = bis.read()) != -1) {
            bos.write(len);
        }

        // 关闭流
        bis.close();
        bos.close();    
    }
}

 二、字符流

计算机并不区分二进制文件与文本文件。所有的文件都是以二进制形式来存储的,因此,从本质上说,所有的文件都是二进制文件。所以字符流是建立在字节流之上的,它能够提供字符层次的编码和解码。可以说字符流就是:字节流 + 编码表,为了更便于操作文字数据。字符流的抽象基类:Reader , Writer。由这些类派生出来的子类名称都是以其父类名作为子类名的后缀,如FileReader、FileWriter。

Java使用Unicode字符集来表示字符串和字符。为了实现与其他程序语言及不同平台的交互,Java提供一种新的数据流处理方案,称作读者(Reader)和写者(Writer)。

1、字符输入流Reader

Reader 是所有的输入字符流的父类,它是一个抽象类。

  • CharReader和SringReader是两种基本的介质流,它们分别将Char数组、String中读取数据。
  • PipedReader 是从与其它线程共用的管道中读取数据。
  • BufferedReader很明显是一个装饰器,它和其他子类负责装饰其他Reader对象。
  • FilterReader是所有自定义具体装饰流的父类,其子类PushBackReader对Reader对象进行装饰,会增加一个行号。
  • InputStreamReader是其中最重要的一个,用来在字节输入流和字符输入流之间作为中介,可以将字节输入流转换为字符输入流。FileReader 可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream 转变为Reader 的方法。

Reader 中各个类的用途和使用方法基本和InputStream 中的类使用一致。

2、字符输出流 Writer

Writer是所有的输出字符流的父类,它是一个抽象类。

  • CharWriter、StringWriter 是两种基本的介质流,它们分别向Char 数组、String 中写入数据。
  • PipedWriter 是向与其它线程共用的管道中写入数据。
  • BufferedWriter 是一个装饰器为Writer 提供缓冲功能。
  • PrintWriter 和PrintStream 极其类似,功能和使用也非常相似。
  • OutputStreamWriter是其中最重要的一个,用来在字节输出流和字符输出流之间作为中介,可以将字节输出流转换为字符输出流。FileWriter 可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将OutputStream转变为Writer 的方法。Writer 中各个类的用途和使用方法基本和OutputStream 中的类使用一致。

 下面展示一个字符输入流和字符输出流综合使用的案例:复制文件。

扩展1:

1、转换流

InputStreamReader 是字节流通向字符流的桥梁 
OutputStreamWriter 是字符流通向字节流的桥梁 
转换流可以将字节转成字符,原因在于,将获取到的字节通过查编码表获取到指定对应字符。 转换流的最强功能就是基于 字节流 + 编码表 。没有转换,没有字符流

2、打印流

PrintWriter  和 PrintStream 
注: 
A:只操作目的地,不操作数据源 
B:可以操作任意类型的数据 
C:如果启用了自动刷新,在调用println(),printf(),format()方法的时候,能够换行并刷新 
D:可以直接操作文件
 

扩展2:

1、序列化

把Java对象转换为字节序列的过程称为对象的序列化,也就是将对象写入到IO流中序列化是为了解决在对对象流进行读写操作时所引发的问题。序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。

要对一个对象序列化,这个对象就需要实现Serializable接口,如果这个对象中有一个变量是另一个对象的引用,则引用的对象也要实现Serializable接口,这个过程是递归的。Serializable接口中没有定义任何方法,只是作为一个标记来指示实现该接口的类可以进行序列化。

要实现序列化,只需两步即可:

  • 步骤一:创建一个ObjectOutputStream输出流;
  • 步骤二:调用ObjectOutputStream对象的 writeObject 方法输出可序列化对象。

序列化只能保存对象的非静态成员变量,而不能保存任何成员方法和静态成员变量,并且保存的只是变量的值,变量的修饰符对序列化没有影响。

有一些对象类不具有可持久化性,因为其数据的特性决定了它会经常变化,其状态只是瞬时的,这样的对象是无法保存去状态的,如Thread对象或流对象。对于这样的成员变量,必须用 transient 关键字标明,否则编译器将报错。任何用 transient 关键字标明的成员变量,都不会被保存。

另外,序列化可能涉及将对象存放到磁盘上或在网络上发送数据,这时会产生安全问题。对于一些需要保密的数据(如用户密码等),不应保存在永久介质中,为了保证安全,应在这些变量前加上 transient 关键字

2、反序列化

反序列化就是从 IO 流中恢复对象。

反序列化也只需两步即可完成:

  • 步骤一:创建 ObjectInputStream 输入流
  • 步骤二:调用ObjectInputStream对象的readObject()得到序列化的对象。

控制台只输出了Person的信息,没有输出构造方法中的内容,说明反序列化的对象是由 JVM 自己生成的,不通过构造方法生成。

3、序列化版本号serialVersionUID

我们知道,反序列化必须拥有 class 文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?

java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常:

 

序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。

什么情况下需要修改serialVersionUID呢?分三种情况。

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;如果修改了非瞬态变量,则可能导致反序列化失败。
  • 如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

序列化使用场景

  • 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
  • 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  • 如果想让某个变量不被序列化,使用transient修饰。序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
  • 反序列化时必须有序列化对象的class文件。当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
  • 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
  • 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。

File类

File类是对文件系统中文件以及文件夹进行封装的对象,可以通过对象的思想来操作文件和文件夹。

File类保存文件或目录的各种元数据信息,包括文件名、文件长度、最后修改时间、是否可读、获取当前文件的路径名,判断指定文件是否存在、获得当前目录中的文件列表,创建、删除文件和目录等方法。

RandomAccessFile简介

我们在对文件的操作过程中,除了使用字节流和字符流的方式之外,我们还可以使用RandomAcessFile这个工具类来实现。

RandomAccessFile可以实现对文件的读 和 写,但是他并不是继承于以上4中基本虚拟类。而且在对文件的操作中RandomAccessFile有一个巨大的优势他可以支持文件的随机访问,程序快可以直接跳转到文件的任意地方来读写数据。所以如果需要访问文件的部分内容,而不是把文件从头读到尾,使用RandomAccessFile将是更好的选择。

RandomAccessFile的方法虽然多,但它有一个最大的局限,就是只能读写文件,不能读写其他IO节点。RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传。

mode中,有4中启动的方式:

  • "r" 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
  • "rw" 打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
  • "rws" 打开以便读取和写入,对于 "rw",还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
  • "rwd" 打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到底层存储设备

RandomAccessFile使用:

读取文件内容

 RandomAccessFile raf = new RandomAccessFile(file,"r");
            String s = null;
            while ((s = raf.readLine())!=null){
                System.out.println(s);
            }
            raf.close();

写入文件内容

String text = "写入的内容 \n";
            RandomAccessFile raf = new RandomAccessFile(file,"rw");
            raf.seek(12);   //改变写入偏移的位置,从地12个字节的位置开始写入
            raf.write(text.getBytes());
            raf.close();

注意:RandomAccessFile虽然可以设置了偏移的方法,但他不能实现中间插入的效果,如果你需要实现文本中间插入的话,要先将后面的文件内容拷贝,然后写入,最后在写入的写一行,将拷贝的东西复制回来。

总结:

  1. FileInputStream/FileOutputStream 需要逐个字节处理原始二进制流的时候使用,效率低下。
  2. FileReader/FileWriter 需要组个字符处理的时候使用。
  3. StringReader/StringWriter 需要处理字符串的时候,可以将字符串保存为字符数组。
  4. PrintStream/PrintWriter 用来包装FileOutputStream 对象,方便直接将String字符串写入文件 。
  5. Scanner 用来包装System.in流,很方便地将输入的String字符串转换成需要的数据类型。
  6. InputStreamReader/OutputStreamReader , 字节和字符的转换桥梁,在网络通信或者处理键盘输入的时候用。
  7. BufferedReader/BufferedWriter, BufferedInputStream/BufferedOutputStream, 缓冲流用来包装字节流后者字符流,提升IO性能,BufferedReader还可以方便地读取一行,简化编程。
  8. SequenceInputStream(InputStream s1, InputStream s2)序列流,合并流对象时使用.
  9. ObjectInputStream、ObjectOutputStream,方法用于序列化对象并将它们写入一个流,另一个方法用于读取流并反序列化对象。
  10. ByteArrayInputStream、ByteArrayOutputStream,操作数组
  11. DataInputStream、DataOutputStream操作基本数据类型和字符串。

 

[文章参考]

https://blog.csdn.net/zzy1832/article/details/48154825

https://baijiahao.baidu.com/s?id=1659851047751244423&wfr=spider&for=pc

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章