说起来,我接触 Java 是从大一下学期开始的,通过逐步学习接触到了 Java IO 这个很重要的模块,同时也是让我很头疼的一个模块,好像一直都没怎么学会。在我的脑子里,一直都是那种,这么多相关的类,怎么能记住啊。再加上,后来转战 Android 就更少有直接使用 Java IO 去操作文件的机会。一个偶然的机会,我学到了可以从另一种视角看 Java IO,突然觉得,Java IO,并不难。现在,我来记录并分享一点自己的经验。
如果是讲操作文件的 API 的话,那估计得写的老长,事实上,我对 Java IO 相关的 API 并不太熟,而且我也不打算讲。在这里我可能是从另一种我觉得是站在全局的角度去看 Java IO。
通常的,我们使用 Java IO 都是为了去对文件进行操作,或读或写。读:从磁盘、网络等渠道将文件内容读入到内存中;写:通过将内存中的数据写入到磁盘、网络等进行持久化保存。
以前啊,我就光想着 Java IO 就是这么读文件,写文件,也没什么的嘛。不就是对文件的操作嘛,没什么可讲的的,现在我不这么想了。现在怎么想呢?我换了一种思路。
我们使用 Java IO 写东西,写的是什么?是字符串,字符串是什么?是字符数组组成的一种的对象,什么是字符?字符就是可以通过字节码的形式表现出来一种东西。而字节码就是 OutputStream 以及 InputStream 直接能进行操作的东西,那什么是它们不是直接操作的?不就是字符串、字符数组嘛。可是我们在写程序的时候,谁还记得这个字符串的字节码是什么,那个字符的字节码是啥。我们都是直接操作字符串以及少部分情况操作字符数组的。那怎么办?
有的人说,通过各种转换就可以对应的字节码,然后进行操作。事实上就是这样的,Java 提供给我们的方法也是如此,通过各种转换得到可以操作 String 或是字符数组,然后进行操作。比如,OutputStreamWriter 就可以使用 OutputStreamWriter.write(String);
来直接操作字符串进行写入文件,还有通过 BufferedReader 就可以使用 String lineString = bufferedReader.readLine()
来直接进行读取一行文字。可是总一些我们可以从另一种角度看的东西。
仔细看一下,第二幅图,没什么啊,不就是数据在他们之间进行转换吗,有什么的?是这样的,但是还可以有另一种看法,字符串就是另种形式的字符数组,而字节数组就是另一种形式的字符数组。他们还是自己本身,只不过穿上了一件 “衣服” 而已。穿上了这身衣服,你以为他变了,从字符串 String
变成了字符数组 char[]
实际上,他还是 String ,只是不同的表现形态而已。没有什么不同,他还是他,只是你需要看穿这件 “衣服”。
那你说,既然数据本身都没有变化,只是穿了件衣服,那为什么 Java IO 的操作,显得那么的麻烦?因为,为了方便处理,也给处理过程穿上了衣服。OutputStream 只能处理字节数组,而包裹了一层 OutputStream 的 OutputStreamWriter 就能直接的操作 char[] 和 String。
好麻烦啊,就不能有一个直接能用来操作字符串的对象吗,每回写文件读写的时候,都得写好几个对象,烦死了。别烦啊,这样做是有这样做的理由的。就拿读操作(InputStream)来说吧,我曾天真的以为读操作只有一个来源就是,文件(FileInputStream)。后来才发现,竟然有 ByteArrayInputStream、ObjectInputStream、PipedInputStream、StringBufferInputStream 等等(只列出部分,全部的都 在这 呢。他们分别处理字节数组、被序列化的对象、线程间的输入流以及来自 StringBuffer 的输入。对应的还有写操作(OutputStream)整体来说,虽然比 InputStream 少,但是也有好几个呢,ByteArrayOutputStream、 FileOutputStream、FilterOutputStream、 ObjectOutputStream、PipedOutputStream。
如果这么多种输入或是输出方式,都要能处理 String、long、int 这种,或是再兼容一些其他方便我们程序员操作的方法,那类就太多了,想想一下,一个 FileInputStream 就可能变成了 StringFileInputStream、LongFileInputStream、IntegerFileInputStream ,想想就觉得可怕。所以,Java 采用了穿 “衣服” 的形式,让程序员通过对数据穿上不同的衣服,从而达到操作不同数据类型的效果。
FileInputStream 只能读出字节数组,InputStreamReader 可以读出字符数组,通过不同的对象进行转换,就能读出或是写出不同形式的数据。而且 FileInputStream 可以作为 InputStreamReader 的提供者,这样的话,就能应对足够复杂的场景,只要给当前的对象穿上另一件"衣服"就能摇身一变成为你想要的结果。
感觉很神奇啊,只要是 InputStream 相关的类,都可以作为生成下一个 xxInputStream 对象输入,只要是一个 OutputStream 相关的类都可以作为生成下一个 xxOutputStream 对象的输入。其实啊,知道细节之后就不会觉得神奇了,只会觉得设计的很巧妙。
FilterOutputStream 和 FilterInputStream 这两个是关键,他们俩分别继承自 OutputStream 和 InputStream,同时又有非常多的子类,就是通过这些子类来实现各种转换,什么线程的输入做到输出字符数组、被序列化的对象输出字符串等等。
FilterOutputStream 子类:
- BufferedOutputStream
- CheckedOutputStream
- CipherOutputStream
- DataOutputStream
- DeflaterOutputStream
- DigestOutputStream
- InflaterOutputStream
- PrintStream
FilterInputStream 子类:
- BufferedInputStream
- CheckedInputStream
- CipherInputStream
- DataInputStream
- DeflaterInputStream
- DigestInputStream
- InflaterInputStream
- LineNumberInputStream
- ProgressMonitorInputStream
- PushbackInputStream
这些子类固然是很重要的,便于操作不同的数据类型,同样重要的还有 Reader、Writer。这两个类并不继承 InputStream 与 OutputStream 但通过他们的子类,也同样能实现更简单的文件读写。比如 OutputStreamWriter、InputStreamReader。都可以直接对 String 进行操作。再搭配上 InputStream 和 OutputStream 那一大家子,真的是可以对文件可以为所欲为了。
对了,还一个 Buffer,它直译过来是缓冲的意思,干什么用的呢?是这样的,磁盘的读取或写入的速度并没有像直接操作内存那样快,所以需要一个缓冲的东西用来缓解双方操作上的时间差,比如在写的时候,可以先写到缓冲区,然后再把缓冲区的数据一点点写到磁盘上。或者说,从磁盘读数据的时候,先读一些,读到缓冲区,等读的东西,够多了,然后再一起写入到内存中去。