这一篇搞懂Java中的IO流

441 阅读18分钟

​本文共 9185字,预计阅读时间:15分钟

本篇是Java基础中的IO和NIO的讲解,IO和NIO在Java中可以说是必不可少,涉及到硬盘文件读写、网络文件读写等,只要是和文件打交道基本少不了IO和NIO的陪伴,那么接下来我们一起来学习IO和NIO吧,博主会持续更新更多文章,觉得不错的可以点个关注

IO流的学习

我们都知道在IO中,IO的超类有字节流InputStream和OutputStream、字符流Reader和Writer,我们先来从整体认识一下IO流

字节流的输入和输出对照图:

字符流的输入和输出对照图:按操作对象分类结构图:

IO流指的是Input/Output,即输入和输出,以内存为中心

  • Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等

  • Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等

**为什么要把数据读取到内存中呢?**因为我们的Java代码是运行在内存中的,因此数据也必须读取到内存中 ,最终的表现形式是字符串、byte数组等

字节流和字符流的区别又是什么呢,为什么要出现这两种流?

首先明确字节和Byte和字符Character的大小:

  • 1 byte = 8 bit

  • 1 char = 2 byte = 16 bit (Java默认UTF-16编码)

虽然1 bit才是数据真正的最小单位,但1 bit 的信息量太少了。

要表示一个有用的信息,需要好几个bit一起表示。所以除了硬件层面存在1个比特位的寄存器,大多数情况下,字节是数据最小的基本单位。我们熟知的基本型的大小都是8 bit(也就是1字节)的整数倍:short是2byte,int是4byte,long是8byte等

原本对于西方世界来说,可能根本用不到字符,一个字节问题全部解决了,因为一个字节8bit,最多有256个字符编码,英语26个字母,再加几个常用符号,标点,256个码位足够了,这就熟悉的ASCII码。

但是无法解决更多的国家的语言,伴随这问题出现了各种类型的编码ISO-8859-1、GBK、UTF-8、UTF-16等多种编码类型(这里不一一介绍了),总而言之,一切都是字节流,可以说其实没有字符流这个东西,字符只是根据编码集对字节流翻译之后的产物。也就是字节流的InputStream和OutputStream是一切的基础,实际总线中流动的只有字节流,需要对字节流做特殊解码才能得到字符流

  • 字节流读取的时候,读到一个字节就返回一个字节;字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在 UTF-8 码表中是 3 个字节)时

  • 字节流没有缓冲区,是直接输出的,而字符流是输出到缓冲区的。因此在输出时,字节流不调用colse()方法时,信息已经输出了,而字符流只有在调用close()方法关闭缓冲区时,信息才输出。要想字符流在未关闭时输出信息,则需要手动调用flush()方法

  • 字节流是最基本的,所有的InputStrem和OutputStream的子类都是字节流,主要用在处理二进制数据,它是按字节来处理的。而字符流是Writer和Reader作为超类来操作字符、字符数组或字符串,可以将字节转换成 2 个字节的 Unicode 字符为单位的字符

  • 字节流和字符流这两个之间通过InputStreamReader,OutputStreamWriter(转换流)来关联,实际上是通过 byte[]和 String来关联的

接下来我们学习使用字节流和字符流

在此之前,我需要先介绍一下File对象和Path对象的使用。在计算机系统中,文件是非常重要的存储方式,Java的标准库java.io提供了File对象来操作文件和目录。我们通过传入文件的路径(可以传入相对路径或者绝对路径)来构造一个File对象,Windows平台使用\作为路径分隔符,在Java字符串中需要用\表示一个\,Linux平台使用/作为路径分隔符

 File f = new File("d:\\test.txt");

File对象既可以表示文件,也可以表示目录。

特别要注意的是,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。我们可以通过File对象创建和删除文件、遍历文件和目录等。Java标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但操作更加简单:

