概述

流是一组有序的,有起点和终点的字节集合,是对数据传输的总称和抽象。即数据在两设备之间的传输称为流,流的本质是数据传输,根据数据传输的特性将流抽象为各种类,方便进行更直观的数据操作。

Java 中 IO 流概览:
IO流.png

流的分类:

  • 根据处理数据的不同分类:字节流和字符流
  • 根据数据流向的不同分类:输入流和输出流

    一切文件数据(文本、图片、视频等)在存储时,都是以二进制的形式保存;同样,文件数据在传输时也是按照二进制的形式,一个字节一个字节的传输。因此,字节流可以传输任何文件,同样,字符流,本质其实就是基于字节流读取时,去查了指定的码表

根据上面的分类产生的四个顶级父类:

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

OutputStream

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

void close() 关闭此输出流并释放与此流相关联的任何系统资源
void flush() 刷新此输出流并强制任何缓冲的输出字节被写出
void write(byte[] b) 将 b.length 字节从指定的字节数组写入此输出流
void write(byte[] b, int offset, int len) 从指定的字节数组写入 len 字节,从下标 off 开始,长度为 len 的字节写入到输出流
abstract void write(int b) 抽象类,将b写入到输出流,b是int类型,占有32位,写入过程是写入b的8个低位,b的24个高位将被忽略

字节输出流的子类:

  1. 介质流:
    • ByteArrayOutputStream:Byte 数组中写入数据;
    • FileOutputStream :本地文件中写入数据;
    • PipedOutputStream:是向与其它线程共用的管道中写入数据;
  2. 装饰流:ObjectOutputStream 和所有FilterOutputStream 的子类都是装饰流。

InputStream

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

void close() 关闭此输入流并释放与此流相关联的任何系统资源
abstract int read() 抽象类,读取1字节,返回0~255内的int字节值。如果已经到达流末尾,而且没有可用的字节,则返回值-1
int read(byte[] b) 读取多字节,数据放到字节数组b中,返回值为实际读取的字节的数量。如果已经到达流末尾,而且没有可用的字节,则返回值-1
int read(byte[] b, int off, int len) 最多读取len字节,数据放到以下标off开始的字节数组b中,将读取的第一个字节存储在元素b[off]中,下一个存储在b[off+1]中,以此类推。返回值为实际读取的字节的数量。如果已经到达流末尾,而且没有可用的字节,则返回值-1

字节输入流的子类:

  1. 介质流:
    • ByteArrayInputStream:Byte 数组中读取数据;
    • StringBufferInputStream:StringBuffer中读取数据;
    • FileInputStream:本地文件中读取数据;
    • PipedInputStream:管道字节输入流,它和PipedOutputStream一起使用,能实现多线程间的管道通信;
  2. 装饰流:ObjectInputStream 和所有FilterInputStream 的子类都是装饰流(装饰器模式的主角)
    • FilterInputStream:装饰者模式中处于装饰者,具体的装饰者都要继承它,所以在该类的子类下都是用来装饰别的流的,也就是处理类
    • DataInputStream:数据输入流,它是用来装饰其它输入流,它“允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型”
    • ObjectInputStream:对象输入流,用来提供对“基本数据或对象”的持久存储。通俗点讲,也就是能直接传输对象(反序列化中使用)

Reader

public abstract class Reader implements Readable, Closeable:Reader 是所有的输入字符流的父类,它是一个抽象类
常用方法:

abstract void close() 关闭流并释放与之关联的所有资源
boolean markSupported() 判断流是否支持 mark() 操作
void mark(int readAheadLimit) 标记流中的当前位置
void reset() 重置该流
long skip(long n) 跳过字符
boolean ready() 判断是否准备读取此流
int read() 读取单个字符
int read(char[] cbuf) 将字符读入数组
int read(char[] cbuf,int off,int len) 将字符读入数组的某一部分
int read(CharBuffer target) 试图将字符读入指定的字符缓冲区

子类:

  1. 介质流:
    • CharReader:从Char数组读取数据
    • StringReader :从String中读取数据
    • PipedReader:是从与其它线程共用的管道中读取数据
  2. 装饰流:
    • BufferedReader:装饰器,它和其子类负责装饰其它Reader 对象
    • FilterReader:是所有自定义具体装饰流的父类,其子类PushbackReader 对Reader 对象进行装饰,会增加一个行号
    • InputStreamReader :一个连接字节流和字符流的桥梁,它将字节流转变为字符流
    • FileReader:可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream 转变为Reader的方法。我们可以从这个类中得到一定的技巧。Reader 中各个类的用途和使用方法基本和InputStream中的类使用一致。后面会有Reader 与InputStream 的对应关系

