一、IO概述

1、什么是IO

  • 数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入input输出output ,即流向内存是输入流,流出内存是输出流
  • Java中I/O操作主要是指使用java.io包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做写出数据

2、IO的分类

  • 根据数据的流向分类

    • 输入流 :把数据从其他设备上读取到内存中的流
      • 以InputStream,Reader结尾
    • 输出流 :把数据从内存 中写出到其他设备上的流
      • 以OutputStream、Writer结尾
  • 根据数据的类型分类

    • 字节流 :以字节为单位,读写数据的流
      • 以InputStream和OutputStream结尾
    • 字符流 :以字符为单位,读写数据的流
      • 以Reader和Writer结尾
  • 根据IO流的角色分类

    • 节点流:可以从或向一个特定的地方(节点)读写数据
      • 文件:FileInputStream、FileOutputStrean、FileReader、FileWriter
      • 字符串:StringReader、StringWriter
      • 数组:ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter
      • 管道:PipedInputStream、PipedOutputStream、PipedReader、PipedWriter
    • 处理流:是对一个已存在的流进行连接和封装,通过所封装的流的功能调用实现数据读写。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接
      • 缓冲流:增加缓冲功能,避免频繁读写硬盘,BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
      • 转换流:实现字节流和字符流之间的转换,InputStreamReader、OutputStreamReader
      • 数据流:提供读写Java基础数据类型功能 ,DataInputStream、DataOutputStream
      • 对象流:提供直接读写Java对象功能,ObjectInputStream、ObjectOutputStream

处理流这种设计是装饰模式(Decorator Pattern)也称为包装模式(Wrapper Pattern),其使用一种对客户端透明的方式来动态地扩展对象的功能,它是通过继承扩展功能的替代方案之一

3、4大顶级抽象父类

输入流 输出流
字节流 字节输入流InputStream 字节输出流OutputStream
字符流 字符输入流Reader 字符输出流Writer

二、字节流

1、字节输出流【OutputStream】

  • java.io.OutputStream抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法

    • public void write(int b) :将指定的字节输出流。虽然参数为int类型四个字节,但是只会保留一个字节的信息写出
    • public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流
    • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流
    • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出
    • public void close() :关闭此输出流并释放与此流相关联的任何系统资源
  • java.io.FileOutputStream类是文件输出流,用于将数据写出到文件

    • public FileOutputStream(File file):创建文件输出流以写入由指定的 File对象表示的文件
    • public FileOutputStream(String name): 创建文件输出流以指定的名称写入文件
    • public FileOutputStream(File file, boolean append): 创建文件输出流以写入由指定的 File对象表示的文件
    • public FileOutputStream(String name, boolean append): 创建文件输出流以指定的名称写入文件
  • 当创建一个流对象时,必须传入一个文件路径。如果该文件不存在,会创建该文件。如果有这个文件,会清空这个文件的数据。如果传入的是一个目录,则会报IOException异常

  • 系统中的换行:

    • Windows系统里,每行结尾是 回车+换行 ,即\r\n
    • Unix系统里,每行结尾只有 换行 ,即\n
    • Mac系统里,每行结尾是 回车 ,即\r。从 Mac OS X开始与Linux统一。

2、字节输入流【InputStream】

  • java.io.InputStream抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法

    • public int read(): 从输入流读取一个字节。返回读取的字节值。虽然读取了一个字节,但是会自动提升为int类型。如果已经到达流末尾,没有数据可读,则返回-1
    • public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。每次最多读取b.length个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1
    • public int read(byte[] b,int off,int len):从输入流中读取一些字节数,并将它们存储到字节数组 b中,从b[off]开始存储,每次最多读取len个字节 。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1
    • public void close() :关闭此输入流并释放与此流相关联的任何系统资源
  • java.io.FileInputStream类是文件输入流,从文件中读取字节

    • FileInputStream(File file):通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名
    • FileInputStream(String name):通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名
  • 当创建一个流对象时,必须传入一个文件路径。如果文件不存在,会抛出FileNotFoundException 。如果传入的是一个目录,则会报IOException异常

三、字符流

当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件

字符流只能操作文本文件,不能操作图片,视频等非文本文件。当我只读写文本文件时使用字符流,其他情况使用字节流