Path p1 = Paths.get(".", "a", "b"); // 构造一个Path对象Path 
p2 = p1.toAbsolutePath(); // 转换为绝对路径Path 
p3 = p2.normalize(); // 转换为规范路径
File f = p3.toFile(); // 转换为File对象
for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path   
  System.out.println("  " + p);}

流可以分为字节流和字符流两种,又可以按照功能分为节点流(该类型可以从或者向一个特定的地点或者节点读写数据)和处理流(该类型是对一个已存在的流的链接和封装)类型。

1、输入字节流InputStream

InputStream是所有的输入字节流的父类,它是一个抽象类

  • FileInputSream:文件输入流,它通常用于对文件进行读取操作

  • BufferedInputStream:缓冲流,对处理流进行装饰,增强,内部会有一个缓存区,用来存放字节,每次都是将缓存区存满然后发送,而不是一个字节或两个字节这样发送。效率更高。(较于FileInputStream,BufferedInputStream更适用于大文件)

  • ByteArrayInputStream:字节数组输入流,该类的功能就是从字节数组(byte[])中进行以字节为单位的读取,也就是将资源文件都以字节的形式存入到该类中的字节数组中去,我们拿也是从这个字节数组中拿

  • PipedInputStream:管道字节输入流,它和PipedOutputStream一起使用,能实现多线程间的管道通信

  • DataInputStream:数据输入流,它是用来装饰其它输入流,它“允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型”

  • ObjectInputStream:对象输入流,用来提供对“基本数据或对象”的持久存储。通俗点讲,也就是能直接传输对象(反序列化中使用)

  • FilterInputStream :装饰者模式中处于装饰者,具体的装饰者都要继承它,所以在该类的子类下都是用来装饰别的流的,也就是处理类

2、输出字节流OutputStream

OutputStream 是所有的输出字节流的父类,它是一个抽象类(和上面的InputStream输入字节流流对应,一般成对使用)

ByteArrayOutputStream、FileOutputStream 是两种基本的介质流,它们分别向Byte 数组、和本地文件中写入数据。PipedOutputStream 是向与其它线程共用的管道中写入数据。BufferedOutputStream是缓冲流,通过缓冲区修饰,更适用于处理大文件

ObjectOutputStream 和所有FilterOutputStream 的子类都是装饰流(序列化中使用)

字节流和字符流,你更喜欢使用哪一个?

个人来说,更喜欢使用字符流,因为他们更新一些。许多在字符流中存在的特性,字节流中不存在。比如使用BufferedReader而不是BufferedInputStreams或DataInputStream,使用newLine()方法来读取下一行,但是在字节流中我们需要做额外的操作

Java的IO标准库提供的InputStream根据来源可以包括但不限于:

IO流工具类:IOUtils.readLines()、FileUtils.readFileToString()、Files工具中的readAllLines();

  • FileInputStream:从文件读取数据,是最终数据源

  • ServletInputStream:从HTTP请求读取数据,是最终数据源

  • Socket.getInputStream():从TCP连接读取数据,是最终数据源

Filter模式进行功能的补充:

JDK将InputStream分为两大类:一类是直接提供数据的基础InputStream,例如:

FileInputStream、ByteArrayInputStream、ServletInputStream;一类是提供额外附加功能的InputStream,例如:BufferedInputStream、DigestInputStream、CipherInputStream;

当我们需要给一个“基础”InputStream附加各种功能时,我们先确定这个能提供数据源的InputStream,紧接着我们可以用附加功能类来包装提供数据源的基础类;在叠加多个FilterInputStream,我们只需要持有最外层的InputStream,并且,当最外层的InputStream关闭时(在try(resource)块的结束处自动关闭),内层的InputStream的close()方法也会被自动调用,并最终调用到最核心的“基础”InputStream,因此不存在资源泄露

不同的环境下配置文件的路径不同,如何读取(有没有路径无关的读取文件的方式呢)

从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把xxx.properties文件放到classpath中,就不用关心它的实际存放路径。在classpath中的资源文件,路径总是以/开头,我们先获取当前的Class对象,然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件,如果资源文件不存在,它将返回null。因此,我们需要检查返回的InputStream是否为null,如果为null,表示资源文件在classpath中没有找到