Writer

public abstract class Writer implements Appendable, Closeable, Flushable:Writer 是所有的输出字符流的父类,它是一个抽象类
常用方法:

void write(int c) 写入单个字符
void write(char[] cbuf) 写入字符数组
abstract void write(char[] cbuf,int off,int len) 写入字符数组的某一部分,off为数组的开始索引,len为写的字符个数
void write(String str) 写入字符串
void write(String str, int off, int len) 写入字符串的某一部分,off为字符串的开始索引,len为写的字符个数
void flush() 刷新该流的缓冲
void close() 关闭此流,但要先刷新它

子类:

  1. 介质流:
    • CharArrayWriter:向Char数组写入数据;
    • StringWriter:向String中写入数据;
    • PipedWriter:是向与其它线程共用的管道中写入数据;
  2. 装饰流:
    • BufferedWriter:是一个装饰器为Writer 提供缓冲功能;
    • PrintWriter 和PrintStream:极其类似,功能和使用也非常相似;
    • OutputStreamWriter 是OutputStream 到Writer 转换的桥梁,它的子类FileWriter是实现此功能的具体类;

文件流

1、java.io.FileOutputStream extends OutputStream:文件字节输出流,把内存中的数据写入到硬盘的文件中
构造方法:

FileOutputStream(String name [, boolean append]) 创建一个向具有指定名称的文件中写入数据的输出文件流,name,文件路径,append,可选参数,是否追加写入文件
FileOutputStream(File file [, boolean append]) 创建一个向指定 File 对象表示的文件中写入数据的文件输出流

构造方法的作用:

  1. 创建一个FileOutputStream对象
  2. 会根据构造方法中传递的文件/文件路径,创建一个空的文件
  3. 会把FileOutputStream对象指向创建好的文件

写入数据的步骤(内存 -> 硬盘):java程序 -> JVM(java虚拟机) -> OS(操作系统) -> OS调用写数据的方法 -> 把数据写入到文件中文件写入步骤:

FileOutputStream实现了Closable接口的close()方法,所以建议使用 try-resource-catch 语法

  1. 创建一个FileOutputStream对象,构造方法中传递写入数据的目的地
  2. 调用FileOutputStream对象中的方法write,把数据写入到文件中
  3. 释放资源(流使用会占用一定的内存,使用完毕要把内存清空,提升程序的效率)

一次写多个字节:

  • 如果写的第一个字节是正数(0-127),那么显示的会查询ASCII表
  • 如果写的第一个字节是负数,那么第一个字节会和第二个字节,两个字节组成一个中文显示,查询系统默认码表(GBK)

2、java.io.FileInputStream extends InputStream:文件字节输入流

  1. 构造方法:
    • public FileInputStream(String name) throws FileNotFoundException
    • public FileInputStream(File file) throws FileNotFoundException
  2. 读取文件的数据源,支持从 name(文件路径)和 file(文件)获取
  3. 读取数据的方法:
    • int read():读取文件中的一个字节并返回,读取到文件的末尾返回 -1**

3、java.io.FileReader:继承 InputStreamReader 类,按字符读取流中数据
构造方法:

FileReader(File file) 从给定 File 中获取一个 FileReader
FileReader(FileDescriptor fd) 从给定 FileDescriptor 中创建一个新的 FileReader
FileReader(String fileName) 从给定文件名中创建一个新的 FileReader

4、FileWriter:继承 OutputStreamWriter 类,按字符向流中写入数据
构造方法:

FileWriter(File file) 使用给定的 file 创建一个 FileWriter 对象
FileWriter(File file, boolean append) append:true 追加写入,false 覆盖
FileWriter(FileDescriptor fd) 使用文件描述符创建 FileWriter 对象
FileWriter(String fileName, boolean append) 使用文件名创建 FileWriter 对象

缓冲流

缓冲流:也叫高效流、装饰流,是对 4 个基本流的增强,使用了装饰器模式,因此可以分为如下类别:

输入流 输出流
字节流 BufferedInputStream BufferedOutputStream
字符流 BufferedReader BufferedWriter

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

1、字节缓冲输入流:java.io.BufferedInputStream extends InputStream

