所谓 I/O 指的是输入输出,输入输出的一端是内存(RAM),另一端可以是文件系统中的文件、网络中的数据或者标准输入输出设备,如键盘、显示器等。所以 Java 程序在这个数据传输的过程中,相当于一个操作工的角色。
计算机中的数据存储在不同的存储介质中,有不同的表现形式。在磁盘中是存储在磁介质的涂层上,光盘中则是利用了光学技术,RAM 随机存储器则是以高低电平的方式存储数据。计算机科学中把这些在底层存储介质上不同表现形式的数据统一抽象成二进制表示的 0 和 1 。所以计算机中最小的存储单位就是位,8 个位组成一个字节,作为数据的基本存储单位。
Java.io 类库中的类都是按照输入输出对应设计的,理解这一点很重要。
文本文件和二进制文件?
文件可以分为文本文件和二进制文件。他们的区别可以这样去理解,文本文件是由字符序列构成的,而二进制文件则是由位序列构成的。比如 .java 文件就是文本文件, .class 文件就是二进制文件。
如果研究的更深层次的话,本质上计算机中的任何数据都是以二进制的形式存储的。区别是当用记事本或者 vim 这样的编辑器打开文本文件时,它会将二进制数据通过其指定的编码方式,转换成字符,展示给人看。对于原生的二进制文件,编辑器则没有此功能。
文本文件读取到内存中时,需要用其指定的编码方式,将字符转换成二进制的形式才能够存储到内存中去。而二进制文件因为本来就是原生二进制存储,所以并不需要转换,可以直接以字节的方式读到内存中去。
文本 I/O 和二进制 I/O
二进制 I/O 是一次读取一个字节的方式读取的数据,如果用它来读取二进制文件的话,它就会直接照搬二进制文件中的数据到内存中,不需要进行转换,效率高。举例:如果要把内存中的数字 199 写入到二进制文件中去,由于 199 在内存中本就是以 0xC7 方式存在的,所以直接照搬复制过去就可以了。
文本 I/O 在读写字符时会进行一个编码和解码的转换,比如要将字符串 "199"
写入到文件中去时,会将"1","9","9" 三个字符转换成 0x31 , 0x39 , 0x39 后,再写入到文件中去。
二进制 I/O 可以读取任何类型的数据,但是文本 I/O 则只能够读取由字符序列组成的文本文件。这里的文本 I/O 和 二进制 I/O 就是通常所说的字符流和二进制流。
重点:所有的 I/O 类的构造方法都会通过 throws 声明不处理 FileNotFoundException 异常,所有的方法几乎都会声明不处理 IOException 异常。记住这两点非常重要!
字节流
以字节的方式处理数据的流,被分类为字节流。其中所谓的流可以理解为传送数据的管道,而且还有能力去操纵这些数据,就是通过流所提供的种种方法。
字节输入输出流的抽象基类
字节输入输出有两个抽象基类,InputStream 和 OutputStream 。这是两个抽象类,没有具体的使用价值,因为它不能创建对象,只能依赖于继承了它的子类去完成具体功能。为什么要设计出这样的继承关系呢?这不就是面向对象的精髓?节省代码啊!
学习启示:学会将之前学习的具体的语法知识点和它的应用联系在一起,建立的联系越多、越频繁,才能够真正达到融汇贯通的层次,多动脑思考。比如这里的抽象类 InputStream 类,它就涉及到关于抽象类无法直接创建对象的具体语法,学会将高一层次的抽象和第一层次的抽象建立联系,才能知其然和所以然。
InputStream 类中有几个重要的方法,下面一一介绍下。
-
int read(byte[] b)
实现的功能就是一次读取一个字节,直到把这个字节数组 b 装满为止。 -
int read(byte[] b,int off,int len)
从流中读取字节并将它们保存在下标 off 开始到 len 的位置结束。 -
int read()
一次读取一个字节,但是因为返回值是 int 类型的,占据 4 个字节,所以会在这一个字节的基础上,补充高位的三个字节作为一个 int 类型的值返回,但这并不影响结果啊。
以上三个方法返回值为实际读取的字节数,如果读到流的末尾,则返回 -1 。
OutputStream 类和 InputStream 类是一一对应的。理解了一个,另外的就理解了。
-
void write(byte[] b)
一次性把这个字节数组的内容输出去。 -
void write(byte[] b,int off,int len)
从 off 开始的 len 个字节输出。 -
void write(int b)
参数是一个 4 个字节的整数,但是输出是一个字节,所以会去掉前面三个高位的内容。 -
void flush()
强行清空管道中可能遗留的数据。
文件流 FileInputStream & FileOutputStream
FileInputStream 和 FileOutputStream 类是专门设计来以字节的方式操作文件的流,两者常用的方法就是继承自它们父类的那几个方法。
FileInputStream 类构造函数有FileInputStream(String name)
和FileInputStream(File file)
。
FileOutputStream 类的方法必然是同样的。有四个构造函数:
FileOutputStream(String name)
FileOutputStream(File file)
FileOutputStream(String name,boolean append)
FileOutputStream(File file,boolean append)
前两个方法都是如果指定的文件存在,则会覆盖该文件的内容,如果不存在就先创建一个新的文件。后两个构造方法的第二个参数如果设置为 true ,如果文件存在会在原来内容的基础上进行追加,如果不存在当然就是创建一个文件啦。
过滤器数据流 FilterInputStream & FilterOutputStream
是为了达到某种目的过滤字节的流,比如基本的文件流只能读取字节,但是如果想要直接读取指定数据类型的数据该怎么办呢?这就需要利用过滤器流来完成这个功能了。所以设计了 FilterInputStream 和 FilterOutputStream 来作为过滤流的基类,实际使用中,一般都是用它的子类。这属于套接流的概念了,在原有字节流的基础上套上新的流,大管子套着小管子。
数据流 DataOutputStream & DataInputStream
DataOutputStream 和 DataInputStream 类被设计来直接操作 java 语言中的 8 种基本数据类型,和以 utf-8 的形式操作字符串,它们继承自 FilterInputStream / FilterOutputStream 类。
以 DataInputStream 类为例,它有个构造方法DataInputStream(InputStream in)
读取数据有一下这些方法:
readBoolean()
dis.readByte()
dis.readChar()
dis.readDouble()
readFloat()
readInt()
readLong()
readShort()
readUTF()
至于 DataOutputStream 则完全是对应的。
缓冲流 BufferedInputStream & BufferedOutputStream
BufferedInputStream 和 BufferedOutputStream 类的内部有一个数组protected volatile byte buf[]
,作为缓冲数据区。所以可以一次读取多个字节的数据到缓冲区,减少对磁盘的读写次数,以此来提高读写效率。当对超过 100M 的数据时,利用缓冲流读写速度能够得到质的提升。
BufferedInputStream 类有两个构造函数:
BufferedInputStream(InputStream in)
-
BufferedInputStream(InputStream in,int size)
此构造函数的第二个参数是指定缓冲字节数组的大小。
BufferedOutputStream 输出流是输入流是对应的。
对象流
ObjectInputStream 和 ObjectOutputStream 类不仅拥有 DataInputStream 和 DataOutputStream 类的直接读写基本数据类型和字符串的功能,还可以实现对象的输入、输出,所以对象流完全可以替代数据流。读写对象也叫作序列化和反序列化!
ObjectInputStream 类有两个构造函数:
ObjectInputStream()
ObjectInputStream(InputStream in)
读写对象是通过Object readObject()
和void writeObject(Object obj)
方法实现的,由于创建对象需要事先加载类的字节码文件,所以readObject()
方法显示声明不处理 ClassNotFoundException 异常。
// TODO:
管道流
PipedInputStream 和 PipedOutputStream 是专门设计来在线程之间传输数据的,可以说是生产者消费者的一个封装好的解决方案。
在生产者线程和消费者线程之间构建一个唯一传送数据的管道,就能够极大的简化线程之间的协作问题,不需要再去用 sychronized 同步锁、notify() 和 wait() 方法的组合弄一个复杂的逻辑去完成协作功能了。那如何构建呢?生产者的输出管道套着消费者的输入管道,两个管道构成一个管道,就能够解决问题了。
PipedInputStream 类有一个构造方法PipedInputStream(PipedOutputStream src)
,PipedOutputStream 类有一个构造方法PipedOutputStream(PipedInputStream snk)
。两个输入输出流的组合就可以了。组合方式如下:
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream(pis);
//或者
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
随机流
这里涉及到一个随机流和顺序流的概念,所谓顺序流就是类似磁带那种只能从前往后顺序读写的流,只能够顺序访问或读写文件。除了这里即将了解到的 RandomAccessFile 类,其他都属于顺序流。所谓随机流就是可以在文件的任意位置上进行读写,所以随机流拥有更新文件的能力。
RandomAccessFile 方法直接继承自 Object 类,同时拥有对于 8 种基本数据类型的读和写操作。而且不分输入和输出流,直接通过构造函数的参数指定。
RandomAccessFile 类的两个构造函数如下:
RandomAccessFile(File file,String mode)
RandomAccessFile(String name,String mode)
随机流是通过参数 "r"
和"rw"
来指定流的读写方式的,第一个表示只读,第二个表示可以读和写。
RandomAccessFile 类之所以能够直接对文件中的任意位置进行读写操作,就是因为有void seek(long pos)
方法作支撑。 RandomAccessFile 类管理的随机流中有一个文件指针作为定位器,来定位文件中的字节位置。参数 pos 为 0 表示此时指针指向第 0 个字节的起始位置,每次读取指定数量字节后,会自动偏移指定数量字节到新的位置。
标准流
就是 System 类的那三个
字符流
InputStreamReader 和 OutputStreamWriter 是字符流的基类。
BufferedReader 有一个 readLine() 方法。
学习启示:上课不认真听讲,课后又没有及时复盘整理,导致笔记无法动笔。这说明上课要认真听讲,笔记要及时整理才行。如果真的上课老师讲的不清楚或者自己没听懂,课后自己找资料去学习后,再来整理笔记也是可以的。