1、字符输入流【Reader】

  • java.io.Reader抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法

    • public int read(): 从输入流读取一个字符。 虽然读取了一个字符,但是会自动提升为int类型。返回该字符的Unicode编码值。如果已经到达流末尾了,则返回-1。
    • public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。每次最多读取cbuf.length个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回-1
    • public int read(char[] cbuf,int off,int len):从输入流中读取一些字符,并将它们存储到字符数组 cbuf中,从cbuf[off]开始的位置存储。每次最多读取len个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回-1
    • public void close() :关闭此流并释放与此流相关联的任何系统资源
  • java.io.FileReader类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区

    • FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
    • FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称。
  • 当创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。如果该文件不存在,则报FileNotFoundException。如果传入的是一个目录,则会报IOException异常

2、字符输出流【Writer】

  • java.io.Writer抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法

    • public void write(int c) 写入单个字符
    • public void write(char[] cbuf)写入字符数组
    • public void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数
    • public void write(String str)写入字符串
    • public void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数
    • public void flush()刷新该流的缓冲
    • public void close() 关闭此流,但要先刷新它
  • java.io.FileWriter类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区

    • FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象
    • FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称
    • public FileWriter(File file,boolean append): 创建文件输出流以写入由指定的 File对象表示的文件
    • public FileWriter(String fileName,boolean append): 创建文件输出流以指定的名称写入文件
  • 当创建一个流对象时,必须传入一个文件路径,类似于FileOutputStream。如果文件不存在,则会自动创建。如果文件已经存在,则会清空文件内容,写入新的内容。

因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush 方法

四、缓冲流

  • 缓冲流也叫高效流,按照数据类型分类

    • 字节缓冲流:BufferedInputStreamBufferedOutputStream
    • 字符缓冲流:BufferedReaderBufferedWriter
  • 缓冲流的基本原理:创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率

    • public BufferedInputStream(InputStream in):创建一个新的字节缓冲输入流
    • public BufferedOutputStream(OutputStream out):创建一个新的字节缓冲输出流
    • public BufferedReader(Reader in) :创建一个新的字符缓冲输入流
    • public BufferedWriter(Writer out): 创建一个新的字符缓冲输出流
  • 字符缓冲流的特有方法

    • BufferedReader:public String readLine(): 读一行文字
    • BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.atguigu.buffer;

import org.junit.Test;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class IOClose {
@Test
public void test01() throws IOException {
FileWriter fw = new FileWriter("d:/1.txt");
BufferedWriter bw = new BufferedWriter(fw);

bw.write("hello");

fw.close();
bw.close();//java.io.IOException: Stream closed
/*
缓冲流BufferedWriter,把数据先写到缓冲区,
默认情况下是当缓冲区满,或调用close,或调用flush这些情况才会把缓冲区的数据输出。

bw.close()时,需要把数据从缓冲区的数据输出。

数据的流向: 写到bw(缓冲区)-->fw ->"d:/1.txt"
此时,我先把fw关闭了,bw的数据无法输出了
*/
}

@Test
public void test02() throws IOException {
FileWriter fw = new FileWriter("d:/1.txt");
BufferedWriter bw = new BufferedWriter(fw);

bw.write("hello");

bw.close();
fw.close();
/*
原则:
先关外面的,再关里面的。

例如:
FileWriter fw = new FileWriter("d:/1.txt"); //里面 穿内衣
BufferedWriter bw = new BufferedWriter(fw); //外面 穿外套

关闭
bw.close(); //先关外面的 先脱外套
fw.close(); //再关里面的 再脱内衣
*/
}
}

五、转换流

1、InputStreamReader类

  • 转换流java.io.InputStreamReader,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集

    • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流
    • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流

2、OutputStreamWriter类

  • 转换流java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集

    • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流
    • OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流

