Java中I/O操作是实现输入和输出的基础,可以方便的实现数据的输入和输出操作 在java中把不同的输入/输出源(键盘,文件,网络连接等)抽象表述为“流”(stream)。通过流的形式允许java程序使用相同的方式来访问不同的输入/输出源。stram是从起源(source)到接收的(sink)的有序数据。 注:java把所有的传统的流类型都放到在java io包下,用于实现输入和输出功能

  • 输入也叫做读取数据
  • 输出也叫做作写出数据

流:代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象 流的本质:数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。

Java IO所采用的模型

Java的IO模型设计非常优秀,它使用Decorator(装饰者)模式,按功能划分Stream,您可以动态装配这些Stream,以便获得您需要的功能
例如,您需要一个具有缓冲的文件输入流,则应当组合使用FileInputStream和BufferedInputStream

Io流的分类

按照不同的分类方式,可以把流分为不同的类型。常用的分类有三种:

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

  • 输入流 只能从中读取数据(设备数据 -> 内存)
  • 输出流 只能向其写入数据(内存 -> 设备数据)

Java IO流 - 图1
对于如图15.1所示的数据流向,数据从内存到硬盘,通常称为输出流。也就是说,这里的输入,输出都是从程序运行所在的内存的角度 来划分的
对于如图15.2所示的数据流向,数据从服务器通过网络流向客户端,在这种情况下:

  • Server端的内存负责将数据输出到网络里,因此Server端的程序使用输出流
  • Client端的内存负责从网络中读取数据,因此Client端的程序应该使用输入流

⚠️ 注意: java的输入流主要是InputStream和Reader作为基类,而输出流则是主要由outputStream和Writer作为基类。它们都是一些抽象基类,无法直接创建实例

按照操作单元划分,可以划分为字节流和字符流

字节流和字符流的用法几乎完成全一样,区别在于字节流和字符流所操作的数据单元不同:

  • 字节流操作的单元是数据单元是8位的字节
    • 由 InputStream 和 outPutStream 作为基类
  • 字符流操作的是数据单元为16位的字符
    • 主要有 Reader 和 Writer 作为基类

⚠️ 注意: 使用字节流读取文件为中文的时候,GBK占用2个字节(byte),UTF-8占用3个字节

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

  • 节点流 可以从/向一个特定的IO设备(如磁盘,网络)读/写数据的流。如FileInputStream。节点流也被称为低级流
  • 处理流 处理流则用于对一个已存在的流进行连接和封装,通过封装后的流来实现数据的读/写功能。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装如BufferedReader。处理流也被称为高级流

Java IO流 - 图2

  • 从图15.3中可以看出,当使用节点流进行输入和输出时,程序直接连接到实际的数据源,和实际的输入/输出节点连接
  • 从图15.4可以看出,当使用处理流进行输入/输出时,程序并不会直接连接到实际的数据源,没有和实际的输入和输出节点连接。使用处理流的一个明显的好处是,只要使用相同的处理流,程序就可以采用完全相同的输入/输出代码来访问不同的数据源,随着处理流所包装的节点流的变化,程序实际所访问的数据源也相应的发生变化

流的原理浅析和常用的流的分类表

流的原理浅析

java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java Io流的40多个类都是从如下4个抽象类基类中派生出来的

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

Java IO流 - 图3

  • 对于InputStream和Reader而言,它们把输入设备抽象成为一个”水管“,这个水管的每个“水滴”依次排列
  • 从图15.5可以看出,字节流和字符流的处理方式其实很相似,只是它们处理的输入/输出单位不同而已。输入流使用隐式的记录指针来表示当前正准备从哪个“水滴”开始读取,每当程序从InputStream或者Reader里面取出一个或者多个“水滴”后,记录指针自定向后移动;除此之外,InputStream和Reader里面都提供了一些方法来控制记录指针的移动

Java IO流 - 图4

  • 对于OutputStream和Writer而言,它们同样把输出设备抽象成一个”水管“,只是这个水管里面没有任何水滴
  • 正如图15.6所示,当执行输出时,程序相当于依次把“水滴”放入到输出流的水管中,输出流同样采用隐示指针来标识当前水滴即将放入的位置,每当程序向OutputStream或者Writer里面输出一个或者多个水滴后,记录指针自动向后移动。
  • 图15.5和图15.6显示了java Io的基本概念模型,除此之外,Java的处理流模型则体现了Java输入和输出流设计的灵活性。处理流的功能主要体现在以下两个方面。
    • 性能的提高:主要以增加缓冲的方式来提供输入和输出的效率。
    • 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入和输出大批量的内容,而不是输入/输出一个或者多个“水滴”。
  • 处理流可以“嫁接”在任何已存在的流的基础之上,这就允许Java应用程序采用相同的代码,透明的方式来访问不同的输入和输出设备的数据流。图15.7显示了处理流的模型

