name=name+"\u0000";
}
this.name=name;
this.age=age;
}
}
运行结果:
第二个员工信息:
name:
Lisi
age:
24
第一个员工的信息:
name:
zhangsan
age:
23
第三个员工的信息:
name:
Wangwu
age:
25
c盘还多了个文件1.txt:
图11.1
上面的这个程序完成了我们想要的功能,演示了RandomAccessFile类的作用。
String.substring(int beginIndex,int endIndex)方法可以用于取出一个字符串中的部分子字符串,要注意的一个细节是:
子字符串中的第一个字符对应的是原字符串中的脚标为beginIndex处的字符,但最后的字符对应的是原字符串中的脚标为endIndex-1处的字符,而不是endIndex处的字符。
在实际生活中,我们常用的数据库和数据库管理工具实际上就是这种原理。
我们的1.txt就相当于数据库的数据文件,而我们这个程序提供了往这个数据文件写入和读取数据的功能。
11.3节点流
11.3.1理解流的概念
数据流是一串连续不断的数据的集合,就象水管里的水流,在水管的一端一点一点地供水,而在水管的另一端看到的是一股连续不断的水流。
数据写入程序可以是一段、一段地向数据流管道中写入数据,这些数据段会按先后顺序形成一个长的数据流。
对数据读取程序来说,看不到数据流在写入时的分段情况,每次可以读取其中的任意长度的数据,但只能先读取前面的数据后,再读取后面的数据。
不管写入时是将数据分多次写入,还是作为一个整体一次写入,读取时的效果都是完全一样的。
我们将IO流类分为两个大类,节点流类和过滤流类(也叫处理流类)。
程序用于直接操作目标设备所对应的类叫节点流类,程序也可以通过一个间接流类去调用节点流类,以达到更加灵活方便地读写各种类型的数据,这个间接流类就是过滤流类(也叫处理流类),我更喜欢称之为包装类。
不管叫什么,都只是一个代名词而已,读者不要太在意,你可以根据自己的习惯和喜好来定。
11.3.2InputStream与OutputStream
程序可以从中连续读取字节的对象叫输入流,用InputStream类完成,程序能向其中连续写入字节的对象叫输出流,用OutputStream类完成。
InputStream与OutputStream对象是两个抽象类,还不能表明具体对应哪种IO设备。
它们下面有许多子类,包括网络,管道,内存,文件等具体的IO设备,如FileInputStream类对应的就是文件输入流,是一个节点流类,我们将这些节点流类所对应的IO源和目标称为流节点(Node)。
很多人搞不清程序要将A文件的内容写入B文件中,程序对A文件的操作所用的是输出类还是输入类这个问题。
读者也先自己想想,再记住下面的话,输入输出类是相对程序而言的,而不是代表文件的,所以我们应该创建一个输入类来完成对A文件的操作,创建一个输出类来完成对B文件的操作。
InputStream定义了Java的输入流模型。
该类中的所有方法在遇到错误的时候都会引发IOException异常,下面是InputStream类中方法的一个简要说明:
✓intread()返回下一个输入字节的整型表示,,如果返回-1表示遇到流的末尾,结束。
✓intread(byte[]b)读入b.length个字节放到b中并返回实际读入的字节数。
✓intread(byte[]b,intoff,intlen)这个方法表示把流中的数据读到,数组b中,第off个开始的len个数组元素中.
✓longskip(longn)跳过输入流上的n个字节并返回实际跳过的字节数。
✓intavailabale()返回当前输入流中可读的字节数。
✓voidmark(intreadlimit)在输入流的当前位置处放上一个标志,允许最多再读入readlimit个字节。
✓voidreset()把输入指针返回到以前所做的标志处。
✓booleanmarkSupported()如果当前流支持mark/reset操作就返回true。
✓voidclose()在操作完一个流后要使用此方法将其关闭,系统就会释放与这个流相关的资源。
InputStream是一个抽象类,程序中实际使用的是它的各种子类对象。
不是所有的子类都会支持InputStream中定义的某些方法的,如skip,mark,reset等,这些方法只对某些子类有用。
一个对象在没有引用变量指向它时会变成垃圾,最终会被垃圾回收器从内存中清除。
对于我们创建的流对象,干嘛还要“调用close方法将它关闭,以释放与其相关的资源”呢?
这相关的资源到底是些什么呢?
我们在程序中创建的对象都是对应现实世界中有形或无形的事物,计算机操作系统所产生的东西当然也是现实世界中的事物,也就是说,程序中的对象也可以对应计算机操作系统所产生的一个其他东西,专业地说,这些东西叫资源,流就是操作系统产生的一种资源。
当我们在程序中创建了一个IO流对象,同时系统内也会创建了一个叫流的东西,在这种情况下,计算机内存中实际上产生了两个事物,一个是Java程序中的类的实例对象,一个是系统本身产生的某种资源,我们以后讲到的窗口,Socket等都是这样的情况。
Java垃圾回收器只能管理程序中的类的实例对象,没法去管理系统产生的资源,所以程序需要调用close方法,去通知系统释放其自身产生的资源。
OutputStream是一个定义了输出流的抽象类,这个类中的所有方法均返回void,并在遇到错误时引发IOException异常。
下面是OutputStream的方法:
✓voidwrite(intb)将一个字节写到输出流。
注意,这里的参数是int型,它允许write使用表达式而不用强制转换成byte型。
✓voidwrite(byte[]b)将整个字节数组写到输出流中。
✓voidwrite(byte[]b,intoff,intlen)将字节数组b中的从off开始的len个字节写到输出流。
✓voidflush彻底完成输出并清空缓冲区。
✓voidclose关闭输出流。
计算机访问外部设备,要比直接访问内存慢得多,如果我们每一次write方法的调用都直接写到外部设备(如直接写入硬盘文件),CPU就要花费更多的时间等待外部设备;如果我们开辟一个内存缓冲区,程序的每一次write方法都是写到这个内存缓冲区中,只有这个缓冲区被装满后,系统才将这个缓冲区的内容一次集中写到外部设备。
使用内存缓冲区有两个方面的好处,一是有效地提高了CPU的使用率,二是write并没有马上真正写入到外设,我们还有机会回滚部分写入的数据。
使用缓冲区,能提高整个计算机系统的效率,但也会降低单个程序自身的效率,由于有这么一个中间缓冲区,数据并没有马上写入到目标中去,例如在网络流中,就会造成一些滞后。
对于输入流,我们也可以使用缓冲区技术。
在程序与外部设备之间到底用不用缓冲区,是由编程语言本身决定的,我们通常用的C语言默认情况下就会使用缓冲区,而在Java语言中,有的类使用了缓冲区,有的类没有使用缓冲区,我们还可以在程序中使用专门的包装类来实现自己的缓冲区。
flush方法就是用于即使在缓冲区没有满的情况下,也将缓冲区的内容强制写入到外设,习惯上称这个过程为刷新。
可见,flush方法不是对所有的OutputStream子类都起作用的,它只对那些使用缓冲区的OutputStream子类有效。
如果我们调用了close方法,系统在关闭这个流之前,也会将缓冲区的内容刷新到硬盘文件的。
作者开发过一个邮件服务器程序,需要7*24小时不间断工作,这个服务器程序要面对internet上各种可能的非法格式的数据输入和攻击,而我的程序正好又没考虑到某种非法格式的数据,一旦碰到这样的情况,程序就会崩溃。
有经验的人都知道,为了找出服务器程序崩溃的原因,我们可以将程序每次接收到的数据都记录到一个文件中,当服务器程序崩溃后,我们便打开这个记录文件,查看最后记录的那条数据,这个数据就是让我的程序毙命的罪魁祸首,然后拿着这条数据一步步测试我们的程序,就很容易找出程序中的问题了。
遗憾的是,我每次用最后记录的这条数据测试我的程序,程序均安然无恙。
最后,我发现就是因为有缓冲区的原因,缓冲区的内容还没来得及刷新到硬盘文件,程序就崩溃了,所以,文件中并没有记录最后接收到的那些数据,我在文件中看到的最后以条记录并不是真正最后接收到的那条数据。
发现了这个原因,我修改程序,在每一次调用write语句后,都立即调用flush语句,这样,我就终于找到了肇事元凶,并修复了程序的这个漏洞。
尽管我以前从来没有真正认真思考和编程试验过缓冲区问题,但是正因为还有那么一点点概念和印象,所以,在出现问题时,我才能从多方面去思考并最终解决问题。
我建议读者花更多的时间去开阔自己的知识面和思维,了解更多的原理,而不是去花大量时间去死记硬背某些细节和术语,特别是一个类中的每个函数名的具体拼写、具体的参数形式,Java中有哪些关键字等这些死板的东西,只要有个印象就足够了。
11.3.3FileInputStream与FileOutputStream
这两个流节点用来操作磁盘文件,在创建一个FileInputStream对象时通过构造函数指定文件的路径和名字,当然这个文件应当是存在的和可读的。
在创建一个FileOutputStream对象时指定文件如果存在将要被覆盖。
下面是对同一个磁盘文件创建FileInputStream对象的两种方式。
其中用到的两个构造函数都可以引发FileNotFoundException异常:
FileInputStreaminOne=newFileInputStream("hello.test");
Filef=newFile("hello.test");
FileInputStreaminTwo=newFileInputStream(f);
尽管第一个构造函数更简单,但第二个构造函数允许在把文件连接到输入流之前对文件做进一步分析。
FileOutputStream对象也有两个和FileInputStream对象具有相同参数的构造函数,创建一个FileOutputStream对象时,可以为其指定还不存在的文件名,但不能是存在的目录名,也不能是一个已被其他程序打开了的文件。
FileOutputStream先创建输出对象,然后再准备输出。
其实在上一章中讲Properties类的时候,我们已经使用过这两个类。
在下面的例子中,我们用FileOutputStream类向文件中写入一串字符,并用FileInputStream读出。
程序清单:
FileStream.java
importjava.io.*;
publicclassFileStream
{
publicstaticvoidmain(String[]args)
{
Filef=newFile("hello.txt");
try
{
FileOutputStreamout=newFileOutputStream(f);
bytebuf[]="www.it315.org".getBytes();
out.write(buf);
out.close();
}
catch(Exceptione)
{
System.out.println(e.getMessage());
}
try
{
FileInputStreamin=newFileInputStream(f);
byte[]buf=newbyte[1024];
intlen=in.read(buf);
System.out.println(newString(buf,0,len));
}
catch(Exceptione)
{
System.out.println(e.getMessage());
}
}
}
编译运行上面的程序,我们能够看到当前目录下产生了一个hello.txt的文件,用记事本程序打开这个文件,能看到我们写入的内容。
随后,程序开始读取文件中的内容,并将读取到