Java——随机读写文件RandomAccessFile

张开发
2026/5/15 11:05:46 15 分钟阅读

分享文章

Java——随机读写文件RandomAccessFile
随机读写文件RandomAccessFile1、用法2、设计一个键值数据库BasicDB2.1、功能2.2、接口2.3、使用2.4、设计3、BasicDB的实现1、用法RandomAccessFile有如下构造方法publicRandomAccessFile(Stringname,Stringmode)throwsFileNotFoundExceptionpublicRandomAccessFile(Filefile,Stringmode)throwsFileNotFoundException参数name和file容易理解表示文件路径和File对象mode是什么意思呢它表示打开模式可以有4个取ç“r”只用于读。“rw”用于读和写。“rws”和rw一样用于读和写另外它要求文件内容和元数据的任何更新都同步到设备上。“rwd”和rw一样用于读和写另外它要求文件内容的任何更新都同步到设备上和rws的区别是元数据的更新不要求同步。RandomAccessFile虽然不是InputStream/OutputStream的子类但它也有类似于读写字节流的方法。另外它还实现了DataInput/DataOutput接口。这些方法我们之前基本都介绍过这里列举部分方法以增强直观感受//读一个字节取最低8位0255publicintread()throwsIOExceptionpublicintread(byteb[])throwsIOExceptionpublicfinalintreadInt()throwsIOExceptionpublicfinalvoidwriteInt(intv)throwsIOExceptionpublicvoidwrite(byteb[])throwsIOExceptionRandomAccessFile还有另外两个read方法publicfinalvoidreadFully(byteb[])throwsIOExceptionpublicfinalvoidreadFully(byteb[],intoff,intlen)throwsIOException与对应的read方法的区别是它们可以确保读够期望的长度如果到了文件结尾也没读够它们会抛出EOFException异常。RandomAccessFile内部有一个文件指针指向当前读写的位置各种read/write操作都会自动更新该指针。与流不同的是RandomAccessFile可以获取该指针也可以更改该指针相关方法是//获取当前文件指针publicnativelonggetFilePointer()throwsIOException//更改当前文件指针到pospublicnativevoidseek(longpos)throwsIOExceptionRandomAccessFile是通过本地方法最终调用操作系统的API来实现文件指针调整的。InputStream有一个skip方法可以跳过输入流中n个字节默认情况下它是通过实际读取n个字节实现的。RandomAccessFile有一个类似方法不过它是通过更改文件指针实现的publicintskipBytes(intn)throwsIOExceptionRandomAccessFile可以直接获取文件长度返回文件字节数方法为publicnativelonglength()throwsIOException它还可以直接修改文件长度方法为publicnativevoidsetLength(longnewLength)throwsIOException如果当前文件的长度小于newLength则文件会扩展扩展部分的内容未定义。如果当前文件的长度大于newLength则文件会收缩多出的部分会截取如果当前文件指针比newLength大则调用后会变为newLength。RandomAccessFile中有如下方法需要注意一下publicfinalvoidwriteBytes(Strings)throwsIOExceptionpublicfinalStringreadLine()throwsIOException看上去writeBytes方法可以直接写入字符串而readLine方法可以按行读入字符串实际上这两个方法都是有问题的它们都没有编码的概念都假定一个字节就代表一个字符这对于中文显然是不成立的所以应避免使用这两个方法。2、设计一个键值数据库BasicDB在日常的一般文件读写中使用流就可以了但在一些系统程序中流是不适合的 RandomAccessFile因为更接近操作系统更为方便和高效。2.1、功能BasicDB提供的接口类似于Map接口可以按键保存、查找、删除但数据可以持久化保存到文件上。此外不像HashMap/TreeMap它们将所有数据保存在内存BasicDB只把元数据如索引信息保存在内存值的数据保存在文件上。相比HashMap/TreeMap, BasicDB的内存消耗可以大大降低存储的键值对个数大大提高尤其当值数据比较大的时候。BasicDB通过索引以及RandomAccessFile的随机读写功能保证效率。2.2、接口对外BasicDB提供的构造方法是publicBasicDB(Stringpath,Stringname)throwsIOExceptionpath表示数据库文件所在的目录该目录必须已存在。name表示数据库的名称 BasicDB会使用以name开头的两个文件一个存储元数据扩展名是meta一个存储键值对中的值数据扩展名是data。比如如果name为student则两个文件为student.meta和student.data这两个文件不一定存在如果不存在则创建新的数据库如果已存在则加载已有的数据库。BasicDB提供的公开方法有//保存键值对键为String类型值为byte数组publicvoidput(Stringkey,byte[]value)throwsIOException//根据键获取值如果键不存在返回nullpublicbyte[]get(Stringkey)throwsIOExceptionpublicvoidremove(Stringkey)//根据键删除publicvoidflush()throwsIOException//确保将所有数据保存到文件publicvoidclose()throwsIOException//关闭数据库为便于实现我们假定值即byte数组的长度不超过1020如果超过会抛出异常当然这个长度在代码中可以调整。在调用put和remove后修改不会马上反映到文件中如果需要确保保存到文件中需要调用flush。2.3、使用在BasicDB中我们设计的值为byte数组这看上去是一个限制不便使用我们主要是为了简化而且任何数据都可以转化为byte数组保存。对于字符串可以使用getBytes()方法对于对象可以使用之前介绍的流转换为byte数组。比如保存一些学生信息到数据库代码可以为privatestaticbyte[]toBytes(Studentstudent)throwsIOException{ByteArrayOutputStreamboutnewByteArrayOutputStream();DataOutputStreamdoutnewDataOutputStream(bout);dout.writeUTF(student.getName());dout.writeInt(student.getAge());dout.writeDouble(student.getScore());returnbout.toByteArray();}publicstaticvoidsaveStudents(MapString,Studentstudents)throwsIOException{BasicDBdbnewBasicDB(./,students);for(Map.EntryString,Studentkv:students.entrySet()){db.put(kv.getKey(),toBytes(kv.getValue()));}db.close();}保存学生信息到当前目录下的students数据库toBytes方法将Student转换为了字节。2.4、设计将键值对分为两部分值保存在单独的data文件中值在data文件中的位置和键称为索引索引保存在meta文件中。在data文件中每个值占用的空间固定固定长度为1024前4个字节表示实际长度然后是实际内容实际长度不够1020的后面是补白字节0。索引信息既保存在meta文件中也保存在内存中在初始化时全部读入内存对索引的更新不立即更新文件调用flush方法才更新。删除键值对不修改data文件但会从索引中删除并记录空白空间下次添加键值对的时候会重用空白空间所有的空白空间也记录到meta文件中。3、BasicDB的实现BasicDB定义了如下静态变量privatestaticfinalintMAX_DATA_LENGTH1020;//补白字节privatestaticfinalbyte[]ZERO_BYTESnewbyte[MAX_DATA_LENGTH];//数据文件扩展名privatestaticfinalStringDATA_SUFFIX.data;//元数据文件扩展名包括索引和空白空间数据privatestaticfinalStringMETA_SUFFIX.meta;内存中表示索引和空白空间的数据结构是MapString,LongindexMap;//索引信息键-值在data文件中的位置QueueLonggaps;//空白空间值为在data文件中的位置表示文件的数据结构是RandomAccessFiledb;//值数据文件FilemetaFile;//元数据文件构造方法的代码为publicBasicDB(Stringpath,Stringname)throwsIOException{FiledataFilenewFile(pathnameDATA_SUFFIX);metaFilenewFile(pathnameMETA_SUFFIX);dbnewRandomAccessFile(dataFile,rw);if(metaFile.exists()){loadMeta();}else{indexMapnewHashMap();gapsnewArrayDeque();}}元数据文件存在时会调用loadMeta将元数据加载到内存我们先假定不存在先来看其他代码。保存键值对的方法是put其代码为publicvoidput(Stringkey,byte[]value)throwsIOException{LongindexindexMap.get(key);if(indexnull){indexnextAvailablePos();indexMap.put(key,index);}writeData(index,value);}先通过索引查找键是否存在如果不存在调用nextAvailablePos方法为值找一个存储位置并将键和存储位置保存到索引中最后调用writeData方法将值写到数据文件中。nextAvailablePos的代码是privatelongnextAvailablePos()throwsIOException{if(!gaps.isEmpty()){returngaps.poll();}else{returndb.length();}}它首先查找空白空间如果有则重用否则定位到文件末尾。writeData方法实际写值数据它的代码是privatevoidwriteData(longpos,byte[]data)throwsIOException{if(data.lengthMAX_DATA_LENGTH){thrownewIllegalArgumentException(maximum allowed length is MAX_DATA_LENGTH, data length is data.length);}db.seek(pos);db.writeInt(data.length);db.write(data);db.write(ZERO_BYTES,0,MAX_DATA_LENGTH-data.length);}它先检查长度长度满足的情况下定位到指定位置写实际数据的长度、写内容、最后补白。可以看出在这个实现中索引信息和空白空间信息并没有实时保存到文件中要保存需要调用flush方法待会我们再看这个方法。根据键获取值的方法是get其代码为publicbyte[]get(Stringkey)throwsIOException{LongindexindexMap.get(key);if(index!null){returngetData(index);}returnnull;}如果键存在就调用getData方法获取数据。getData方法的代码为privatebyte[]getData(longpos)throwsIOException{db.seek(pos);intlengthdb.readInt();byte[]datanewbyte[length];db.readFully(data);returndata;}代码也很简单定位到指定位置读取实际长度然后调用readFully方法读够内容。删除键值对的方法是remove其代码为publicvoidremove(Stringkey){LongindexindexMap.remove(key);if(index!null){gaps.offer(index);}}从索引结构中删除并添加到空白空间队列中。同步元数据的方法是flush()其代码为publicvoidflush()throwsIOException{saveMeta();db.getFD().sync();}回顾一下getFD方法会返回文件描述符其sync方法会确保文件内容保存到设备上 saveMeta方法的代码为privatevoidsaveMeta()throwsIOException{DataOutputStreamoutnewDataOutputStream(newBufferedOutputStream(newFileOutputStream(metaFile)));try{saveIndex(out);saveGaps(out);}finally{out.close();}}索引信息和空白空间保存在一个文件中saveIndex保存索引信息代码为privatevoidsaveIndex(DataOutputStreamout)throwsIOException{out.writeInt(indexMap.size());for(Map.EntryString,Longentry:indexMap.entrySet()){out.writeUTF(entry.getKey());out.writeLong(entry.getValue());}}先保存键值对个数然后针对每条索引信息保存键及值在data文件中的位置。saveGaps方法保存空白空间信息代码为privatevoidsaveGaps(DataOutputStreamout)throwsIOException{out.writeInt(gaps.size());for(Longpos:gaps){out.writeLong(pos);}}也是先保存长度然后保存每条空白空间信息。在构造方法中我们提到了loadMeta方法它是saveMeta的逆操作代码为privatevoidloadMeta()throwsIOException{DataInputStreaminnewDataInputStream(newBufferedInputStream(newFileInputStream(metaFile)));try{loadIndex(in);loadGaps(in);}finally{in.close();}}loadIndex加载索引代码为privatevoidloadIndex(DataInputStreamin)throwsIOException{intsizein.readInt();indexMapnewHashMapString,Long((int)(size/0.75f)1,0.75f);for(inti0;isize;i){Stringkeyin.readUTF();longindexin.readLong();indexMap.put(key,index);}}loadGaps加载空白空间代码为privatevoidloadGaps(DataInputStreamin)throwsIOException{intsizein.readInt();gapsnewArrayDeque(size);for(inti0;isize;i){longindexin.readLong();gaps.add(index);}}数据库关闭的代码为publicvoidclose()throwsIOException{flush();db.close();}

更多文章