Java输入/输出流体系中常用的流的分类表

⚠️Tip: 表中粗体字所标出的类代表节点流,必须直接与指定的物理节点关联:斜体字标出的类代表抽象基类,无法直接创建实例

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
访问管道 PipedInputStream PipedOutputStream PipedReader PipedWriter
访问字符串 StringReader StringWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
抽象基类 FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流 PrintStream PrintWriter
推回输入流 PushbackInputStream PushbackReader
特殊流 DataInputStream DataOutputStream

image.png
image.png


常用的io流的用法

下面是整理的常用的Io流的特性及使用方法,只有清楚每个Io流的特性和方法。才能在不同的需求面前正确的选择对应的IO流进行开发
Io体系的基类(InputStream/Reader,OutputStream/Writer), 字节流和字符流的操作方式基本一致,只是操作的数据单元不同

  • 字节流的操作单元是字节
  • 字符流的操作单元是字符

所以字节流和字符流就整理在一起了

InputStream和Reader是所有输入流的抽象基类

本身并不能创建实例来执行输入,但它们将成为所有输入流的模板,它们的方法是所有输入流都可使用的方法。
读取数据的原理(内存 -> 硬盘): java程序->JVM(java虚拟机)->OS(操作系统)->OS调用读数据的方法—>把数据读取到内存中
在InputStream里面包含如下3个方法

方法 说明
int read() 从输入流中读取单个字节(相当于从图15.5所示的水管中取出一滴水),返回所读取的字节数据(字节数据可直接转换为int类型)
int read(byte[] b) 从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数
int read(byte[] b,int off,int len) 从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入数组b中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数
void mark(int readAheadLimit) 在记录指针当前位置记录一个标记(mark)
boolean markSupported() 判断此输入流是否支持mark()操作,即是否支持记录标记
void reset() 将此流的记录指针重新定位到上一次记录标记(mark)的位置
long skip(long n) 记录指针向前移动n个字节/字符
void close() 关闭此输入流并释放与此流相关联的任何系统资源

在Reader中包含如下3个方法

方法 说明
int read() 从输入流中读取单个字符(相当于从图15.5所示的水管中取出一滴水),返回所读取的字符数据(字节数据可直接转换为int类型)
int read(char[] b) 从输入流中最多读取b.length个字符的数据,并将其存储在字节数组b中,返回实际读取的字符数
int read(char[] b,int off,int len) 从输入流中最多读取len个字符的数据,并将其存储在数组b中,放入数组b中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字符数
void mark(int readAheadLimit) 在记录指针当前位置记录一个标记(mark)
boolean markSupported() 判断此输入流是否支持mark()操作,即是否支持记录标记
void reset() 将此流的记录指针重新定位到上一次记录标记(mark)的位置
long skip(long n) 记录指针向前移动n个字节/字符
void close() 关闭此输入流并释放与此流相关联的任何系统资源

对比InputStream和Reader所提供的方法,就不难发现这两个基类的功能基本是一样的

  • InputStream和Reader都是将输入数据抽象成如图15.5所示的水管,所以程序即可以通过read()方法每次读取一个”水滴“
  • 也可以通过read(char[] chuf)或者read(byte[] b)方法来读取多个“水滴”。
  • 当使用数组作为read()方法中的参数, 我们可以理解为使用一个“竹筒”到如图15.5所示的水管中取水
  • read(char[] cbuf)方法的参数可以理解成一个”竹筒“,程序每次调用输入流read(char[] cbuf)或read(byte[] b)方法,就相当于用“竹筒”从输入流中取出一筒“水滴”,程序得到“竹筒”里面的”水滴“后,转换成相应的数据即可;
  • 程序多次重复这个“取水”过程,直到最后。程序如何判断取水取到了最后呢?直到read(char[] chuf)或者read(byte[] b)方法返回-1,即表明到了输入流的结束点
    Java IO流 - 图7

OutputStream和Writer是所有输出流的抽象基类