BufferedInputStream(InputStream in) 创建一个新的字节缓冲输入流
BufferedInputStream(InputStream in, int size) 创建一个具有指定缓冲区大小的字节缓冲输入流

2、字节缓冲输出流:java.io.BufferedOutputStream extends OutputStream

BufferedOutputStream(OutputStream out) 创建一个新的字节缓冲输出流
BufferedOutputStream(OutputStream out,int size) 创建一个具有指定缓冲区大小的字节缓冲输出流

3、字符缓冲输入流:public class BufferedReader extends Reader

注意特殊方法:

  • String readline():读取一个文本行,返回值为不包含终止符(\r、\n、\r\n)的字符串,如果已经到达流末尾,返回 null
BufferedReader(Reader in) 创建一个新的字符缓冲输入流
BufferedReader(Reader in, int sz) 创建一个指定缓冲区大小的字节缓冲输入流

4、字符缓冲输出流:public class BufferedWriter extends Writer

注意特殊方法:

  • public void newLine() throws IOException:创建新行,会根据系统类型自动选择,而无需自己手动指定
  • flush():在写入后记得使用 flush 方法把缓冲区的数据冲刷到硬盘上
BufferedWriter(Writer out) 创建一个新的字符缓冲输出流
BufferedWriter(Writer out, int sz) 创建指定大小的字符缓冲输出流


转换流

在字节流与字符流转换时,涉及字符的编码与解码问题,因此需要转换流,java中的转换流有两种:

  • 字节流转换为字符流(解码):InputStreamReader类
  • 字符流转换为字节流(编码):OutputStreamWriter类

    java中的 FileReader 和 FileWriter 会自动查询系统的字符集进行编解码

在JVM中使用的是双字节的UTF-8,因此在读取、写入文件中的中文字符时,有可能出现乱码问题

对象的序列化

Java 的序列化和反序列化:

  • 序列化:将保存在内存中的对象数据转化为二进制数据流进行传输,任何对象都可以序列化
  • 反序列化:将二进制数据转换为对象

注意:

  • 序列化和反序列化只能序列化对象的属性,且不包括被 transient 标记的属性
  • static 修饰的静态变量为 Class 对象持有,不属于对象的属性,也不能被序列化和反序列化

1、序列化:实现 java.io.Serializable 接口

  • serialVersionUID:序列号,用于唯一标识一个类
  • 编译器 javac.exe 会为每个实现了 Serializable 接口的类添加一个序列号 serialVersionUID,反序列化的时候会根据该序列号唯一标识一个类,然而如果修改了原类,在重新编译的时候会生成一个新的 serializableUID,导致反序列化错误,因此在实现 Serializable 接口的同时,应该定义一个 private static final long serialVersionUID

2、反序列化:

  • 构造方法:ObjectInputStream(InputStream in)
  • 常用方法:Object readObject() 从 ObjectInputStream 读取对象

序列化和反序列化:

  1. /* User 类 */
  2. public class User implements Serializable {
  3. private static final long serialVersionUID = 8749345376788010604L;
  4. static int y = 0;
  5. private String name;
  6. private Integer age;
  7. private transient String sex;
  8. public User(String name, Integer age, String sex) {
  9. this.name = name;
  10. this.age = age;
  11. this.sex = sex;
  12. }
  13. @Override
  14. public String toString() {
  15. return "User{" +
  16. "name='" + name + '\'' +
  17. ", age=" + age +
  18. ", sex='" + sex + '\'' +
  19. '}';
  20. }
  21. }
  22. /* 测试 */
  23. // 写对象
  24. try (ObjectOutputStream oos =
  25. new ObjectOutputStream(
  26. new FileOutputStream("/user"))) {
  27. User user = new User("漳卅",18,"男");
  28. oos.writeObject(user);
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. }
  32. // 读对象
  33. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/user"))) {
  34. User user = (User) ois.readObject();
  35. System.out.println(user);
  36. // 输出:User{name='漳卅', age=18, sex='null'}
  37. } catch (IOException | ClassNotFoundException e) {
  38. e.printStackTrace();
  39. }
  40. /* 写入对象集合 */
  41. String target = "/users";
  42. try (FileOutputStream fos = new FileOutputStream(target);
  43. ObjectOutputStream oos = new ObjectOutputStream(fos);) {
  44. // 创建对象集合
  45. ArrayList<User> users = new ArrayList<>();
  46. users.add(new User("Bob", 12, "man"));
  47. users.add(new User("Tom", 20, "woman"));
  48. users.add(new User("Dave", 32, "man"));
  49. // 写入对象
  50. oos.writeObject(users);
  51. } catch (IOException e) {
  52. e.printStackTrace();
  53. }
  54. /* 读取集合并打印 */
  55. try (FileInputStream fis = new FileInputStream(target);
  56. ObjectInputStream ois = new ObjectInputStream(fis)) {
  57. ArrayList<User> users = (ArrayList<User>) ois.readObject();
  58. for (User user : users) {
  59. System.out.println(user);
  60. }
  61. } catch (IOException | ClassNotFoundException e) {
  62. e.printStackTrace();
  63. }

