0%

Java基本IO

前言

本文是介绍 Java 基本 I/O 。

目录

一、IO简介

I/O(Input/Outpu) 即输入/输出 。从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。从应用程序的视角来看的话,应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。

为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space)内核空间(Kernel space ) 。用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间。

当应用程序发起 I/O 调用后,会经历两个步骤:

  1. 内核等待 I/O 设备准备好数据;
  2. 内核将数据从内核空间拷贝到用户空间。

在开发过程中接触最多的就是磁盘 IO(读写文件)和 网络 IO(网络请求和响应)。UNIX 系统下, IO 模型一共有5种: 同步阻塞 I/O、同步非阻塞 I/O、I/O多路复用、信号驱动 I/O和异步I/O。

二、Java I/O

数据流是一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。

流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种:

1) 字节流:数据流中最小的数据单元是字节
2) 字符流:数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节。

Java.io包中最重要的就是5个类和一个接口。

5个类指的是File、OutputStream、InputStream、Writer、Reader; 一个接口指的是Serializable。掌握了这些就掌握了Java I/O的精髓了。

Java I/O主要包括如下3层次:

  1. 流式部分——最主要的部分。如:OutputStream、InputStream、Writer、Reader等

  2. 非流式部分——如:File类、RandomAccessFile类和FileDescriptor等类

  3. 其他——文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系

    统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。

三、装饰器模式

IO类的实现使用了设计模式中的装饰器模式。可以动态地为一个对象增加新的功能;用于代替继承的技术,无须通过继承增加子类就能扩展对象的新功能,使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀。有以下几个角色:

  • Component:抽象构建接口,真实对象和装饰对象有相同的接口

  • ConcreteComponent:具体的构建对象,实现组件对象接口,通常就是被装饰的原始对象。就对这个对象添加功能。

  • Decorator:所有装饰器的抽象父类,需要定义一个与组件接口一致的接口,内部持有一个被装饰的 Component对象。 装饰对象接受所有客户端的请求,并把这些请求转发给真实的对象 。这样就能在真实对象调用前后增加新的功能。

  • ConreteDecoratorA/ConreteDecoratorB:实际的装饰器对象,实现具体添加功能。

优点:

  • 扩展对象功能,比继承灵活,不会导致类个数急剧增加。
  • 可以对一个对象进行多次装饰,创造出不同行为的组合,得到功能更加强大的对象。
  • 具体构建类和具体装饰类可以独立变化,用户可以根据需要自己增加新的具体构件子类和具体装饰子类。

缺点 :

  • 产生很多小对象。大量小对象占据内存,一定程度上影响性能,调试排查比较麻烦。

Java IO中的装饰器模式:

四、字节流

在Android 平台,从应用的角度出发,我们最需要关注和研究的就是字节流(Stream)、字符流(Reader/Writer) 和 File/ RandomAccessFile。关于字符和字节,例如文本文件, XML这些都是用字符流来读取和写入。而如RAR,EXE文件,图片等非文本,则用字节流来读取和写入。

因为从流的整个发展历史,出现的各种类之间的关系看,都是沿用了修饰模式,都是一个类的功能可以用来修饰其他类,然后组合成为一个比较复杂的流。比如说:

1
2
3
4
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(
new File(file)));

为了向文件中写入数据,首先需要创建一个FileOutputStream,然 后为了提升访问的效率,所以将它发送给具备缓存功能的BufferedOutputStream,而为了实现与机器类型无关的 java基本类型数据的输出,所以,我们将缓存的流传递给了DataOutputStream。从上面的关系,我们可以看到, 其根本目的都是为outputSteam添加额外的功能。而这种额外功能的添加就是采用了装饰模式来构建的代码。

下面的图是一个关于字节流的图谱,这张图谱比较全面的概况了我们字节流中间的各个类以及他们之间的关系。

根据它们的功能可得出:OutputStream -> FileOutputStream/FilterOutputStream ->DataOutputStream->BufferedOutputStream

OutputStream

抽象类,是所有表示字节输出流的类的超类。从内存中把字节流写出到数据目的地。

FileOutputStream

是OutputStream的具体构建对象,继承自OutputStream。文件输出流用于将数据写入 File的输出流,用于写入原始字节流,如图像数据。具体构建对象还有ByteArrayOutputStream/PipeOutputStream/ObjectOutputStream。

FilterOutputStream

是OutputStream的装饰类,是所有具体装饰器的父类,继承自OutputStream,并持有OutputStream的引用。将所有请求传递给底层输出流,覆盖了OutputStream所有方法。 其子类可以进一步覆盖这些方法,并提供额外的方法和字段。

DataOutputStream

是OutputStream的一个具体装饰器类,继承自FilterOutputStream,数据输出流允许将Java基础数据类型写入输出流,可使用DataInputStream数据输入流将数据读回。

BufferedOutputStream

是OutputStream的一个具体装饰器类,继承自FilterOutputStream,实现了一个缓冲输出流。可以一次性写入一个字节数组到输出流,减少了对磁盘的访问次数以提升性能。