写入数据的原理(内存->硬盘): java程序->JVM(java虚拟机)->OS(操作系统)->OS调用写数据的方法->把数据写入到文件中
OutputStream和Writer的用法也非常相似,两个流都提供了如下三个方法:

方法 说明
void write(int c) 将指定的字节输出到输出流中,其中c即可以代表字节,也可以代表字符
void write(byte[] buf) 将字节数组中的数据输出到指定输出流中
void write(byte[] buf, int off,int len ) 将字节数组中从off位置开始,长度为len的字节输出到输出流中
public void flush() 刷新此输出流并强制任何缓冲的输出字节被写出
public void close() 关闭此输出流并释放与此流相关联的任何系统资源

因为字符流直接以字符作为操作单位,所以Writer可以用字符串来代替字符数组,即以String对象作为参数。Writer里面还包含如下两个方法

方法 说明
void write(int c) 将指定的字符输出到输出流中,其中c即可以代表字节,也可以代表字符
void write(char[] buf) 将字符数组中的数据输出到指定输出流中
void write(char[] buf, int off,int len ) 将字符数组中从off位置开始,长度为len的字符输出到输出流中
void write(String str) 将str字符串里包含的字符输出到指定输出流中
void write (String str, int off, int len) 将str字符串里面从off位置开始,长度为len的字符输出到指定输出流中
void flush() 刷新缓冲区,流对象可以继续使用。
void close() 关闭此流
先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了

IO体系的基类文件流的使用

FileInputStream/FileReader ,FileOutputStream/FileWriter
前面说过InputStream和Reader都是抽象类,本身不能创建实例,但是继承它们的子类有对应的功能:

  • FileInputStream
  • FileOutputStream
  • FileReader
  • Writer

FileInputStream读取文件

  1. 构造方法
  2. 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名
  3. FileInputStream(File file)
  4. 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名
  5. FileInputStream(String name)

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException

  1. import java.io.FileInputStream;
  2. import java.io.IOException;
  3. public class FileInputStreamTest {
  4. public static void main(String[] args) throws IOException {
  5. FileInputStream fis = null;
  6. try {
  7. // 1.创建字节输入流(使用FileInputStream对象中的)
  8. fis = new FileInputStream("Test.java");
  9. byte[] bytes = new byte[1024]; // 创建储存字节的容器大小
  10. int len = 0; // 记录每次读取的有效个数
  11. // while ((len = fis.read(bytes)) !=-1) { //每次读指针都会后移,直到末尾返回-1,是可以迭代的对象
  12. while ((len = fis.read(bytes)) > 0) {
  13. System.out.print(new String(bytes, 0, len)); // 将字节数组转换成字符串进行输出
  14. }
  15. } catch (IOException e) {
  16. e.printStackTrace();
  17. } finally {
  18. fis.close();
  19. }
  20. }
  21. }

⚠️ 注意: 上面程序最后使用了fis.close()来关闭该文件的输入流,与JDBC编程一样,程序里面打开的文件IO资源不属于内存的资源,垃圾回收机制无法回收该资源,所以应该显示的关闭打开的IO资源。Java 7改写了所有的IO资源类,它们都实现了AntoCloseable接口,因此都可以通过自动关闭资源的try语句来关闭这些Io流

FileOutputStream写出数据

构造方法
    以写入由指定的 File对象表示的文件(new File()出来的)
    public FileOutputStream(File file)

    以指定的名称写入文件(详细的路径名字)
    public FileOutputStream(String name)


创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?   
数据追加续写
    public FileOutputStream(File file, boolean append)
    public FileOutputStream(String name, boolean append)

FileOutputStream.png

数据追加续写.png

import java.util.Arrays;

public class FileOutputStreamTest {
  public static void main(String[] args) throws IOException {
    // FileInputStream fis = new FileInputStream(); // 创建字节输入流
    FileOutputStream fos = new FileOutputStream(new File("a.txt").getPath(),true); // 创建字节输出流
    byte[] byte1 = { 65, 66, 67 };
    byte[] byte2 = "你好".getBytes();
    System.out.println(Arrays.toString(byte1)); // [65, 66, 67]
    System.out.println(Arrays.toString(byte2)); // [-28, -67, -96, -27, -91, -67]
    fos.write(97);
    fos.write(-97);
    fos.write(byte1);
    fos.write(byte2);
    fos.write(byte2, 3, 3);
  }
}
import java.io.*;