PrintStream 和 Scanner

1、java.io.PrintStream extends OutputStream:打印流,为其他输出流添加了功能,使它们能够方便的打印出各种数据值表示形式
PrintStream特点:

  1. 只负责数据的输出,不负责数据的读取
  2. 与其他输出流不同,PrintStream永远不会抛出 IOException
  3. 有其特有的方法:
    • void print(任意类型的值)
    • void println(任意类型的值并换行)
  4. 构造方法:
    • PrintStream(File file)
    • PrintStream(OutputStream out)
    • PrintStream(String fileName)
  5. 注意事项:PrintStream 继承了 OutputStream ,然而重写了父类的 write() 方法,查看数据的时候会查询编码表;而其自身特有的 print/println 方法写数据,写的时候会原样输出

2、System类对IO的支持:针对一些频繁的设备交互,Java语言系统预定了3个可以直接使用的流对象,分别是:

  • System.in(标准输入),通常代表键盘输入;
  • System.out(标准输出,为PrintStream):通常写往显示器;

    可以使用System.setOut(PrintStream out)方法通过传入打印流改变输出的目的地

  • System.err(标准错误输出):通常写往显示器;

    可以使用System.setErr(PrintStream err)方法通过传入打印流改变错误输出的目的地

Java程序可通过命令行参数与外界进行简短的信息交换,同时,也规定了与标准输入、输出设备,如键盘、显示器进行信息交换的方式。而通过文件可以与外界进行任意数据形式的信息交换。

3、Scanner
Java 5添加了java.util.Scanner类,这是一个用于扫描输入文本的新的实用程序。它是以前的StringTokenizer和Matcher类之间的某种结合。由于任何数据都必须通过同一模式的捕获组检索或通过使用一个索引来检索文本的各个部分。于是可以结合使用正则表达式和从输入流中检索特定类型数据项的方法。这样,除了能使用正则表达式之外,Scanner类还可以任意地对字符串和基本类型(如int和double)的数据进行分析。借助于Scanner,可以针对任何要处理的文本内容编写自定义的语法分析器。
Scanner套接字节流或字符流:

  • 字节流的套接:在Scanner的构造方法中Scanner(InputStream source),InputStream只要经过适当的套接,总能获得你想要的流接口。
  • 字符流的套接:Scanner(Readable source),你需要使用Java SE5中新加入的一个接口Readable,该接口表示“具有read()方法的某种东西”,查看Readable接口的API你可以发现你想要的带有Reader的类基本都在其中。

小案例

1、使用文件输入、输出字节流进行文件复制:

  1. public void fileCopyWithStream(File source, File target) {
  2. try (FileInputStream fis = new FileInputStream(source); // 获取文件输入流
  3. // 获取文件输出流
  4. FileOutputStream fos = new FileOutputStream(target);) {
  5. // 缓冲数组
  6. byte[] bytes = new byte[1024];
  7. // 每次读取的字节数
  8. int read = 0;
  9. while ((read = fis.read(bytes)) != -1) {
  10. // 将读取的数据写入文件
  11. fos.write(bytes, 0, read);
  12. }
  13. // 冲刷缓冲区
  14. fos.flush();
  15. } catch (IOException e) {
  16. e.printStackTrace();
  17. }
  18. }

2、使用字符流进行文件复制:

  1. public void fileCopyWithReaderWriter(File source, File target) {
  2. try (FileWriter fw = new FileWriter(target); // 获取写文件字符流
  3. // 获取读文件字符流
  4. FileReader fr = new FileReader(source)) {
  5. // 字符数组
  6. char[] cs = new char[1024];
  7. // 每次读取的字符数
  8. int read = 0;
  9. while ((read = fr.read(cs)) != -1) {
  10. // 将读取的字符写入文件
  11. fw.write(cs, 0, read);
  12. }
  13. // 将字符输出流的数据清空确保完全写入
  14. fw.flush();
  15. } catch (IOException e) {
  16. e.printStackTrace();
  17. }
  18. }