对应的输入流相关类有:InputStream->FileInputStream/FilterInputStream->DataInputStream->BufferedInputStream。

五、字符流

字节流面向的是我们未知或者即使知道了他们的编码格式也意义不大的文件(png,exe, zip)的时候是采用字节,而面对一些我们知道文件构造我们就能够搞懂它的意义的文件(json,xml)等文件的时候我们还是需要以字符的形式来读取,所以就出现了字符流。英文字符占一个字节,而中文是一个字符,至少占2个字节,就需要指定字符集编码。

Writer- >FilterWriter->OutputStreamWriter->FileWriter->BufferedWriter->其他

Writer

用于写入字符流的抽象类。

FilterWriter

是Writer的装饰类,本身提供了将所有请求传递给包含的流的默认方法。其子类应该覆盖其中一些方法,并且还可以提供额外的方法和字段。

OutputStreamWriter

是从字符流到字节流的桥梁:写入其中的字符使用指定的字符集编码成字节写入输出流。

FileWriter

用于写入字符流。用于编写字符文件的类。

BufferedWriter

将文本写入字符输出流,缓冲字符以提供多个字符的高效写入。可以指定缓冲区大小,也可以接受默认大小。

对应的读取字符流相关类有:Reader->FilterReader->InputStreamReader->FileReader->BufferedReader->其他。

1.字节:byte:用来计量存储容量的一种计量单位;位:bit

2.一个字节等于8位 1byte = 8bit

Java中,char占用的是2个字节 16位,所以一个char类型的可以存储一个汉字。

六、字节流与字符流的关系

OutputStream/InputStream :是字节流(stream of bytes),它一个字节一个字节的向外边送数据。

Writer/Reader:是字符流(streams of characters),它一个字符一个字符的向外边送数据。

不同之处:

  • 字符流和字节流最大的区别是它包含了一个readline()接口,这个接口标明了,一行数据的意义,这也是可以理解的,因为只有字符才具备行的概念,相反字节流中的行也就是一个字节符号。

  • 字节流在操作的时本身是不会用到缓冲区(内存)的,是与文件本身直接操作的。即使不关闭资源(close方法)文件也能输出;而字符流在操作时是使用到缓冲区的,如果不使用close方法关闭资源,则不会输出如何内容,并且可以使用flush方法强制进行刷新缓存区,这时才能在不关闭资源的情况下输出内容。

使用场景:

  • 在所有的硬盘上保存文件或进行传输的时候都是以字节的方法进行的,包括图片也是按字节完成,而字符是只有在内存中才会形成的,所以使用字节的操作是最多的。

  • 如果要Java程序实现一个拷贝功能,应该选用字节流进行操作(可能拷贝的是图片),并且采用边读边写的方式 (节省内存)。

字节流和字符流两者之间转换:

  • OutputStreamWriter用于将写入的字符编码成字节后写入一个字节输出流。其内部实现是通过StreamEncoder对象,使用指定的或者默认的编码集将字符转码为字节,调用其自身实现的写入方法将转码后的字节写入到底层字节输出流中。

  • InputSreamReader用于将一个字节输入流中的字节解码成字符。其内部实现是通过StreamDecoder对象,使用指定的或者默认的编码集将字节解码为字符,调用其自身实现的读取方法将解码后的字符读取到程序中。

七、RandomAccessFile

支持读取和写入随机访问文件。随机访问文件的行为类似于存储在文件系统中的大量字节。有一种游标,或隐含数组的索引,称为文件指针;输入操作从文件指针开始读取字节,并将文件指针前进到读取的字节之后。运用场景多线程中分段下载。

构造方法:

1
2
RandomAccessFile raf = newRandomAccessFile(File file, String mode); 
其中参数 mode 的值可选 "r":可读,"w" :可写,"rw":可读性;

成员方法:

seek(int index):可以将指针移动到某个位置开始读写;

setLength(long len):给写入文件预留空间。

特点和优势:

  • 既可以读也可以写。RandomAccessFile不属于InputStream和OutputStream类系的它是一个完全独立的类,所有方 法(绝大多数都只属于它自己)都是自己从头开始规定的,这里面包含读写两种操作
  • 可以指定位置读写。能在文件里面前后移动,在文件里移动用的seek( ),所以它的行为与其它的I/O类 有些根本性的不同。总而言之,它是一个直接继承Object的,独立的类。只有RandomAccessFile才有seek搜寻方法,而这个方法也只适用于文件.

IO模型:

BIO 属于同步阻塞 IO 模型

同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

NIO (Non-blocking/New I/O)

Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , SelectorBuffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。

FileChannel用于读取、写入、映射和操作文件的通道。 FileChannel配合着ByteBuffer,将读写的数据缓存到内存中,然后以批量/缓存的方式read/write,省去了非批量操作时的重复中间操作,操纵大文件时可 以显著提高效率。

AIO (Asynchronous I/O)

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

总结