利用java序列化实现基于文件的快速索引 博客分类: 干货 序列化与反序列化快速索引Java数据保存协议
本篇文章重点关注以下问题:
- 规定数据存放协议,加快索引
- java利用序列化与反序列化实现对象的读写
- 对象、字节数组、文件之间的转换
- 整合Demo
1. 数据在文件中的存放协议
在面向对象的语言中,将数据以对象的形式保存极为常见,而通过序列化把对象直接保存在文件或数据库中,再反序列化得到对象,中间过程无需自己解析也极为方便,但是因为对象序列化后需要保存类信息,存储空间开销较大,如何建立快速索引,避免不必要的开销则至关重要。
首先规定每条记录的存储格式:索引(4byte) + 记录长度(4byte) + 数据(变长);
- 索引为记录的唯一标识,如若命中索引,则返回数据部分;如若为命中,继续查找下一记录。(索引大小可自定义)
- 记录长度为数据部分所占的存储空间,程序可根据此字段跳过此条记录数据部分的读取。(记录长度的大小可自定义)
- 数据部分存储数据,不同对象可能会占用不同的空间。
- 写文件:以流的方式写文件,将每条记录以上述协议一条一条写入文件,即可将对象直接序列化至文件,也可先将对象转换成字节数组后保存至文件。
- 读文件:以流的方式读文件,从文件头开始扫描,每条记录只读前8byte(索引+记录长度),如若命中索引,则读取数据部分后直接返回对象;如若未命中记录,则skip(跳过)数据部分,继续下一条数据的比对。
2. java利用序列化与反序列化实现对象的读写
2.1 定义实体类
实体类为需要序列化保存的数据,需要注意的是所有序列化保存的对象必须实现Serializable接口(Externalnalizable也行)。
/** * 实体类保存信息 * @author Administrator */ public class PersonEntity implements Serializable { private static final long serialVersionUID = -5802989937522643573L; private String name; private int age; public PersonEntity(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "PersonEntity [name=" + name + ", age=" + age + "]"; } }
2.2 序列化、反序列化对象,并实现数据读写
/** * 将对象向文件中写、读 * @author Administrator */ public class Main { private static String FILE_PATH = "E:/temp.cache"; private static File FILE_TEMP; static { try { FILE_TEMP = new File(FILE_PATH); FILE_TEMP.delete(); FILE_TEMP.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException, ClassNotFoundException { try ( FileOutputStream outF = new FileOutputStream(FILE_TEMP); ObjectOutputStream out = new ObjectOutputStream(outF)) { /* 准备写入文件的对象(数据部分). */ PersonEntity entity1 = new PersonEntity("熊燕子", 26); PersonEntity entity2 = new PersonEntity("军", 25); /* 写第一条记录. */ out.writeInt(1); out.writeInt(64); out.writeObject(entity1); /* 写第二条记录. */ out.writeInt(2); out.writeInt(128); out.writeObject(entity2); /* 将数据刷入文件. */ out.flush(); } try ( FileInputStream inF = new FileInputStream(FILE_TEMP); ObjectInputStream in = new ObjectInputStream(inF)) { /* 读第一条记录. */ int no1 = in.readInt(); int len1 = in.readInt(); PersonEntity entity1 = (PersonEntity) in.readObject(); /* 读第二条记录. */ int no2 = in.readInt(); int len2 = in.readInt(); PersonEntity entity2 = (PersonEntity) in.readObject(); System.out.println("no1 = " + no1 + "; len1 = " + len1 + "; " + entity1); System.out.println("no2 = " + no2 + "; len2 = " + len2 + "; " + entity2); } } }
上述运行结果为:
从运行结果可以看出对象被正确写入文件、正确从文件中读取,但是上述过程并没有发挥快速检索的优势,仍然是从流中依次读取每个字段。分析ObjectOutputStream、ObjectInputStream的API,貌似并不支持对象的skip方法,因此,可以考虑将对象转化为字节数组写入流中,从流中读取对象的字节数组后转为我们所需要的对象。那么,关键问题就转为如何实现对象和字节数组之间的等价转换。
3. 对象、字节数组、文件之间的转换
直接上转换的工具类:
package com.wj.serializable; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Byte_File_Object { /** * 将文件内容转化为字节数组 * @param file 文件 * @return * @throws IOException */ public static byte[] getBytesFromFile (File file) throws IOException { if (file == null) return null; try ( FileInputStream inF = new FileInputStream(file); ByteArrayOutputStream out = new ByteArrayOutputStream()) { byte[] bs = new byte[1024]; int readLen = 0; while ((readLen = inF.read(bs)) != -1) { out.write(bs, 0, readLen); } return out.toByteArray(); } } /** * 把字节数组保存为一个文件 * @param bs 待保存的字节数组 * @param filePath 文件路径 * @return * @throws IOException */ public static File getFileFromBytes(byte[] bs, String filePath) throws IOException { File file = new File(filePath); try ( FileOutputStream fileOutPutStream = new FileOutputStream(file); BufferedOutputStream bufferOutputStream = new BufferedOutputStream(fileOutPutStream)) { bufferOutputStream.write(bs); } return file; } /** * 将字节数组转化为对象 * @param objBytes 待转化的对象 * @return * @throws IOException * @throws ClassNotFoundException */ public static Object getObjectFromBytes(byte[] objBytes) throws IOException, ClassNotFoundException { if (objBytes == null || objBytes.length == 0) return null; ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(objBytes); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); return objectInputStream.readObject(); } /** * 将对象转化为字节数组 * @param obj * @return * @throws IOException */ public static byte[] getBytesFromObject(Serializable obj) throws IOException { if (obj == null) return null; ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); return byteArrayOutputStream.toByteArray(); } }
只要了解java流的读写API都是基于装饰者模式,很容易理解上述代码,对象、文件、字节数组之间的转化也很简单。下面将对象的序列化读写操作和上述对象、字节数组之间的转化相结合,实现基于文件存储的快速索引。
4. 整合利用java序列化与反序列化的基于文件的快速索引Demo
package com.wj.serializable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; /** * 快速索引Demo * 按照每条记录的协议:索引(4byte) + 数据长度(4byte) + 数据部分(变长) 的格式写入三条记录 * 读取数据的时候: * * 第一条数据假设命中,读取数据部分; * * 第二条记录假设未命中,跳过数据部分; * * 第三条记录假设命中,读取数据部分 * 若可正确读取,则表示此方式可行(性能项目中实测提升名明显,这里未做测试) */ public class QuickIndex { private static String FILE_PATH = "E:/temp.cache"; private static File FILE_TEMP; static { try { FILE_TEMP = new File(FILE_PATH); FILE_TEMP.delete(); FILE_TEMP.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { /** 第一部分连续写入三条记录. */ try ( FileOutputStream outF = new FileOutputStream(FILE_TEMP); ObjectOutputStream out = new ObjectOutputStream(outF)) { /* 准备写入文件的对象(数据部分). */ PersonEntity entity1 = new PersonEntity("熊燕子", 26); PersonEntity entity2 = new PersonEntity("小王", 25); PersonEntity entity3 = new PersonEntity("大王", 49); /* 写第一条记录. */ byte[] objBytes1 = Byte_File_Object.getBytesFromObject(entity1); int no1 = 1; int len1 = objBytes1.length; out.writeInt(no1); out.writeInt(len1); out.write(objBytes1); /* 写第二条记录. */ byte[] objBytes2 = Byte_File_Object.getBytesFromObject(entity2); int no2 = 2; int len2 = objBytes2.length; out.writeInt(no2); out.writeInt(len2); out.write(objBytes2); /* 写第三条记录. */ byte[] objBytes3 = Byte_File_Object.getBytesFromObject(entity3); int no3 = 3; int len3 = objBytes3.length; out.writeInt(no3); out.writeInt(len3); out.write(objBytes3); /* 将数据刷入文件. */ out.flush(); } /** 第二部分读取记录(第二条记录skip). */ try ( FileInputStream inF = new FileInputStream(FILE_TEMP); ObjectInputStream in = new ObjectInputStream(inF)) { /* 读第一条记录. */ int no1 = in.readInt(); int len1 = in.readInt(); byte[] byteArray1 = readAccurateLenBytesFromInStream(in, len1); PersonEntity entity1 = (PersonEntity) Byte_File_Object.getObjectFromBytes(byteArray1); /* 读第二条记录. */ int no2 = in.readInt(); int len2 = in.readInt(); in.skip(len2); // 跳过第二条记录的数据部分 /* 读第三条记录. */ int no3 = in.readInt(); int len3 = in.readInt(); byte[] byteArray3 = readAccurateLenBytesFromInStream(in, len3); PersonEntity entity3 = (PersonEntity) Byte_File_Object.getObjectFromBytes(byteArray3); System.out.println("no1 = " + no1 + "; len1 = " + len1 + "; " + entity1); System.out.println("no2 = " + no2 + "; len2 = " + len2 + "; "); System.out.println("no3 = " + no3 + "; len3 = " + len3 + "; " + entity3); } } /** * 从输入流中读取指定长度的字节,保存到字节数组中 * @param in 输入流 * @param len 读取的字节数 * @return 返回字节数组 * @throws IOException */ private static byte[] readAccurateLenBytesFromInStream(ObjectInputStream in, int len) throws IOException { byte[] bs = new byte[len]; int tempLen = 0; int readLen = 0; while (true) { tempLen = in.read(bs, readLen, len - readLen); readLen += tempLen; if (readLen == len) break; } return bs; } }
运行结果为:
可以发现,读取流的过程中,成功跳过第二条记录的数据部分,正确读出第三条记录。
代码中的相关说明已经很详细,我这里也不做过多阐述,不过其中有一点值得一提,java从输入流以阻塞方式读取数据时,受操作系统保存文件的分区或网络传输的影响,一次读取的字节数组大小并不确定,因此提取readAccurateLenBytesFromInStream方法从流中读取指定大小的字节数组,从而正确转化为对象。
代码下载地址:http://pan.baidu.com/s/1dE5HMH7,密码:h7hl