java零基础入门-高级特性篇(十二) IO 流 2
本章先来看两大“流”派中的字节流。字节流相对字符流总体结构简单一点,只用记住它的4个最基本的操作类就可以了。下面一张图来看看这四个基本的操作类。
上面这张图通过两个方面进行划分,一个是输入和输出,一个是具不具备缓冲功能。下面来具体分析一下。
不带缓冲的输入输出
FileInputStream
首先在磁盘上创建一个txt文件,我在D盘根目录创建(文件名为demo.txt),然后使用FileInputStream这个类来读取这个文件。
这是最基本的文件读取方法。这段代码中有几个地方要注意一下。首先是File类型。这个也是处理文件的重要类型,下面先插个队,来先介绍一下File。
File
File用来操作文件,注意,这里是操作文件本身,而不是获得文件的内容,获取文件的内容就需要使用流了。比如上面的demo.txt文件,可以用File类通过文件在系统中的路径获取文件,但File无法读取demo.txt中的内容。通过文件路径创建File类型的对象以后,就可以通过一系列的API来操作文件,比如常用的一些方法:
getName():用于返回文件名或者路径。getPath():返回对象的路径。exists():判断文件是否存在等等。除了操作文件,还可以操作文件夹,比如mkdir()方法可以创建文件夹,经常和exists方法一起使用,判断是否需要创建文件夹,如果需要的文件夹不存在则创建它。
以上代码就是File的基本操作,其他的操作可以参考API,这里不再逐一演示。总之,记住一点,File类型操作的是文件本身,无法操作文件内容。
第二个要注意的是D:\\demo.txt这个路径。我们通常使用windows作为编码的系统,而windows中路径分隔符是单个 \ ,但是在java代码中,需要添加一个\作为转义符,这样才能被java识别为路径分隔符。注意,我这里强调了windows系统,因为好死不死,在linux里面的路径分隔符恰恰是反的 / 。由于我们的代码最终会放在服务器上运行,所以我们不能将路径写死成只有windows系统可以识别的 \\ 。我们需要一个在windows里是 \ ,在linux里是 / 的方法。
这个方法File类也帮我们做好了,就是File.separator,将上面的路径改造为平台无关的写法就是
"D:" + File.separator + "demo.txt"
平台无关的路径分隔符:意思就是无论哪个平台都可以获取正确的路径分隔符,在windows下File.separator是 \,在linux下File.separator是 / 。
好了,File的基本操作介绍完了,下面继续介绍流。使用File类型根据文件路径创建一个文件的对象,然后用这个对象作为FileInputStream输入流的构造器参数,创建一个输入流。这样就可以通过流来获取文件的内容了。上例中,通过while循环逐个字节的读取文件中的内容,然后转换为char类型进行输出。
来看一下FileInputStream的构造器。FileInputStream有两个我们常用的构造器,一个接受File类型参数,就是上例中的写法。还有一个构造器接受一个字符串的参数,也就是文件路径。
这里顺便复习一下this关键字,在构造器中的this表示调用这个类的另一个不同参数的构造器,来看看this后面括号表示的是什么意思。如果参数中的文件不为空,那么就根据参数地址创建一个匿名文件对象,然后调用下面这个参数为File类型的构造器,所以上例中可以省略掉File对象的创建,直接给流传递一个文件路径也是可以的,因为接受字符串的构造器也可以完成创建File类型对象的工作。
FileInputStream fis = new FileInputStream("D:\\demo.txt");
但是这种输出方式有缺点,上一章介绍过中文编码,由于中文数量过于庞大,所以根据编码表中的编码,一个汉字可能占用2个或者3个甚至4个字节,这样如果逐个字节输出的话,当需要输出的内容是中文的时候,就会出现乱码。因为可能只输出了二分之一个或者三分之一个中文,这样没法显示一个完整的中文,只能是乱码。所以如果需要输出一个正确的中文,需要对代码进行改造。
改造的话就不能再是逐个字节的输出,而是需要将多个字节放在一起,同时读出来。
这样将多个字节内容,通过String的构造器将字节转换为字符串,就可以正确的输出中文了。
为什么不读取一个视频或者图片,而要读取一个文本文件?文本文件不是应该使用字符流吗?
因为这里使用文本文件方便演示,如果读取一个图片或者视频,Eclipse没有办法来展示读取的图片或者视频,所以用文本文件来做例子比较方便。
FileOutputStream
既然输入流是读取文件的内容,那么相对应的,输出流就是将内容写入到文件中。下面来看看如何将内容写入文件。
首先看代码,首先是系统无关的分隔符写法,这里没有使用 \\ 而是使用File.separator替代。另外,和输入流类似的,输出流也有字符串参数的构造器。在这个构造器中,也有将文件路径转为File对象的操作,所以这里没有创建File对象的过程。
与输入流对应的,输出流将字符转为对应的int,然后逐个将int使用输出流的write方法,写入到文件中。除了使用int类型写入文件,还可以使用字节写入文件,这里与输入流操作类似,就不在过多解释,各位可以参照上面输入流的方法和API自行完成。
还是给点提示吧,字符串转为字节,可以使用getBytes()方法完成。上例中不再需要循环逐个读入字符,而是将str转为字节,str.getBytes(),然后用输出流fos调用write方法的重载方法write(str.getBytes())即可。
具有缓冲功能的输入输出
介绍完两个最基本的输入输出流后,再来看看具有缓冲功能的流如何使用。在看代码之前,首先要弄清楚,什么是具有缓冲功能。
上面讲解的普通流是逐个字节进行输入或输出,这样虽然可以完成工作,但是在效率上有很大的问题。当我们将文件读取的时候,会先加载到内存,然而刚刚加载了一个字节到内存,马上又要告诉磁盘,喂~大兄弟,给我把这个字节写到磁盘上,我们知道磁盘的效率比内存要低很多的,在磁盘写入的过程中,内存只能干瞪眼,当磁盘写完一个字节后,内存再把下一个字节交给磁盘,喂~大兄弟,继续写下一个,然后内存又等着磁盘写下一个字节。
普通流效率低下的最大原因就在于此,频繁的调用磁盘,导致无法发挥内存速度快的优点。于是为了提高效率,缓冲流出现了。看看缓冲流缓冲了什么?缓冲流并不是每一个字节都要调用一次磁盘,而是根据设置的缓冲区大小,每当缓冲区满了以后,再调用一次磁盘,比如上图中,缓冲区设置为3,结果就是每次缓冲区有3个字节的数据以后,再调用一次磁盘,这样一来,调用磁盘的次数就减少了很多,使效率得到了很大的提升。文件越大,缓冲流效率的提升越明显。
下面来看一个例子,首先是普通流。
这里的普通流没有设置缓冲区,逐个字节进行文件读入和写入,花了17秒完成5m文件的复制。这里要注意的是流是需要关闭的,如果不关闭流可能会出现资源被占用或者内存泄漏的问题,通常在finally中关闭流,避免导致没有执行到流的关闭就抛出异常导致关闭流不成功。
使用缓冲流进行文件的复制,可以看到文件的复制效率提高了很多。缓冲流的创建,需要InputStream子类作为参数,除了将普通流外面包装了一层,其他代码与普通流没有区别,这种包一层就能有更强功能的流,还有个名称叫做高级流,这种包一层的做法,有种更优雅的名称---“装饰模式”。关于装饰模式,后面章节来具体说明。
缓冲流自带缓冲区,这个缓冲区多大?
理解了普通流的用法,缓冲流用起来没有什么难度,它仅仅是包装了一层而已,所以当我们需要对磁盘上的文件进行读写操作的时候,建议使用缓冲流,效率要高很多。