六、数据流与对象流

  • DataOutputStream:数据输出流允许应用程序以适当方式将基本 Java 数据类型写入输出流中。然后,应用程序可以使用数据输入流(DataInputStream)将数据读入

  • DataInputStream:数据输入流允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型

  • ObjectOutputStream:将 Java 基本数据类型和对象写入字节输出流中。稍后可以使用 ObjectInputStream 将数据读入。通过在流中使用文件可以实现Java各种基本数据类型的数据以及对象的持久存储。如果流是网络套接字流,则可以在另一台主机上或另一个进程中接收这些数据或重构对象

  • ObjectInputStream:ObjectInputStream 对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化

    • public ObjectOutputStream(OutputStream out): 创建一个指定OutputStream的ObjectOutputStream
    • public ObjectInputStream(InputStream in): 创建一个指定InputStream的ObjectInputStream

1、使用对象流读写各种类型的数据

  • ObjectOutpuStream也从OutputStream父类中继承基本方法:

    • public void write(int b) :将指定的字节输出流。虽然参数为int类型四个字节,但是只会保留一个字节的信息写出
    • public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流
    • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流
    • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出
    • public void close() :关闭此输出流并释放与此流相关联的任何系统资源
    • writeBoolean():写入一个boolean值
    • writeByte():写入一个8位字节
    • writeShort():写入一个16位的short值
    • writeChar():写入一个16位的char值
    • writeInt():写入一个32位的int值
    • writeLong():写入一个64位的long值
    • writeFloat():写入一个32位的float值
    • writeDouble():写入一个64位的double值
    • public void writeUTF(String str):将表示长度信息的两个字节写入输出流,后跟字符串 s 中每个字符的 UTF-8 修改版表示形式。如果 s 为 null,则抛出 NullPointerException。根据字符的值,将字符串 s 中每个字符转换成一个字节、两个字节或三个字节的字节组。注意,将 String 作为基本数据写入流中与将它作为 Object 写入流中明显不同
  • ObjectInputStream除了从InputStream父类中继承基本方法之外,还有以下方法

    • public int read(): 从输入流读取一个字节。返回读取的字节值。虽然读取了一个字节,但是会自动提升为int类型。如果已经到达流末尾,没有数据可读,则返回-1
    • public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。每次最多读取b.length个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1
    • public int read(byte[] b,int off,int len):从输入流中读取一些字节数,并将它们存储到字节数组 b中,从b[off]开始存储,每次最多读取len个字节 。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1
    • public void close() :关闭此输入流并释放与此流相关联的任何系统资源
    • readBoolean():读取一个boolean值
    • readByte():读取一个8位的字节
    • readShort():读取一个16位的short值
    • readChar():读取一个16位的char 值
    • readInt():读取一个32位的int 值
    • readLong():读取一个64位的long值
    • readFloat():读取一个32位的float值
    • readDouble():读取一个 64位的double值
    • readUTF():读取UTF-8修改版格式的String

2、序列化与反序列化概念

  • Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的类型对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息

  • 反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据对象的类型对象中存储的数据信息,都可以用来在内存中创建对象

  • ObjectOutputStream流中支持序列化的方法是:

    • public final void writeObject (Object obj) : 将指定的对象写出
  • ObjectInputStream流中支持反序列化的方法是:

    • public final Object readObject () : 读取一个对象

3、Serializable序列化接口与transient关键字

  • 某个类的对象需要序列化输出时,该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
  • 如果对象的某个属性也是引用数据类型,那么如果该属性也要序列化的话,也要实现Serializable 接口
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰
  • 静态变量的值不会序列化。因为静态变量的值不属于某个对象

如果有多个对象需要序列化,则可以将对象放到集合中,再序列化集合对象即可

4、反序列化失败问题

  • 首先,对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常

  • 其次,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。发生这个异常的原因如下:

    • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
    • 该类包含未知数据类型
  • Serializable 接口给需要序列化的类,提供了一个序列版本号。serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。如果没有声明serialVersionUID,则每次编译都会产生新的serialVersionUID序列化版本ID值,这样如果在序列化完成之后修改了类导致类重新编译,则原来的数据将无法反序列化。所以通常我们都会在实现Serializable接口时,声明一个serialVersionUID,并为其指定一个值。serialVersionUID必须是static和final修饰的long类型的数据,它的值由程序员随意指定即可

  • 如果声明了serialVersionUID,即使在序列化完成之后修改了类导致类重新编译,则原来的数据也能正常反序列化,只是新增的字段值是默认值而已