try (
  InputStream input = getClass().getResourceAsStream("/xxx.properties")) {    
if (input != null) {   
     // TODO: 
   }}

SequenceInputStream的功能是什么?

在拷贝多个文件到一个目标文件的时候是非常有用的。SequenceInputStream 可以将两个或多个其他 InputStream 合并为一个。首先,SequenceInputStream 将读取第一个 InputStream 中的所有字节,然后读取第二个 InputStream 中的所有字节。这就是它被称为 SequenceInputStream 的原因,因为 InputStream 实例是按顺序读取的

IO流中的序列化

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。为什么要把Java对象序列化呢?因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象

安全性:因为Java的序列化机制可以导致一个实例能直接从byte[]数组创建,而不经过构造方法,因此,它存在一定的安全隐患。一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息

字节流代码示例:

public static List<String> readFile1(File file) {   
 List<String> list = new ArrayList<>();  //使用StringBuilder将获取的单个字符拼接成字符串  
  StringBuilder stringBuilder = new StringBuilder();  
  try {      InputStream inputStream = new FileInputStream(file);   
   int byteToint;     
 while ((byteToint = inputStream.read()) != -1) {      
  char c = (char) byteToint;    
    if (c != 'r' && c != 'n') {   
   stringBuilder.append(c);   
  } else {   
    if (!stringBuilder.toString().equals("")) {
  list.add(stringBuilder.toString()); 
          stringBuilder.delete(0, stringBuilder.length());
 }   }    }     } catch (Exception e) {  
            throw new RuntimeException(e);  
        }        
  return list;

3、输入字符流Reader

Reader 是所有的输入字符流的父类,它是一个抽象类。Reader 中各个类的用途和使用方法基本和InputStream 中的类使用一致,因为Reader的实现类很多方法就是使用InputStream的实现类的底层方法实现的

  • InputStreamReader 是一个连接字节流和字符流的桥梁,它将字节流转变为字符流

  • FileReader 可以说是一个操作文件的字符流,在其源代码中明显使用了将FileInputStream 转变为Reader 的方法。我们可以从这个类中得到一定的技巧

  • BufferedReader 很明显就是一个装饰器,它和其子类负责装饰其它Reader 对象。(相较于FileReader ,BufferedReader包装之后对于大文件的操作效率更高)

  • CharReader、StringReader 是两种基本的介质流,它们分别将Char 数组、String中读取数据

  • PipedReader 是从与其它线程共用的管道中读取数据

  • FilterReader 是所有自定义具体装饰流的父类,其子类PushbackReader 对Reader 对象进行装饰,会增加一个行号

4、输出字符流Writer

Writer 是所有的输出字符流的父类,它是一个抽象类。Writer中各个类的用途和使用方法基本和OutputStream 中的类使用一致,因为Writer的实现类很多方法就是使用OutputStream的实现类的底层方法实现的

  • FileWriter是操作文件的字符流

  • BufferedWriter 是一个装饰器为Writer 提供缓冲功能;(相较于FileWriter,BufferedWriter 包装之后对于大文件的操作效率更高)

  • CharArrayWriter、StringWriter 是两种基本的介质流,它们分别向Char 数组、String 中写入数据

  • PipedWriter 是向与其它线程共用的管道中写入数据

  • PrintWriter 和PrintStream 极其类似,功能和使用也非常相似

  • OutputStreamWriter 是OutputStream 到Writer 转换的桥梁,它的子类FileWriter 其实就是一个实现此功能的具体类

字符流和字节流的转换:

InputStreamReader 是字节到字符的转换,OutputStreamWriter 是字符到字节的转换,转换流的作用,文本文件在硬盘中以字节流的形式存储时,通过InputStreamReader读取后转化为字符流给程序处理,程序处理的字符流通过OutputStreamWriter转换为字节流保存。(可以经过指定编码进行转换)

System类对IO的支持:

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

  • System.in(标准输入),通常代表键盘输入

  • System.out(标准输出):通常写往显示器

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

管道流的用处?

管道流的主要作用就是可以进行两个线程间的通信。一个线程作为管道输出流,另一个线程作为管道输入流, 在启动线程前,只需要调用connect方法将这两个线程的管道流连接到一起就可以。这要就很方便的实现了两个线程间的通信

RandomAccessFile?

它在java.io包中是一个特殊的类,既不是输入流也不是输出流,它两者都可以做到。它是Object的直接子类。通常来说,一个流只有一个功能,要么读,要么写。但是RandomAccessFile既可以读文件,也可以写文件。DataInputStream 和 DataOutStream有的方法,在RandomAccessFile中都存在

NIO流的学习

在讲解之前,我们需要先学习网络通信的三种模型:IO、NIO和AIO:
IO(同步阻塞**)**:传统的网络通信模型,就是BIO,同步阻塞IO,客户端的每次会话会去连接服务器的ServerSocket,服务端收到之后创建一个Socket和一个线程去和客户端通信,客户端和服务端进行的是阻塞通信,服务端不返回数据的期间客户端是无法做其它事情的,当客户端请求过大造成服务负载过高,最后崩溃。比较适用于连接少、对服务消耗资源大的地方;

老李烧水,机器在烧水,他人坐着等着,啥也不能干,等待水开了,接水继续做下一件事

NIO(同步非阻塞):NIO是一种同步非阻塞IO, 基于Reactor模型来实现的。其实相当于就是一个线程处理大量的客户端的请求,通过一个线程轮询大量的channel,每次就获取一批有事件的channel,然后对每个请求启动一个线程处理即可。这里的核心就是非阻塞,就那个selector一个线程就可以不停轮询channel,所有客户端请求都不会阻塞,直接就会进来,大不了就是等待一下排着队而已。这里面优化BIO的核心就是,一个客户端并不是时时刻刻都有数据进行交互,没有必要死耗着一个线程不放,所以客户端选择了让线程歇一歇,只有客户端有相应的操作的时候才发起通知,创建一个线程来处理请求

老李烧水,觉得刚刚有点笨,于是人走开了,每过一段时间来看看水开了没,没开继续干别的,开了就接水

AIO(异步非阻塞**)**:AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了

老李烧水,觉得还是麻烦,在网上买了个带提醒的烧水的机子,人走开干别的事了,水烧开之后机子吹哨,老李听到之后知道水开了,过来接水

在JDK1.7中,在Java.nio.channels包下增加了下面四个异步通道:

进入主题:

Java中的NIO(翻译成 no-blocking io 或者 new io ,我觉得都可以哈)可以说是由Channels、Buffers、Selectors几个核心部分组成,其它组件,如Pipe和FileLock,只不过是与三个核心组件共同使用的工具类

传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达),因此,单个线程可以监听多个数据通道
IO面向流,NIO面向缓冲。面向流意味着每次从流中读取一个或者多个字节直到读取完所有的字节,过程是阻塞的,没有被缓存的地方;它不能前后移动流中的数据,如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。面向缓冲意味着数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。正是因为上面的面向流造成了IO是阻塞的,而NIO是非阻塞的。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)

Channel

首先说一下Channel,国内大多翻译成“通道”。Channel和IO中的Stream(流)是差不多一个等级的,只不过Stream是单向的,譬如:InputStream, OutputStream,而Channel是双向的,数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中

下面是JAVA NIO中的一些主要Channel的实现,这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO

  • FileChannel

  • DatagramChannel

  • SocketChannel

  • ServerSocketChannel

Buffer

NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。Java NIO 还有个 MappedByteBuffer,用于表示内存映射文件

Selector

Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。这是在一个单线程中使用一个Selector处理3个Channel的图示:

要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等

结束语

可爱的你,应该不会吝啬动一动小手的点赞和转发吧,不会吧不会吧?