public class FileOutputStreamTest {
  public static void main(String[] args) throws IOException {
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
      fis = new FileInputStream("./Test.java"); // 创建字节输入流
      fos = new FileOutputStream("./newTest.txt"); // 创建字节输出流

      byte[] b = new byte[1024];
      int len = 0;


      while ((len = fis.read(b)) > 0) {
        fos.write(b, 0, len); // 每读取一次,即写入文件输入流,读了多少,就写多少。
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      fis.close();
      fos.close();
    }
  }
}

⚠️注意: 使用java的io流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之外,可能还可以将输出流缓冲区中的数据flush到物理节点中里(因为在执行close()方法之前,自动执行输出流的flush()方法)。java很多输出流默认都提供了缓存功能,其实我们没有必要刻意去记忆哪些流有缓存功能,哪些流没有,只有正常关闭所有的输出流即可保证程序正常。

使用FileReader读取文件

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

构造方法
    创建一个新的 FileReader ,给定要读取的File对象
    FileReader(File file)

    创建一个新的 FileReader ,给定要读取的文件的名称
    FileReader(String fileName)

单个单个来读取字符数据

import java.io.*;

public class FileInputStreamTest {
  public static void main(String[] args) throws IOException {
     FileReader fis = new FileReader("Test.java");
    int len;
    while ((len = fis.read()) != -1) {
      System.out.print((char) len); // 将字节数组转换成字符串进行输出
    }
    fis.close();
  }
}

一次读取多个字符,将字符读入数组

import java.io.*;

public class FileInputStreamTest {
  public static void main(String[] args) throws IOException {
    FileReader fis = null;
    try {
      // 1.创建字节输入流(使用FileInputStream对象中的)
      fis = new FileReader("Test.java");
      char[] chars = new char[1024]; // 创建储存字节的容器大小
      int len = 0; // 记录每次读取的有效个数

      while ((len = fis.read(chars)) != -1) { // 每次读指针都会后移,直到末尾返回-1,是可以迭代的对象
        // while ((len = fis.read(chars)) > 0) {
        System.out.print(new String(chars, 0, len)); // 将字节数组转换成字符串进行输出
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      fis.close();
    }
  }
}

可以看出使用FileInputStream和FileReader进行文件的读写并没有什么区别,只是操作单元不同而且

使用FileReader写出文件

写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区

构造方法
    创建一个新的 FileWriter,给定要读取的File对象
    FileWriter(File file)

    创建一个新的 FileWriter,给定要读取的文件的名称
    FileWriter(String fileName)

FileWriter基础.png
FileWriter练习.png
续写和换行
FileWriter续写和换行.png

IO异常的处理

JDK7前处理

JDK7前处理.png
JDK7的处理 (扩展知识点了解内容).png

JDK9的改进(扩展知识点了解内容)

JDK9中try-with-resource 的改进,对于引入对象的方式,支持的更加简洁。被引入的对象,同样可以自动关闭,无需手动close
JDK9中try-with-resource 的改进.png

何为NIO,和传统IO有何区别?

我们使用InputStream从输入流中读取数据时,如果没有读取到有效的数据,程序将在此处阻塞该线程的执行。其实传统的输入里和输出流都是阻塞式的进行输入和输出。 不仅如此,传统的输入流、输出流都是通过字节的移动来处理的(即使我们不直接处理字节流,但底层实现还是依赖于字节处理),也就是说,面向流的输入和输出一次只能处理一个字节,因此面向流的输入和输出系统效率通常不高。

  • 从JDk1.4开始,java提供了一系列改进的输入和输出处理的新功能,这些功能被统称为新IO(NIO)。新增了许多用于处理输入和输出的类,这些类都被放在java.nio包及其子包下,并且对原io的很多类都以NIO为基础进行了改写。新增了满足NIO的功能。
  • NIO采用了内存映射对象的方式来处理输入和输出,NIO将文件或者文件的一块区域映射到内存中,这样就可以像访问内存一样来访问文件了。通过这种方式来进行输入/输出比传统的输入和输出要快的多。

JDk1.4使用NIO改写了传统IO后,传统IO的读写速度和NIO差不了太多

在开发中正确使用IO流

了解了Java IO的整体类结构和每个类的一下特性后,我们可以在开发的过程中根据需要灵活的使用不同的Io流进行开发。下面是我整理2点原则:

  • 如果是操作二进制文件那我们就使用字节流,如果操作的是文本文件那我们就使用字符流。
  • 尽可能的多使用处理流,这会使我们的代码更加灵活,复用性更好。