3、BufferedReader 与 BufferedWriter 使用:从控制台获取字符并写入到文件中

  1. public static void consoleToFile(File file) {
  2. BufferedReader reader = null;
  3. BufferedWriter writer = null;
  4. try {
  5. // 将控制台的字节输入流转换成字符缓冲流
  6. // InputStreamReader为中间流,用于将字节输入流转换成字符输入流
  7. reader = new BufferedReader(new InputStreamReader(System.in));
  8. // 将字符输出流转换成字节输出流写入文件
  9. // OutputStreamWriter为中间流,用于将字符输出流转换成字节输出流
  10. // 此处也可以使用文件字符输出流的包装类:FileWriter
  11. // 等效写法:writer = new BufferedWriter(new FileWriter(file));
  12. writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
  13. // 用于接受从字符缓冲区的数据
  14. String msg = null;
  15. // BufferedReader.readLine()方法会一直等待输入,直到碰到输入字符中存在\t\n等换行符才会完成读取
  16. while ((msg = reader.readLine()) != null) {
  17. // 使用exit终止写入
  18. if ("exit".equalsIgnoreCase(msg)) break;
  19. // 将数据写入文件
  20. writer.write(msg);
  21. // 写入换行符
  22. writer.newLine();
  23. }
  24. // 冲刷写缓冲
  25. writer.flush();
  26. } catch (IOException e) {
  27. e.printStackTrace();
  28. } finally {
  29. close(reader);
  30. close(writer);
  31. }
  32. }
  33. private static void close(Closeable closeable) {
  34. if (closeable != null) {
  35. try {
  36. closeable.close();
  37. } catch (IOException e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. }

4、文件编码转换:

  1. public static void utf8ToGbk(File source, File target) {
  2. BufferedWriter bw = null;
  3. BufferedReader br = null;
  4. try {
  5. // 1.以UTF-8读取文件
  6. InputStreamReader inputStreamReader = new InputStreamReader(
  7. new FileInputStream(source), StandardCharsets.UTF_8);
  8. br = new BufferedReader(inputStreamReader);
  9. // 2.以GBK写入文件
  10. OutputStreamWriter outputStreamWriter = new OutputStreamWriter(
  11. new FileOutputStream(target), Charset.forName("GBK"));
  12. bw = new BufferedWriter(outputStreamWriter);
  13. String str = null;
  14. // 追行读取
  15. while ((str = br.readLine()) != null) {
  16. bw.write(str);
  17. bw.newLine();
  18. }
  19. bw.flush();
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. } finally {
  23. close(br);
  24. close(bw);
  25. }
  26. }
  27. private static void close(Closeable closeable) {
  28. if (closeable != null) {
  29. try {
  30. closeable.close();
  31. } catch (IOException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }

字节输入流与字节输出流

字节流输入与输出的对应:蓝色的为主要的对应部分,红色的部分就是不对应部分。紫色的虚线部分代表这些流一般要搭配使用
IO 流 - 图2

  • LineNumberInputStream:主要完成从流中读取数据时,会得到相应的行号,至于什么时候分行、在哪里分行是由该类主动确定的,并不是在原始中有这样一个行号。在输出部分没有对应的部分,我们完全可以自己建立一个LineNumberOutputStream,在最初写入时会有一个基准的行号,以后每次遇到换行时会在下一行添加一个行号,看起来也是可以的。
  • PushbackInputStream:查看最后一个字节,不满意就放入缓冲区。主要用在编译器的语法、词法分析部分。输出部分的BufferedOutputStream几乎实现相近的功能。
  • StringBufferInputStream 已经被Deprecated,本身就不应该出现在InputStream部分,主要因为String 应该属于字符流的范围。已经被废弃了,当然输出部分也没有必要需要它了!还允许它存在只是为了保持版本的向下兼容而已。
  • SequenceInputStream 可以认为是一个工具类,将两个或者多个输入流当成一个输入流依次读取。完全可以从IO 包中去除,还完全不影响IO 包的结构,却让其更“纯洁”――纯洁的Decorator 模式。
  • PrintStream 也可以认为是一个辅助工具。主要可以向其他输出流,或者FileInputStream写入数据,本身内部实现还是带缓冲的。本质上是对其它流的综合运用的一个工具而已。System.out 和System.out 就是PrintStream 的实例。