I/O 问题是任何编程语言都无法回避的问题,可以说 I/O 问题是整个人机交互的核心问题,因为 I/O 是机器获取和交换信息的主要渠道。在当今这个数据大爆炸时代,I/O 问题尤其突出,很容易成为一个性能瓶颈。
本文的目的是分析 I/O 的内在工作机制,以及java I/O类库的基本架构,以帮助初学者从总体上了解Java I/O。具体各个I/O类库的使用,请读者在遇到实际问题时,结合本文的讲解来理解掌握。
1.社么是I/O
描述Java的I/O机制,我们用从河中抽水的例子来解释读文件的过程。
要将河水抽出来,首先我们需要一个抽水机,然后把抽水机丢到河流里。这里,河水相当于文件,我们需要建立一个抽水机,并将抽水机与河水关联起来:
FileInputStream fis = new FileInputStream("E:/rivier.txt");
fis就是抽水机,E:/rivier.txt就是河水。
设备准备好了,就可以打开水龙头获得水啦,但获得的水需要存放,所以第二步,我们还需要先有一个盛水的容器:
byte[] bottle = new byte[10];
容器bottle是一个byte类型的数组,大小为10.之所以将容器定义为byte数组而不是其它数组类型,是因为我们使用的这个抽水机比较特殊,抽出来的水最小单位就是1个byte(你就当1滴水来理解吧)。
接下来,将容器放到水龙头下面,打开水龙头,就开始将水抽进容器里了。
fis.read(bottle);
这样抽水机fis就抽出了10滴水(10个byte)放到bottle这个容器里面了。然后bottle里的数据就可以任意有我们处置了。
Java为我们提供了两种类型的抽水机,第一种抽水机的名字叫做XXXInputStream,它抽水的单位是字节,当然盛水的容器自然就是字节数组byte[]。
第二种抽水机的名字叫做XXXReader,它抽水的单位是字符,盛水容器就是字符数组char[]。
也就是说,我们如果我们用第一种抽水机XXXInputStream,抽出来的是byte;如果用第二种抽水机XXXReader,抽出来的是char。(有关字节byte和字符char的区别,自己脑补...)
这里的XXX表示抽的水是河水,还是湖水、海水、污水......,即XXX表示数据的来源。如FileInputStream,表示这个抽水机是专门从文件中读取数据的;ByteArrayInputStream,就是专门从字符数组也就是内存中读数据的。
小结一下,要读一个文件,步骤如下:
1. 实例化一个XXXInputStream对象(选择一种抽水机,指定水源)
2. 确定接收数据的对象(准备容器)
3. read()(打开水龙头)
现在,通过上面的抽水机,用户可以从各种地方,按各种大小的被子来得到水。但用户觉得抽水机功能还是弱了点, 如果打开水龙头,就是热水,就是热咖啡就好啦!
显然抽水机已经力不从心了,如果把加热,煮咖啡的功能设计到抽水机里面,那么抽水机会显得太庞杂,也增加了出故障时维修的难度。
所以,我们换一种思路,在抽水机的水龙头下,接一根加热管:
FileInputStream fis = new FileInputStream("e:/yellow_river.txt");//准备好抽水机 ObjectInputStream ois= new ObjectInputStream(fis);//增加一个加热器
这样,只需要调用加热管的水龙头,就可以获得热水啦:
ois.readDouble();//读出来的就是一个double型的数据,不再是byte数据(即抽出的就是热水)
所以,总的来说,在Java中读文件,就像抽水,首先,需要根据读取单位,根据读取源,选择合适的抽水机。如果需要,还可以在抽水机后添加管子,增强功能。
请试着按照排水的例子,描述写文件的机制。
2. 实例理解
现在我们结合上面的例子,来理解几个实例。
1)按字节读文件FileInputStream
FileInputStream fis = new FileInputStream("e:/in.txt"); //创建输入流对象(抽水机) while (fis.available() > 0) { byte[] b = new byte[10]; //创建数组(盛水容器) int nResult = fis.read(b); //读数据到数组,并且读到数据时,nResult被赋值为>-1的整数,表示实际读了多少个byte if (nResult == -1) //文件中的数据读完了,没数据可读了 break; System.out.println(new String(b)); } fis.close(); //数据读完了,抽水机需要关掉
2)按字符读文件FileReader:
FileReader rdFile = new FileReader("e:/in.txt"); //实例化文件输入流对象(抽水机) while (rdFile.ready()) { //输入流是否准备好读(抽水机是否准备好抽水) char[] chIn = new char[10]; //准备容器 int nResult = rdFile.read(chIn); //读数据 if (nResult == -1) //如果没有读到数据 、 break; System.out.println(chIn); } rdFile.close(); //不抽水了,关掉抽水机
FileReader满足XXXReader命名规则,从名字可以判断是按字符来读数据,并且数据来源是文件。
3.带缓冲区的读
还是用抽水的例子来讲解什么是缓冲区。我们在使用水的时候,可以有两种方法,一种是每次要用水,就去打开水龙头,等着它将容器装满。第二种方法是,家里有一个大水缸,先将大水缸装满,每次用水的时候,直接去水缸里取。当水缸里的谁不够用时,再打开水龙头将大水缸放满。
不考虑水存放时间长了会变质的影响,显然第二种方法效率高。
文件通常放在磁盘等设备中,读写速度远没有内存的速度快。上面FileInputStream或FileReader每次执行read()方法,都会去硬盘读取一次。如果事先将文件中的数据读取一定大小到内存中,以后每次read(),从这块内存中读,就会减少硬盘的读写次数,效率高得多。如果这块内存中的数据不够用了,再去硬盘继续读数据把这块内存填满,后面继续使用这块内存。这块内存就是缓冲区。
用法示例:
FileReader rdFile = new FileReader("e:/in.txt"); //创建字符文件输入流对象 BufferedReader brdFile = new BufferedReader(rdFile); //创建一个BufferedReader对象 String strLine; while ((strLine = brdFile.readLine()) != null) { System.out.println(strLine); } brdFile.close();
例子中,将rdFile这个FileReader对象包装在BufferedReader对象brdFile里,直接操作brdFile对象,就可以实现带缓冲功能的读操作了。
同样,按字节读可以使用BufferedInputStream类来对InputSteam进行包装。
4. I/O体系结构
我们已经了解了Java I/O 系统,并学会了一些简单的I/O操作,接下来请打开Java Doc,让我们看看Sun公司是怎样通过面向对象的思想,设计I/O系统的。
总结一下:
1.InputStream和OutputStream是按字节读写数据的抽象类,是所有字节输入输出流的基类。
2. Reader和Writer是按字符读写数据的抽象类,是所有字符输入输出流的基类。
3. 继承它们的都是针对特定数据源的“抽水机”或“排水机”,提供了read和write的基本实现。
4. 构造方法一般都需要提供数据源。